scaleway: add pre validate step (check image and snapshot names) (#9840)
* Implement Stringer inteface for multistep.StepAction * scaleway: add pre validate step (check image and snapshot names) Before, it was possible to create multiple images with the same name, leading to a confusing and wasteful situation (same for snapshots). Now, we perform the same kind of checks done by the AWS EC2 builder, and refuse to proceed if there is an existing image with the same name (same for snapshots). As usual, invoking `packer build -force` will bypass the checks. Closes #9839.
This commit is contained in:
parent
77817f80a2
commit
4d7e42220b
|
@ -65,6 +65,11 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||
state.Put("ui", ui)
|
||||
|
||||
steps := []multistep.Step{
|
||||
&stepPreValidate{
|
||||
Force: b.config.PackerForce,
|
||||
ImageName: b.config.ImageName,
|
||||
SnapshotName: b.config.SnapshotName,
|
||||
},
|
||||
&stepCreateSSHKey{
|
||||
Debug: b.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("scw_%s.pem", b.config.PackerBuildName),
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
package scaleway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
// StepPreValidate provides an opportunity to pre-validate any configuration for
|
||||
// the build before actually doing any time consuming work
|
||||
//
|
||||
type stepPreValidate struct {
|
||||
Force bool
|
||||
ImageName string
|
||||
SnapshotName string
|
||||
}
|
||||
|
||||
func (s *stepPreValidate) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
if s.Force {
|
||||
ui.Say("Force flag found, skipping prevalidating image name")
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
ui.Say(fmt.Sprintf("Prevalidating image name: %s", s.ImageName))
|
||||
|
||||
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
|
||||
images, err := instanceAPI.ListImages(
|
||||
&instance.ListImagesRequest{Name: &s.ImageName},
|
||||
scw.WithAllPages())
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error: getting image list: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
for _, im := range images.Images {
|
||||
if im.Name == s.ImageName {
|
||||
err := fmt.Errorf("Error: image name: '%s' is used by existing image with ID %s",
|
||||
s.ImageName, im.ID)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
ui.Say(fmt.Sprintf("Prevalidating snapshot name: %s", s.SnapshotName))
|
||||
|
||||
snapshots, err := instanceAPI.ListSnapshots(
|
||||
&instance.ListSnapshotsRequest{Name: &s.SnapshotName},
|
||||
scw.WithAllPages())
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error: getting snapshot list: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
for _, sn := range snapshots.Snapshots {
|
||||
if sn.Name == s.SnapshotName {
|
||||
err := fmt.Errorf("Error: snapshot name: '%s' is used by existing snapshot with ID %s",
|
||||
s.SnapshotName, sn.ID)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepPreValidate) Cleanup(multistep.StateBag) {
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package scaleway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
// 1. Configure a httptest server to return the list of fakeImgNames or fakeSnapNames
|
||||
// (depending on the endpoint).
|
||||
// 2. Instantiate a Scaleway API client and wire it to send requests to the httptest
|
||||
// server.
|
||||
// 3. Return a state (containing the client) ready to be passed to the step.Run() method.
|
||||
// 4. Return a teardown function meant to be deferred from the test.
|
||||
func setup(t *testing.T, fakeImgNames []string, fakeSnapNames []string) (*multistep.BasicStateBag, func()) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
enc := json.NewEncoder(w)
|
||||
switch r.URL.Path {
|
||||
case "/instance/v1/zones/fr-par-1/images":
|
||||
var imgs instance.ListImagesResponse
|
||||
for _, name := range fakeImgNames {
|
||||
imgs.Images = append(imgs.Images, &instance.Image{
|
||||
ID: strconv.Itoa(rand.Int()),
|
||||
Name: name,
|
||||
Zone: "fr-par-1",
|
||||
})
|
||||
}
|
||||
imgs.TotalCount = uint32(len(fakeImgNames))
|
||||
if err := enc.Encode(imgs); err != nil {
|
||||
t.Fatalf("fake server: encoding reply: %s", err)
|
||||
}
|
||||
case "/instance/v1/zones/fr-par-1/snapshots":
|
||||
var snaps instance.ListSnapshotsResponse
|
||||
for _, name := range fakeSnapNames {
|
||||
snaps.Snapshots = append(snaps.Snapshots, &instance.Snapshot{
|
||||
ID: strconv.Itoa(rand.Int()),
|
||||
Name: name,
|
||||
Zone: "fr-par-1",
|
||||
})
|
||||
}
|
||||
snaps.TotalCount = uint32(len(fakeSnapNames))
|
||||
if err := enc.Encode(snaps); err != nil {
|
||||
t.Fatalf("fake server: encoding reply: %s", err)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("fake server: unexpected path: %q", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
|
||||
clientOpts := []scw.ClientOption{
|
||||
scw.WithDefaultZone(scw.ZoneFrPar1),
|
||||
scw.WithAPIURL(ts.URL),
|
||||
}
|
||||
|
||||
client, err := scw.NewClient(clientOpts...)
|
||||
if err != nil {
|
||||
ts.Close()
|
||||
t.Fatalf("setup: client: %s", err)
|
||||
}
|
||||
|
||||
state := multistep.BasicStateBag{}
|
||||
state.Put("ui", &packer.BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: new(bytes.Buffer),
|
||||
})
|
||||
state.Put("client", client)
|
||||
|
||||
teardown := func() {
|
||||
ts.Close()
|
||||
}
|
||||
return &state, teardown
|
||||
}
|
||||
|
||||
func TestStepPreValidate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
fakeImgNames []string
|
||||
fakeSnapNames []string
|
||||
step stepPreValidate
|
||||
wantAction multistep.StepAction
|
||||
}{
|
||||
{"happy path: both image name and snapshot name are new",
|
||||
[]string{"image-old"},
|
||||
[]string{"snapshot-old"},
|
||||
stepPreValidate{
|
||||
Force: false,
|
||||
ImageName: "image-new",
|
||||
SnapshotName: "snapshot-new",
|
||||
},
|
||||
multistep.ActionContinue,
|
||||
},
|
||||
{"want failure: old image name",
|
||||
[]string{"image-old"},
|
||||
[]string{"snapshot-old"},
|
||||
stepPreValidate{
|
||||
Force: false,
|
||||
ImageName: "image-old",
|
||||
SnapshotName: "snapshot-new",
|
||||
},
|
||||
multistep.ActionHalt,
|
||||
},
|
||||
{"want failure: old snapshot name",
|
||||
[]string{"image-old"},
|
||||
[]string{"snapshot-old"},
|
||||
stepPreValidate{
|
||||
Force: false,
|
||||
ImageName: "image-new",
|
||||
SnapshotName: "snapshot-old",
|
||||
},
|
||||
multistep.ActionHalt,
|
||||
},
|
||||
{"old image name but force flag",
|
||||
[]string{"image-old"},
|
||||
[]string{"snapshot-old"},
|
||||
stepPreValidate{
|
||||
Force: true,
|
||||
ImageName: "image-old",
|
||||
SnapshotName: "snapshot-new",
|
||||
},
|
||||
multistep.ActionContinue,
|
||||
},
|
||||
{"old snapshot name but force flag",
|
||||
[]string{"image-old"},
|
||||
[]string{"snapshot-old"},
|
||||
stepPreValidate{
|
||||
Force: true,
|
||||
ImageName: "image-new",
|
||||
SnapshotName: "snapshot-old",
|
||||
},
|
||||
multistep.ActionContinue,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
state, teardown := setup(t, tc.fakeImgNames, tc.fakeSnapNames)
|
||||
defer teardown()
|
||||
|
||||
if action := tc.step.Run(context.Background(), state); action != tc.wantAction {
|
||||
t.Fatalf("step.Run: want: %v; got: %v", tc.wantAction, action)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -2,7 +2,10 @@
|
|||
// discrete steps.
|
||||
package multistep
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// A StepAction determines the next step to take regarding multi-step actions.
|
||||
type StepAction uint
|
||||
|
@ -12,6 +15,18 @@ const (
|
|||
ActionHalt
|
||||
)
|
||||
|
||||
// Implement the stringer interface; useful for testing.
|
||||
func (a StepAction) String() string {
|
||||
switch a {
|
||||
case ActionContinue:
|
||||
return "ActionContinue"
|
||||
case ActionHalt:
|
||||
return "ActionHalt"
|
||||
default:
|
||||
return "Unexpected value: " + strconv.Itoa(int(a))
|
||||
}
|
||||
}
|
||||
|
||||
// This is the key set in the state bag when using the basic runner to
|
||||
// signal that the step sequence was cancelled.
|
||||
const StateCancelled = "cancelled"
|
||||
|
|
Loading…
Reference in New Issue