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:
Marco Molteni 2020-09-18 21:37:48 +02:00 committed by GitHub
parent 77817f80a2
commit 4d7e42220b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 255 additions and 1 deletions

View File

@ -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),

View File

@ -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) {
}

View File

@ -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)
}
})
}
}

View File

@ -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"