diff --git a/client/firecracker_client.go b/client/firecracker_client.go index 01f1b066..6417c109 100644 --- a/client/firecracker_client.go +++ b/client/firecracker_client.go @@ -116,7 +116,7 @@ func (cfg *TransportConfig) WithSchemes(schemes []string) *TransportConfig { // Firecracker is a client for firecracker type Firecracker struct { - Operations *operations.Client + Operations operations.ClientIface Transport runtime.ClientTransport } @@ -125,6 +125,6 @@ type Firecracker struct { func (c *Firecracker) SetTransport(transport runtime.ClientTransport) { c.Transport = transport - c.Operations.SetTransport(transport) + c.Operations = operations.NewClient(transport, nil) } diff --git a/client/operations/operations_client.go b/client/operations/operations_client.go index 4a2bacd8..792f00a5 100644 --- a/client/operations/operations_client.go +++ b/client/operations/operations_client.go @@ -37,6 +37,14 @@ type Client struct { formats strfmt.Registry } +// NewClient will return a new client with the given transport and formats +func NewClient(transport runtime.ClientTransport, formats strfmt.Registry) *Client { + return &Client{ + transport: transport, + formats: formats, + } +} + /* GetMachineConfig gets the machine configuration of the VM @@ -425,3 +433,21 @@ func (a *Client) PutMachineConfiguration(params *PutMachineConfigurationParams) func (a *Client) SetTransport(transport runtime.ClientTransport) { a.transport = transport } + +// ClientIface is an interface that can be used to mock out a Firecracker agent +// for testing purposes. +type ClientIface interface { + GetMachineConfig(params *GetMachineConfigParams) (*GetMachineConfigOK, error) + GetMmds(params *GetMmdsParams) (*GetMmdsOK, error) + PatchMmds(params *PatchMmdsParams) (*PatchMmdsNoContent, error) + PutMmds(params *PutMmdsParams) (*PutMmdsNoContent, error) + CreateSyncAction(params *CreateSyncActionParams) (*CreateSyncActionNoContent, error) + DescribeInstance(params *DescribeInstanceParams) (*DescribeInstanceOK, error) + PatchGuestDriveByID(params *PatchGuestDriveByIDParams) (*PatchGuestDriveByIDNoContent, error) + PutGuestBootSource(params *PutGuestBootSourceParams) (*PutGuestBootSourceNoContent, error) + PutGuestDriveByID(params *PutGuestDriveByIDParams) (*PutGuestDriveByIDNoContent, error) + PutGuestNetworkInterfaceByID(params *PutGuestNetworkInterfaceByIDParams) (*PutGuestNetworkInterfaceByIDNoContent, error) + PutGuestVsockByID(params *PutGuestVsockByIDParams) (*PutGuestVsockByIDCreated, *PutGuestVsockByIDNoContent, error) + PutLogger(params *PutLoggerParams) (*PutLoggerNoContent, error) + PutMachineConfiguration(params *PutMachineConfigurationParams) (*PutMachineConfigurationNoContent, error) +} diff --git a/fctesting/firecracker_mock_client.go b/fctesting/firecracker_mock_client.go new file mode 100644 index 00000000..b13769d0 --- /dev/null +++ b/fctesting/firecracker_mock_client.go @@ -0,0 +1,140 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package fctesting + +import ( + ops "github.com/firecracker-microvm/firecracker-go-sdk/client/operations" +) + +type MockClient struct { + GetMachineConfigFn func(params *ops.GetMachineConfigParams) (*ops.GetMachineConfigOK, error) + GetMmdsFn func(params *ops.GetMmdsParams) (*ops.GetMmdsOK, error) + PatchMmdsFn func(params *ops.PatchMmdsParams) (*ops.PatchMmdsNoContent, error) + PutMmdsFn func(params *ops.PutMmdsParams) (*ops.PutMmdsNoContent, error) + CreateSyncActionFn func(params *ops.CreateSyncActionParams) (*ops.CreateSyncActionNoContent, error) + DescribeInstanceFn func(params *ops.DescribeInstanceParams) (*ops.DescribeInstanceOK, error) + PatchGuestDriveByIDFn func(params *ops.PatchGuestDriveByIDParams) (*ops.PatchGuestDriveByIDNoContent, error) + PutGuestBootSourceFn func(params *ops.PutGuestBootSourceParams) (*ops.PutGuestBootSourceNoContent, error) + PutGuestDriveByIDFn func(params *ops.PutGuestDriveByIDParams) (*ops.PutGuestDriveByIDNoContent, error) + PutGuestNetworkInterfaceByIDFn func(params *ops.PutGuestNetworkInterfaceByIDParams) (*ops.PutGuestNetworkInterfaceByIDNoContent, error) + PutGuestVsockByIDFn func(params *ops.PutGuestVsockByIDParams) (*ops.PutGuestVsockByIDCreated, *ops.PutGuestVsockByIDNoContent, error) + PutLoggerFn func(params *ops.PutLoggerParams) (*ops.PutLoggerNoContent, error) + PutMachineConfigurationFn func(params *ops.PutMachineConfigurationParams) (*ops.PutMachineConfigurationNoContent, error) +} + +func (c *MockClient) GetMachineConfig(params *ops.GetMachineConfigParams) (*ops.GetMachineConfigOK, error) { + if c.GetMachineConfigFn != nil { + return c.GetMachineConfigFn(params) + } + + return nil, nil +} + +func (c *MockClient) GetMmds(params *ops.GetMmdsParams) (*ops.GetMmdsOK, error) { + if c.GetMmdsFn != nil { + return c.GetMmdsFn(params) + } + + return nil, nil +} + +func (c *MockClient) PatchMmds(params *ops.PatchMmdsParams) (*ops.PatchMmdsNoContent, error) { + if c.PatchMmdsFn != nil { + return c.PatchMmdsFn(params) + } + + return nil, nil +} + +func (c *MockClient) PutMmds(params *ops.PutMmdsParams) (*ops.PutMmdsNoContent, error) { + if c.PutMmdsFn != nil { + return c.PutMmdsFn(params) + } + + return nil, nil +} + +func (c *MockClient) CreateSyncAction(params *ops.CreateSyncActionParams) (*ops.CreateSyncActionNoContent, error) { + if c.CreateSyncActionFn != nil { + return c.CreateSyncActionFn(params) + } + + return nil, nil +} + +func (c *MockClient) DescribeInstance(params *ops.DescribeInstanceParams) (*ops.DescribeInstanceOK, error) { + if c.DescribeInstanceFn != nil { + return c.DescribeInstanceFn(params) + } + + return nil, nil +} + +func (c *MockClient) PatchGuestDriveByID(params *ops.PatchGuestDriveByIDParams) (*ops.PatchGuestDriveByIDNoContent, error) { + if c.PatchGuestDriveByIDFn != nil { + return c.PatchGuestDriveByIDFn(params) + } + + return nil, nil +} + +func (c *MockClient) PutGuestBootSource(params *ops.PutGuestBootSourceParams) (*ops.PutGuestBootSourceNoContent, error) { + if c.PutGuestBootSourceFn != nil { + return c.PutGuestBootSourceFn(params) + } + + return nil, nil +} + +func (c *MockClient) PutGuestDriveByID(params *ops.PutGuestDriveByIDParams) (*ops.PutGuestDriveByIDNoContent, error) { + if c.PutGuestDriveByIDFn != nil { + return c.PutGuestDriveByIDFn(params) + } + + return nil, nil +} + +func (c *MockClient) PutGuestNetworkInterfaceByID(params *ops.PutGuestNetworkInterfaceByIDParams) (*ops.PutGuestNetworkInterfaceByIDNoContent, error) { + if c.PutGuestNetworkInterfaceByIDFn != nil { + return c.PutGuestNetworkInterfaceByIDFn(params) + } + + return nil, nil +} + +func (c *MockClient) PutGuestVsockByID(params *ops.PutGuestVsockByIDParams) (*ops.PutGuestVsockByIDCreated, *ops.PutGuestVsockByIDNoContent, error) { + if c.PutGuestVsockByIDFn != nil { + return c.PutGuestVsockByIDFn(params) + } + + return nil, nil, nil +} + +func (c *MockClient) PutLogger(params *ops.PutLoggerParams) (*ops.PutLoggerNoContent, error) { + if c.PutLoggerFn != nil { + return c.PutLoggerFn(params) + } + + return nil, nil +} + +func (c *MockClient) PutMachineConfiguration(params *ops.PutMachineConfigurationParams) (*ops.PutMachineConfigurationNoContent, error) { + if c.PutMachineConfigurationFn != nil { + return c.PutMachineConfigurationFn(params) + } + + return nil, nil +} diff --git a/firecracker.go b/firecracker.go index ec5e32e8..08729ab5 100644 --- a/firecracker.go +++ b/firecracker.go @@ -27,98 +27,204 @@ import ( const firecrackerRequestTimeout = 500 * time.Millisecond -// FirecrackerClient is a client for interacting with the Firecracker API -type FirecrackerClient struct { +// newFirecrackerClient creates a FirecrackerClient +func newFirecrackerClient(socketPath string, logger *logrus.Entry, debug bool) *client.Firecracker { + httpClient := client.NewHTTPClient(strfmt.NewFormats()) + + transport := NewUnixSocketTransport(socketPath, logger, debug) + httpClient.SetTransport(transport) + + return httpClient +} + +// ClientOpt is a functional option used to modify the client after construction. +type ClientOpt func(*Client) + +// WithOpsClient will return a functional option and replace the operations +// client. This is useful for mock and stub testing. +func WithOpsClient(opsClient ops.ClientIface) ClientOpt { + return func(c *Client) { + c.client.Operations = opsClient + } +} + +// Client is a client for interacting with the Firecracker API +type Client struct { client *client.Firecracker } -// NewFirecrackerClient creates a FirecrackerClient -func NewFirecrackerClient(socketPath string, logger *logrus.Entry, debug bool) *FirecrackerClient { - unixTransport := NewUnixSocketTransport(socketPath, logger, debug) - firecracker := client.New(unixTransport, strfmt.NewFormats()) +// NewClient creates a Client +func NewClient(socketPath string, logger *logrus.Entry, debug bool, opts ...ClientOpt) *Client { + httpClient := newFirecrackerClient(socketPath, logger, debug) + c := &Client{client: httpClient} + for _, opt := range opts { + opt(c) + } - return &FirecrackerClient{client: firecracker} + return c } -func (f *FirecrackerClient) PutLogger(ctx context.Context, logger *models.Logger) (*ops.PutLoggerNoContent, error) { +// PutLoggerOpt is a functional option to be used for the PutLogger API in +// setting any additional optional fields. +type PutLoggerOpt func(*ops.PutLoggerParams) + +// PutLogger is a wrapper for the swagger generated client to make calling of +// the API easier. +func (f *Client) PutLogger(ctx context.Context, logger *models.Logger, opts ...PutLoggerOpt) (*ops.PutLoggerNoContent, error) { timeout, cancel := context.WithTimeout(ctx, firecrackerRequestTimeout) defer cancel() loggerParams := ops.NewPutLoggerParamsWithContext(timeout) loggerParams.SetBody(logger) + for _, opt := range opts { + opt(loggerParams) + } return f.client.Operations.PutLogger(loggerParams) } -func (f *FirecrackerClient) PutMachineConfiguration(ctx context.Context, cfg *models.MachineConfiguration) (*ops.PutMachineConfigurationNoContent, error) { +// PutMachineConfigurationOpt is a functional option to be used for the +// PutMachineConfiguration API in setting any additional optional fields. +type PutMachineConfigurationOpt func(*ops.PutMachineConfigurationParams) + +// PutMachineConfiguration is a wrapper for the swagger generated client to +// make calling of the API easier. +func (f *Client) PutMachineConfiguration(ctx context.Context, cfg *models.MachineConfiguration, opts ...PutMachineConfigurationOpt) (*ops.PutMachineConfigurationNoContent, error) { timeout, cancel := context.WithTimeout(ctx, firecrackerRequestTimeout) defer cancel() mc := ops.NewPutMachineConfigurationParamsWithContext(timeout) mc.SetBody(cfg) + for _, opt := range opts { + opt(mc) + } return f.client.Operations.PutMachineConfiguration(mc) } -func (f *FirecrackerClient) PutGuestBootSource(ctx context.Context, source *models.BootSource) (*ops.PutGuestBootSourceNoContent, error) { +// PutGuestBootSourceOpt is a functional option to be used for the +// PutGuestBootSource API in setting any additional optional fields. +type PutGuestBootSourceOpt func(*ops.PutGuestBootSourceParams) + +// PutGuestBootSource is a wrapper for the swagger generated client to make +// calling of the API easier. +func (f *Client) PutGuestBootSource(ctx context.Context, source *models.BootSource, opts ...PutGuestBootSourceOpt) (*ops.PutGuestBootSourceNoContent, error) { timeout, cancel := context.WithTimeout(ctx, firecrackerRequestTimeout) defer cancel() bootSource := ops.NewPutGuestBootSourceParamsWithContext(timeout) bootSource.SetBody(source) + for _, opt := range opts { + opt(bootSource) + } return f.client.Operations.PutGuestBootSource(bootSource) } -func (f *FirecrackerClient) PutGuestNetworkInterfaceByID(ctx context.Context, ifaceID string, ifaceCfg *models.NetworkInterface) (*ops.PutGuestNetworkInterfaceByIDNoContent, error) { +// PutGuestNetworkInterfaceByIDOpt is a functional option to be used for the +// PutGuestNetworkInterfaceByID API in setting any additional optional fields. +type PutGuestNetworkInterfaceByIDOpt func(*ops.PutGuestNetworkInterfaceByIDParams) + +// PutGuestNetworkInterfaceByID is a wrapper for the swagger generated client +// to make calling of the API easier. +func (f *Client) PutGuestNetworkInterfaceByID(ctx context.Context, ifaceID string, ifaceCfg *models.NetworkInterface, opts ...PutGuestNetworkInterfaceByIDOpt) (*ops.PutGuestNetworkInterfaceByIDNoContent, error) { timeout, cancel := context.WithTimeout(ctx, firecrackerRequestTimeout) defer cancel() cfg := ops.NewPutGuestNetworkInterfaceByIDParamsWithContext(timeout) cfg.SetBody(ifaceCfg) cfg.SetIfaceID(ifaceID) + for _, opt := range opts { + opt(cfg) + } return f.client.Operations.PutGuestNetworkInterfaceByID(cfg) } -func (f *FirecrackerClient) PutGuestDriveByID(ctx context.Context, driveID string, drive *models.Drive) (*ops.PutGuestDriveByIDNoContent, error) { +// PutGuestDriveByIDOpt is a functional option to be used for the +// PutGuestDriveByID API in setting any additional optional fields. +type PutGuestDriveByIDOpt func(*ops.PutGuestDriveByIDParams) + +// PutGuestDriveByID is a wrapper for the swagger generated client to make +// calling of the API easier. +func (f *Client) PutGuestDriveByID(ctx context.Context, driveID string, drive *models.Drive, opts ...PutGuestDriveByIDOpt) (*ops.PutGuestDriveByIDNoContent, error) { timeout, cancel := context.WithTimeout(ctx, 250*time.Millisecond) defer cancel() params := ops.NewPutGuestDriveByIDParamsWithContext(timeout) params.SetDriveID(driveID) params.SetBody(drive) + for _, opt := range opts { + opt(params) + } return f.client.Operations.PutGuestDriveByID(params) } -func (f *FirecrackerClient) PutGuestVsockByID(ctx context.Context, vsockID string, vsock *models.Vsock) (*ops.PutGuestVsockByIDCreated, *ops.PutGuestVsockByIDNoContent, error) { +// PutGuestVsockByIDOpt is a functional option to be used for the +// PutGuestVsockByID API in setting any additional optional fields. +type PutGuestVsockByIDOpt func(*ops.PutGuestVsockByIDParams) + +// PutGuestVsockByID is a wrapper for the swagger generated client to make +// calling of the API easier. +func (f *Client) PutGuestVsockByID(ctx context.Context, vsockID string, vsock *models.Vsock, opts ...PutGuestVsockByIDOpt) (*ops.PutGuestVsockByIDCreated, *ops.PutGuestVsockByIDNoContent, error) { params := ops.NewPutGuestVsockByIDParams() params.SetContext(ctx) params.SetID(vsockID) params.SetBody(vsock) + for _, opt := range opts { + opt(params) + } + return f.client.Operations.PutGuestVsockByID(params) } -func (f *FirecrackerClient) CreateSyncAction(ctx context.Context, info *models.InstanceActionInfo) (*ops.CreateSyncActionNoContent, error) { +// CreateSyncActionOpt is a functional option to be used for the +// CreateSyncAction API in setting any additional optional fields. +type CreateSyncActionOpt func(*ops.CreateSyncActionParams) + +// CreateSyncAction is a wrapper for the swagger generated client to make +// calling of the API easier. +func (f *Client) CreateSyncAction(ctx context.Context, info *models.InstanceActionInfo, opts ...CreateSyncActionOpt) (*ops.CreateSyncActionNoContent, error) { params := ops.NewCreateSyncActionParams() params.SetContext(ctx) params.SetInfo(info) + for _, opt := range opts { + opt(params) + } return f.client.Operations.CreateSyncAction(params) } -func (f *FirecrackerClient) PutMmds(ctx context.Context, metadata interface{}) (*ops.PutMmdsNoContent, error) { +// PutMmdsOpt is a functional option to be used for the PutMmds API in setting +// any additional optional fields. +type PutMmdsOpt func(*ops.PutMmdsParams) + +// PutMmds is a wrapper for the swagger generated client to make calling of the +// API easier. +func (f *Client) PutMmds(ctx context.Context, metadata interface{}, opts ...PutMmdsOpt) (*ops.PutMmdsNoContent, error) { params := ops.NewPutMmdsParams() params.SetContext(ctx) params.SetBody(metadata) + for _, opt := range opts { + opt(params) + } return f.client.Operations.PutMmds(params) } -func (f *FirecrackerClient) GetMachineConfig() (*ops.GetMachineConfigOK, error) { +// GetMachineConfigOpt is a functional option to be used for the +// GetMachineConfig API in setting any additional optional fields. +type GetMachineConfigOpt func(*ops.GetMachineConfigParams) + +// GetMachineConfig is a wrapper for the swagger generated client to make +// calling of the API easier. +func (f *Client) GetMachineConfig(opts ...GetMachineConfigOpt) (*ops.GetMachineConfigOK, error) { p := ops.NewGetMachineConfigParams() p.SetTimeout(firecrackerRequestTimeout) + for _, opt := range opts { + opt(p) + } return f.client.Operations.GetMachineConfig(p) } diff --git a/firecracker_test.go b/firecracker_test.go new file mode 100644 index 00000000..4cddfd53 --- /dev/null +++ b/firecracker_test.go @@ -0,0 +1,55 @@ +package firecracker + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + models "github.com/firecracker-microvm/firecracker-go-sdk/client/models" + log "github.com/sirupsen/logrus" +) + +func TestClient(t *testing.T) { + if testing.Short() { + t.Skip() + } + + ctx := context.Background() + socketpath := filepath.Join(testDataPath, "test.socket") + + cmd := VMCommandBuilder{}. + WithBin(getFirecrackerBinaryPath()). + WithSocketPath(socketpath). + Build(ctx) + + if err := cmd.Start(); err != nil { + t.Fatalf("failed to start firecracker vmm: %v", err) + } + + defer func() { + if err := cmd.Process.Kill(); err != nil { + t.Errorf("failed to kill process: %v", err) + } + os.Remove(socketpath) + }() + + drive := &models.Drive{ + DriveID: String("test"), + IsReadOnly: Bool(false), + IsRootDevice: Bool(false), + PathOnHost: String(filepath.Join(testDataPath, "drive-2.img")), + } + + client := NewClient(socketpath, log.NewEntry(log.New()), true) + deadlineCtx, deadlineCancel := context.WithTimeout(ctx, 250*time.Millisecond) + defer deadlineCancel() + if err := waitForAliveVMM(deadlineCtx, client); err != nil { + t.Fatal(err) + } + + if _, err := client.PutGuestDriveByID(ctx, "test", drive); err != nil { + t.Errorf("unexpected error on PutGuestDriveByID, %v", err) + } +} diff --git a/go_swagger_layout.yaml b/go_swagger_layout.yaml new file mode 100644 index 00000000..eb5570a3 --- /dev/null +++ b/go_swagger_layout.yaml @@ -0,0 +1,6 @@ +layout: + application: + - name: mockClient + source: asset:mockclient + target: "{{ joinFilePath .Target .ClientPackage }}" + file_name: "{{ .Name }}_mock_client.go" diff --git a/machine.go b/machine.go index 0b396f17..80c37e5a 100644 --- a/machine.go +++ b/machine.go @@ -27,7 +27,6 @@ import ( "time" models "github.com/firecracker-microvm/firecracker-go-sdk/client/models" - ops "github.com/firecracker-microvm/firecracker-go-sdk/client/operations" log "github.com/sirupsen/logrus" ) @@ -35,22 +34,10 @@ const ( userAgent = "firecracker-go-sdk" ) +// ErrAlreadyStarted signifies that the Machine has already started and cannot +// be started again. var ErrAlreadyStarted = errors.New("firecracker: machine already started") -// Firecracker is an interface that can be used to mock -// out an Firecracker agent for testing purposes. -type Firecracker interface { - PutLogger(ctx context.Context, logger *models.Logger) (*ops.PutLoggerNoContent, error) - PutMachineConfiguration(ctx context.Context, cfg *models.MachineConfiguration) (*ops.PutMachineConfigurationNoContent, error) - PutGuestBootSource(ctx context.Context, source *models.BootSource) (*ops.PutGuestBootSourceNoContent, error) - PutGuestNetworkInterfaceByID(ctx context.Context, ifaceID string, ifaceCfg *models.NetworkInterface) (*ops.PutGuestNetworkInterfaceByIDNoContent, error) - PutGuestDriveByID(ctx context.Context, driveID string, drive *models.Drive) (*ops.PutGuestDriveByIDNoContent, error) - PutGuestVsockByID(ctx context.Context, vsockID string, vsock *models.Vsock) (*ops.PutGuestVsockByIDCreated, *ops.PutGuestVsockByIDNoContent, error) - CreateSyncAction(ctx context.Context, info *models.InstanceActionInfo) (*ops.CreateSyncActionNoContent, error) - PutMmds(ctx context.Context, metadata interface{}) (*ops.PutMmdsNoContent, error) - GetMachineConfig() (*ops.GetMachineConfigOK, error) -} - // Config is a collection of user-configurable VMM settings type Config struct { // SocketPath defines the file path where the Firecracker control socket @@ -144,7 +131,7 @@ type Machine struct { Handlers Handlers cfg Config - client Firecracker + client *Client cmd *exec.Cmd logger *log.Entry machineConfig models.MachineConfiguration // The actual machine config as reported by Firecracker @@ -230,7 +217,7 @@ func NewMachine(ctx context.Context, cfg Config, opts ...Opt) (*Machine, error) } if m.client == nil { - m.client = NewFirecrackerClient(cfg.SocketPath, m.logger, cfg.Debug) + m.client = NewClient(cfg.SocketPath, m.logger, cfg.Debug) } m.cfg = cfg @@ -532,8 +519,10 @@ func (m *Machine) attachDrive(ctx context.Context, dev models.Drive) error { return err } - log.Infof("Attaching drive %s, slot %s, root %t.", hostPath, StringValue(dev.DriveID), BoolValue(dev.IsRootDevice)) - respNoContent, err := m.client.PutGuestDriveByID(ctx, StringValue(dev.DriveID), &dev) + driveID := StringValue(dev.DriveID) + log.Infof("Attaching drive %s, slot %s, root %t.", hostPath, driveID, BoolValue(dev.IsRootDevice)) + + respNoContent, err := m.client.PutGuestDriveByID(ctx, driveID, &dev) if err == nil { m.logger.Printf("Attached drive %s: %s", hostPath, respNoContent.Error()) } else { @@ -548,6 +537,7 @@ func (m *Machine) addVsock(ctx context.Context, dev VsockDevice) error { GuestCid: int64(dev.CID), ID: &dev.Path, } + resp, _, err := m.client.PutGuestVsockByID(ctx, dev.Path, &vsockCfg) if err != nil { return err @@ -577,18 +567,13 @@ func (m *Machine) EnableMetadata(metadata interface{}) { // SetMetadata sets the machine's metadata for MDDS func (m *Machine) SetMetadata(ctx context.Context, metadata interface{}) error { - respnocontent, err := m.client.PutMmds(ctx, metadata) - - if err == nil { - var message string - if respnocontent != nil { - message = respnocontent.Error() - } - m.logger.Printf("SetMetadata successful: %s", message) - } else { + if _, err := m.client.PutMmds(ctx, metadata); err != nil { m.logger.Errorf("Setting metadata: %s", err) + return err } - return err + + m.logger.Printf("SetMetadata successful") + return nil } // refreshMachineConfig synchronizes our cached representation of the machine configuration diff --git a/machine_test.go b/machine_test.go index 3eda2fc3..6df55e36 100644 --- a/machine_test.go +++ b/machine_test.go @@ -11,8 +11,6 @@ // express or implied. See the License for the specific language governing // permissions and limitations under the License. -//go:generate mockgen -source=machine.go -destination=fctesting/machine_mock.go -package=fctesting - package firecracker import ( @@ -32,7 +30,6 @@ import ( "github.com/firecracker-microvm/firecracker-go-sdk/client/operations" ops "github.com/firecracker-microvm/firecracker-go-sdk/client/operations" "github.com/firecracker-microvm/firecracker-go-sdk/fctesting" - "github.com/golang/mock/gomock" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) @@ -133,7 +130,12 @@ func TestMicroVMExecution(t *testing.T) { exitchannel <- m.Wait(vmmCtx) close(exitchannel) }() - time.Sleep(2 * time.Second) + + deadlineCtx, deadlineCancel := context.WithTimeout(vmmCtx, 250*time.Millisecond) + defer deadlineCancel() + if err := waitForAliveVMM(deadlineCtx, m.client); err != nil { + t.Fatal(err) + } t.Run("TestCreateMachine", func(t *testing.T) { testCreateMachine(ctx, t, m) }) t.Run("TestMachineConfigApplication", func(t *testing.T) { testMachineConfigApplication(ctx, t, m, cfg) }) @@ -380,14 +382,12 @@ func testStopVMM(ctx context.Context, t *testing.T, m *Machine) { } func TestWaitForSocket(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - okClient := fctesting.NewMockFirecracker(ctrl) - okClient.EXPECT().GetMachineConfig().AnyTimes().Return(nil, nil) - - errClient := fctesting.NewMockFirecracker(ctrl) - errClient.EXPECT().GetMachineConfig().AnyTimes().Return(nil, errors.New("http error")) + okClient := fctesting.MockClient{} + errClient := fctesting.MockClient{ + GetMachineConfigFn: func(params *ops.GetMachineConfigParams) (*ops.GetMachineConfigOK, error) { + return nil, errors.New("http error") + }, + } // testWaitForSocket has three conditions that need testing: // 1. The expected file is created within the deadline and @@ -411,13 +411,13 @@ func TestWaitForSocket(t *testing.T) { }() // Socket file created, HTTP request succeeded - m.client = okClient + m.client = NewClient(filename, log.NewEntry(log.New()), true, WithOpsClient(&okClient)) if err := m.waitForSocket(500*time.Millisecond, errchan); err != nil { t.Errorf("waitForSocket returned unexpected error %s", err) } // Socket file exists, HTTP request failed - m.client = errClient + m.client = NewClient(filename, log.NewEntry(log.New()), true, WithOpsClient(&errClient)) if err := m.waitForSocket(500*time.Millisecond, errchan); err != context.DeadlineExceeded { t.Error("waitforSocket did not return an expected timeout error") } @@ -450,8 +450,6 @@ func testSetMetadata(ctx context.Context, t *testing.T, m *Machine) { } func TestLogFiles(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() cfg := Config{ Debug: true, KernelImagePath: filepath.Join(testDataPath, "vmlinux"), SocketPath: filepath.Join(testDataPath, "socket-path"), @@ -466,39 +464,15 @@ func TestLogFiles(t *testing.T) { DisableValidation: true, } - client := fctesting.NewMockFirecracker(ctrl) + opClient := fctesting.MockClient{ + GetMachineConfigFn: func(params *ops.GetMachineConfigParams) (*ops.GetMachineConfigOK, error) { + return &ops.GetMachineConfigOK{ + Payload: &models.MachineConfiguration{}, + }, nil + }, + } ctx := context.Background() - client. - EXPECT(). - PutMachineConfiguration( - ctx, - gomock.Any(), - ). - AnyTimes(). - Return(&ops.PutMachineConfigurationNoContent{}, nil) - - client. - EXPECT(). - PutGuestBootSource( - ctx, - gomock.Any(), - ). - AnyTimes(). - Return(&ops.PutGuestBootSourceNoContent{}, nil) - - client. - EXPECT(). - PutGuestDriveByID( - ctx, - gomock.Any(), - gomock.Any(), - ). - AnyTimes(). - Return(&ops.PutGuestDriveByIDNoContent{}, nil) - - client.EXPECT().GetMachineConfig().AnyTimes().Return(&ops.GetMachineConfigOK{ - Payload: &models.MachineConfiguration{}, - }, nil) + client := NewClient("socket-path", log.NewEntry(log.New()), true, WithOpsClient(&opClient)) stdoutPath := filepath.Join(testDataPath, "stdout.log") stderrPath := filepath.Join(testDataPath, "stderr.log") diff --git a/opts.go b/opts.go index d02b4ea6..50f95fa0 100644 --- a/opts.go +++ b/opts.go @@ -25,7 +25,7 @@ type Opt func(*Machine) // WithClient will use the client in place rather than the client constructed // during bootstrapping of the machine. This option is useful for mocking out // tests. -func WithClient(client Firecracker) Opt { +func WithClient(client *Client) Opt { return func(machine *Machine) { machine.client = client } diff --git a/swagger.go b/swagger.go index fd762acd..745d1dab 100644 --- a/swagger.go +++ b/swagger.go @@ -16,7 +16,8 @@ // --skip-validation is used in the command-lines below to remove the network dependency that the swagger generator has // in attempting to validate that the email address specified in the yaml file is valid. -//go:generate docker run --rm --net=none -v $PWD:/work -w /work quay.io/goswagger/swagger generate model -f ./client/swagger.yaml --model-package=client/models --client-package=client --copyright-file=COPYRIGHT_HEADER --skip-validation -//go:generate docker run --rm --net=none -v $PWD:/work -w /work quay.io/goswagger/swagger generate client -f ./client/swagger.yaml --model-package=client/models --client-package=client --copyright-file=COPYRIGHT_HEADER --skip-validation +//go:generate docker run --rm --net=none -v $PWD:/work -w /work quay.io/goswagger/swagger generate model -f ./client/swagger.yaml -T ./templates --model-package=client/models --client-package=client --copyright-file=COPYRIGHT_HEADER --skip-validation +//go:generate docker run --rm --net=none -v $PWD:/work -w /work quay.io/goswagger/swagger generate client -f ./client/swagger.yaml -T ./templates --model-package=client/models --client-package=client --copyright-file=COPYRIGHT_HEADER --skip-validation +//go:generate docker run --rm --net=none -v $PWD:/work -w /work quay.io/goswagger/swagger generate client -f ./client/swagger.yaml -C ./go_swagger_layout.yaml -T ./templates --model-package=client/models --client-package=fctesting --copyright-file=COPYRIGHT_HEADER --skip-validation package firecracker diff --git a/templates/client/client.gotmpl b/templates/client/client.gotmpl new file mode 100644 index 00000000..401bf73f --- /dev/null +++ b/templates/client/client.gotmpl @@ -0,0 +1,97 @@ +// Code generated by go-swagger; DO NOT EDIT. + + +{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }} + + +package {{ .Name }} + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + "github.com/go-openapi/errors" + "github.com/go-openapi/swag" + "github.com/go-openapi/runtime" + "github.com/go-openapi/validate" + + strfmt "github.com/go-openapi/strfmt" + + {{ range .DefaultImports }}{{ printf "%q" .}} + {{ end }} + {{ range $key, $value := .Imports }}{{ $key }} {{ printf "%q" $value }} + {{ end }} +) + +// New creates a new {{ humanize .Name }} API client. +func New(transport runtime.ClientTransport, formats strfmt.Registry) *Client { + return &Client{transport: transport, formats: formats} +} + +/* +Client {{ if .Summary }}{{ .Summary }}{{ if .Description }} + +{{ blockcomment .Description }}{{ end }}{{ else if .Description}}{{ blockcomment .Description }}{{ else }}for {{ humanize .Name }} API{{ end }} +*/ +type Client struct { + transport runtime.ClientTransport + formats strfmt.Registry +} + +// NewClient will return a new client with the given transport and formats +func NewClient(transport runtime.ClientTransport, formats strfmt.Registry) *Client { + return &Client{ + transport: transport, + formats: formats, + } +} + +{{ range .Operations }}/* +{{ pascalize .Name }} {{ if .Summary }}{{ pluralizeFirstWord (humanize .Summary) }}{{ if .Description }} + +{{ blockcomment .Description }}{{ end }}{{ else if .Description}}{{ blockcomment .Description }}{{ else }}{{ humanize .Name }} API{{ end }} +*/ +func (a *Client) {{ pascalize .Name }}(params *{{ pascalize .Name }}Params{{ if .Authorized }}, authInfo runtime.ClientAuthInfoWriter{{end}}{{ if .HasStreamingResponse }}, writer io.Writer{{ end }}) {{ if .SuccessResponse }}({{ range .SuccessResponses }}*{{ pascalize .Name }}, {{ end }}{{ end }}error{{ if .SuccessResponse }}){{ end }} { + // TODO: Validate the params before sending + if params == nil { + params = New{{ pascalize .Name }}Params() + } + {{ $length := len .SuccessResponses }} + {{ if .SuccessResponse }}result{{else}}_{{ end }}, err := a.transport.Submit(&runtime.ClientOperation{ + ID: {{ printf "%q" .Name }}, + Method: {{ printf "%q" .Method }}, + PathPattern: {{ printf "%q" .Path }}, + ProducesMediaTypes: {{ printf "%#v" .ProducesMediaTypes }}, + ConsumesMediaTypes: {{ printf "%#v" .ConsumesMediaTypes }}, + Schemes: {{ printf "%#v" .Schemes }}, + Params: params, + Reader: &{{ pascalize .Name }}Reader{formats: a.formats{{ if .HasStreamingResponse }}, writer: writer{{ end }}},{{ if .Authorized }} + AuthInfo: authInfo,{{ end}} + Context: params.Context, + Client: params.HTTPClient, + }) + if err != nil { + return {{ if .SuccessResponse }}{{ padSurround "nil" "nil" 0 $length }}, {{ end }}err + } + {{ if .SuccessResponse }}{{ if eq $length 1 }}return result.(*{{ pascalize .SuccessResponse.Name }}), nil{{ else }}switch value := result.(type) { {{ range $i, $v := .SuccessResponses }} + case *{{ pascalize $v.Name }}: + return {{ padSurround "value" "nil" $i $length }}, nil{{ end }} } + return {{ padSurround "nil" "nil" 0 $length }}, nil{{ end }} + {{ else }}return nil{{ end }} + +} +{{ end }} + +// SetTransport changes the transport on the client +func (a *Client) SetTransport(transport runtime.ClientTransport) { + a.transport = transport +} + +// ClientIface is an interface that can be used to mock out a Firecracker agent +// for testing purposes. +type ClientIface interface { +{{ range .Operations -}} +{{ pascalize .Name }}(params *{{ pascalize .Name }}Params{{ if .Authorized }}, authInfo runtime.ClientAuthInfoWriter{{end}}{{ if .HasStreamingResponse }}, writer io.Writer{{ end }}) {{ if .SuccessResponse }}({{ range .SuccessResponses }}*{{ pascalize .Name }}, {{ end }}{{ end }}error{{ if .SuccessResponse }}){{ end }} +{{ end -}} +} diff --git a/templates/client/facade.gotmpl b/templates/client/facade.gotmpl new file mode 100644 index 00000000..6deb0f7b --- /dev/null +++ b/templates/client/facade.gotmpl @@ -0,0 +1,132 @@ +// Code generated by go-swagger; DO NOT EDIT. + + +{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }} + + +package {{ .Package }} + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + + +import ( + "net/http" + "github.com/go-openapi/runtime" + httptransport "github.com/go-openapi/runtime/client" + "github.com/go-openapi/swag" + "github.com/go-openapi/spec" + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + + strfmt "github.com/go-openapi/strfmt" + + {{ range .DefaultImports }}{{ printf "%q" .}} + {{ end }} + {{ range $key, $value := .Imports }}{{ $key }} {{ printf "%q" $value }} + {{ end }} +) + +// Default {{ humanize .Name }} HTTP client. +var Default = NewHTTPClient(nil) + +const ( + // DefaultHost is the default Host + // found in Meta (info) section of spec file + DefaultHost string = {{ printf "%#v" .Host }} + // DefaultBasePath is the default BasePath + // found in Meta (info) section of spec file + DefaultBasePath string = {{ printf "%#v" .BasePath }} +) + +// DefaultSchemes are the default schemes found in Meta (info) section of spec file +var DefaultSchemes = {{ printf "%#v" .Schemes }} + +// NewHTTPClient creates a new {{ humanize .Name }} HTTP client. +func NewHTTPClient(formats strfmt.Registry) *{{ pascalize .Name }} { + return NewHTTPClientWithConfig(formats, nil) +} + +// NewHTTPClientWithConfig creates a new {{ humanize .Name }} HTTP client, +// using a customizable transport config. +func NewHTTPClientWithConfig(formats strfmt.Registry, cfg *TransportConfig) *{{ pascalize .Name }} { + // ensure nullable parameters have default + if cfg == nil { + cfg = DefaultTransportConfig() + } + + // create transport and client + transport := httptransport.New(cfg.Host, cfg.BasePath, cfg.Schemes) + return New(transport, formats) +} + +// New creates a new {{ humanize .Name }} client +func New(transport runtime.ClientTransport, formats strfmt.Registry) *{{ pascalize .Name }} { + // ensure nullable parameters have default + if formats == nil { + formats = strfmt.Default + } + + cli := new({{ pascalize .Name }}) + cli.Transport = transport + {{ range .OperationGroups }} + cli.{{ pascalize .Name }} = {{ .Name }}.New(transport, formats) + {{ end }} + return cli +} + +// DefaultTransportConfig creates a TransportConfig with the +// default settings taken from the meta section of the spec file. +func DefaultTransportConfig() *TransportConfig { + return &TransportConfig { + Host: DefaultHost, + BasePath: DefaultBasePath, + Schemes: DefaultSchemes, + } +} + +// TransportConfig contains the transport related info, +// found in the meta section of the spec file. +type TransportConfig struct { + Host string + BasePath string + Schemes []string +} + +// WithHost overrides the default host, +// provided by the meta section of the spec file. +func (cfg *TransportConfig) WithHost(host string) *TransportConfig { + cfg.Host = host + return cfg +} + +// WithBasePath overrides the default basePath, +// provided by the meta section of the spec file. +func (cfg *TransportConfig) WithBasePath(basePath string) *TransportConfig { + cfg.BasePath = basePath + return cfg +} + +// WithSchemes overrides the default schemes, +// provided by the meta section of the spec file. +func (cfg *TransportConfig) WithSchemes(schemes []string) *TransportConfig { + cfg.Schemes = schemes + return cfg +} + +// {{ pascalize .Name }} is a client for {{ humanize .Name }} +type {{ pascalize .Name }} struct { + {{ range .OperationGroups }} + {{ pascalize .Name }} {{ .Name }}.ClientIface + {{ end }} + Transport runtime.ClientTransport +} + + +// SetTransport changes the transport on the client and all its subresources +func (c *{{pascalize .Name}}) SetTransport(transport runtime.ClientTransport) { + c.Transport = transport + {{ range .OperationGroups }} + c.{{ pascalize .Name }} = operations.NewClient(transport, nil) + {{ end }} +} diff --git a/templates/mockclient.gotmpl b/templates/mockclient.gotmpl new file mode 100644 index 00000000..e05b6f6a --- /dev/null +++ b/templates/mockclient.gotmpl @@ -0,0 +1,28 @@ +// Code generated by go-swagger; DO NOT EDIT. + + +{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }} + + +package fctesting + +import ( + ops "github.com/firecracker-microvm/firecracker-go-sdk/client/operations" +) + +type MockClient struct { +{{ range .Operations -}} +{{ pascalize .Name }}Fn func(params *ops.{{ pascalize .Name }}Params{{ if .Authorized }}, authInfo runtime.ClientAuthInfoWriter{{end}}{{ if .HasStreamingResponse }}, writer io.Writer{{ end }}) {{ if .SuccessResponse }}({{ range .SuccessResponses }}*ops.{{ pascalize .Name }}, {{ end }}{{ end }}error{{ if .SuccessResponse }}){{ end }} +{{ end -}} +} +{{ range .Operations -}} + +func (c *MockClient) {{ pascalize .Name }}(params *ops.{{ pascalize .Name }}Params{{ if .Authorized }}, authInfo runtime.ClientAuthInfoWriter{{end}}{{ if .HasStreamingResponse }}, writer io.Writer{{ end }}) {{ if .SuccessResponse }}({{ range .SuccessResponses }}*ops.{{ pascalize .Name }}, {{ end }}{{ end }}error{{ if .SuccessResponse }}){{ end }} { + if c.{{ pascalize .Name }}Fn != nil { + return c.{{ pascalize .Name }}Fn(params{{ if .Authorized }}, authInfo{{end}}{{ if .HasStreamingResponse }}, writer{{ end }}) + } + + return {{ if .SuccessResponse }}{{ range .SuccessResponses }}nil, {{ end }}{{ end }} nil +} + +{{ end -}} diff --git a/utils.go b/utils.go new file mode 100644 index 00000000..7284ba74 --- /dev/null +++ b/utils.go @@ -0,0 +1,29 @@ +package firecracker + +import ( + "context" + "time" +) + +const ( + defaultAliveVMMCheckDur = 10 * time.Millisecond +) + +// waitForAliveVMM will check for periodically to see if the firecracker VMM is +// alive. If the VMM takes too long in starting, an error signifying that will +// be returned. +func waitForAliveVMM(ctx context.Context, client *Client) error { + t := time.NewTicker(defaultAliveVMMCheckDur) + defer t.Stop() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-t.C: + if _, err := client.GetMachineConfig(); err == nil { + return nil + } + } + } +}