mirror of https://github.com/hashicorp/packer
- Startup scripts can be provided through the instance creation metadata field 'startup-script'. - Script log can be copied to a GCS location by setting the metadata field 'startup-script-log-dest'. Added Retry method to googlecompute package. Added GetSerialPortOutput to googlecompute Drivers. Added StepWaitInstanceStartup (and associated test) which waits for an instance startup-script to finish. Changed the instance service account to use the same service account as the one provided in the Packer config template. It was the project default service account. Tested googlecompute package with 'go test' and also performed builds with a startup script and without a startup script.pull/3639/head
parent
37fe764727
commit
7190fbeed8
@ -0,0 +1,44 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
var RetryExhaustedError error = fmt.Errorf("Function never succeeded in Retry")
|
||||
|
||||
// Retry retries a function up to numTries times with exponential backoff.
|
||||
// If numTries == 0, retry indefinitely. If interval == 0, Retry will not delay retrying and there will be
|
||||
// no exponential backoff. If maxInterval == 0, maxInterval is set to +Infinity.
|
||||
// Intervals are in seconds.
|
||||
// Returns an error if initial > max intervals, if retries are exhausted, or if the passed function returns
|
||||
// an error.
|
||||
func Retry(initialInterval float64, maxInterval float64, numTries uint, function func() (bool, error)) error {
|
||||
if maxInterval == 0 {
|
||||
maxInterval = math.Inf(1)
|
||||
} else if initialInterval < 0 || initialInterval > maxInterval {
|
||||
return fmt.Errorf("Invalid retry intervals (negative or initial < max). Initial: %f, Max: %f.", initialInterval, maxInterval)
|
||||
}
|
||||
|
||||
var err error
|
||||
done := false
|
||||
interval := initialInterval
|
||||
for i := uint(0); !done && (numTries == 0 || i < numTries); i++ {
|
||||
done, err = function()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !done {
|
||||
// Retry after delay. Calculate next delay.
|
||||
time.Sleep(time.Duration(interval) * time.Second)
|
||||
interval = math.Min(interval * 2, maxInterval)
|
||||
}
|
||||
}
|
||||
|
||||
if !done {
|
||||
return RetryExhaustedError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRetry(t *testing.T) {
|
||||
numTries := uint(0)
|
||||
// Test that a passing function only gets called once.
|
||||
err := Retry(0, 0, 0, func() (bool, error) {
|
||||
numTries++
|
||||
return true, nil
|
||||
})
|
||||
if numTries != 1 {
|
||||
t.Fatal("Passing function should not have been retried.")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Passing function should not have returned a retry error. Error: %s", err)
|
||||
}
|
||||
|
||||
// Test that a failing function gets retried (once in this example).
|
||||
numTries = 0
|
||||
results := []bool{false, true}
|
||||
err = Retry(0, 0, 0, func() (bool, error) {
|
||||
result := results[numTries]
|
||||
numTries++
|
||||
return result, nil
|
||||
})
|
||||
if numTries != 2 {
|
||||
t.Fatalf("Retried function should have been tried twice. Tried %d times.", numTries)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Successful retried function should not have returned a retry error. Error: %s", err)
|
||||
}
|
||||
|
||||
// Test that a function error gets returned, and the function does not get called again.
|
||||
numTries = 0
|
||||
funcErr := fmt.Errorf("This function had an error!")
|
||||
err = Retry(0, 0, 0, func() (bool, error) {
|
||||
numTries++
|
||||
return false, funcErr
|
||||
})
|
||||
if numTries != 1 {
|
||||
t.Fatal("Errant function should not have been retried.")
|
||||
}
|
||||
if err != funcErr {
|
||||
t.Fatalf("Errant function did not return the right error %s. Error: %s", funcErr, err)
|
||||
}
|
||||
|
||||
// Test when a function exhausts its retries.
|
||||
numTries = 0
|
||||
expectedTries := uint(3)
|
||||
err = Retry(0, 0, expectedTries, func() (bool, error) {
|
||||
numTries++
|
||||
return false, nil
|
||||
})
|
||||
if numTries != expectedTries {
|
||||
t.Fatalf("Unsuccessul retry function should have been called %d times. Only called %d times.", expectedTries, numTries)
|
||||
}
|
||||
if err != RetryExhaustedError {
|
||||
t.Fatalf("Unsuccessful retry function should have returned a retry exhausted error. Actual error: %s", err)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const StartupScriptStartLog string = "Packer startup script starting."
|
||||
const StartupScriptDoneLog string = "Packer startup script done."
|
||||
const StartupScriptKey string = "startup-script"
|
||||
const StartupWrappedScriptKey string = "packer-wrapped-startup-script"
|
||||
|
||||
// We have to encode StartupScriptDoneLog because we use it as a sentinel value to indicate
|
||||
// that the user-provided startup script is done. If we pass StartupScriptDoneLog as-is, it
|
||||
// will be printed early in the instance console log (before the startup script even runs;
|
||||
// we print out instance creation metadata which contains this wrapper script).
|
||||
var StartupScriptDoneLogBase64 string = base64.StdEncoding.EncodeToString([]byte(StartupScriptDoneLog))
|
||||
|
||||
var StartupScript string = fmt.Sprintf(`#!/bin/bash
|
||||
echo %s
|
||||
RETVAL=0
|
||||
|
||||
GetMetadata () {
|
||||
echo "$(curl -f -H "Metadata-Flavor: Google" http://metadata/computeMetadata/v1/instance/attributes/$1 2> /dev/null)"
|
||||
}
|
||||
|
||||
STARTUPSCRIPT=$(GetMetadata %s)
|
||||
STARTUPSCRIPTPATH=/packer-wrapped-startup-script
|
||||
if [ -f "/var/log/startupscript.log" ]; then
|
||||
STARTUPSCRIPTLOGPATH=/var/log/startupscript.log
|
||||
else
|
||||
STARTUPSCRIPTLOGPATH=/var/log/daemon.log
|
||||
fi
|
||||
STARTUPSCRIPTLOGDEST=$(GetMetadata startup-script-log-dest)
|
||||
|
||||
if [[ ! -z $STARTUPSCRIPT ]]; then
|
||||
echo "Executing user-provided startup script..."
|
||||
echo "${STARTUPSCRIPT}" > ${STARTUPSCRIPTPATH}
|
||||
chmod +x ${STARTUPSCRIPTPATH}
|
||||
${STARTUPSCRIPTPATH}
|
||||
RETVAL=$?
|
||||
|
||||
if [[ ! -z $STARTUPSCRIPTLOGDEST ]]; then
|
||||
echo "Uploading user-provided startup script log to ${STARTUPSCRIPTLOGDEST}..."
|
||||
gsutil -h "Content-Type:text/plain" cp ${STARTUPSCRIPTLOGPATH} ${STARTUPSCRIPTLOGDEST}
|
||||
fi
|
||||
|
||||
rm ${STARTUPSCRIPTPATH}
|
||||
fi
|
||||
|
||||
echo $(echo %s | base64 --decode)
|
||||
exit $RETVAL
|
||||
`, StartupScriptStartLog, StartupWrappedScriptKey, StartupScriptDoneLogBase64)
|
||||
@ -0,0 +1,50 @@
|
||||
package googlecompute
|
||||
|
||||
import(
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type StepWaitInstanceStartup int
|
||||
|
||||
// Run reads the instance serial port output and looks for the log entry indicating the startup script finished.
|
||||
func (s *StepWaitInstanceStartup) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
driver := state.Get("driver").(Driver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
instanceName := state.Get("instance_name").(string)
|
||||
|
||||
ui.Say("Waiting for any running startup script to finish...")
|
||||
|
||||
// Keep checking the serial port output to see if the startup script is done.
|
||||
err := Retry(10, 60, 0, func() (bool, error) {
|
||||
output, err := driver.GetSerialPortOutput(config.Zone, instanceName)
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error getting serial port output: %s", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
done := strings.Contains(output, StartupScriptDoneLog)
|
||||
if !done {
|
||||
ui.Say("Startup script not finished yet. Waiting...")
|
||||
}
|
||||
|
||||
return done, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for startup script to finish: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
ui.Say("Startup script, if any, has finished running.")
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// Cleanup.
|
||||
func (s *StepWaitInstanceStartup) Cleanup(state multistep.StateBag) {}
|
||||
@ -0,0 +1,38 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStepWaitInstanceStartup(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepWaitInstanceStartup)
|
||||
config := state.Get("config").(*Config)
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
|
||||
testZone := "test-zone"
|
||||
testInstanceName := "test-instance-name"
|
||||
|
||||
config.Zone = testZone
|
||||
state.Put("instance_name", testInstanceName)
|
||||
// The done log triggers step completion.
|
||||
driver.GetSerialPortOutputResult = StartupScriptDoneLog
|
||||
|
||||
// Run the step.
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("StepWaitInstanceStartup did not return a Continue action: %#v", action)
|
||||
}
|
||||
|
||||
// Check that GetSerialPortOutput was called properly.
|
||||
if driver.GetSerialPortOutputZone != testZone {
|
||||
t.Fatalf(
|
||||
"GetSerialPortOutput wrong zone. Expected: %s, Actual: %s", driver.GetSerialPortOutputZone,
|
||||
testZone)
|
||||
}
|
||||
if driver.GetSerialPortOutputName != testInstanceName {
|
||||
t.Fatalf(
|
||||
"GetSerialPortOutput wrong instance name. Expected: %s, Actual: %s", driver.GetSerialPortOutputName,
|
||||
testInstanceName)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue