From f4e0ed34a967ed09ae7a789299ac44de509a491e Mon Sep 17 00:00:00 2001 From: xibz Date: Thu, 13 Dec 2018 16:58:19 -0800 Subject: [PATCH] Adding custom templates for go-swagger This gets rid of the need for mockgen and uses go-swagger to instead generate a mock client. In addition, this change also gets rid of the wrapper client and instead uses the client.Firecracker instead. Also, the change also generates an interface in the operations package and also needed some updating to the default go-swagger template to use the interface instead of types directly. --- client/firecracker_client.go | 4 +- client/operations/operations_client.go | 26 +++++ fctesting/firecracker_mock_client.go | 140 +++++++++++++++++++++++++ firecracker.go | 138 +++++++++++++++++++++--- firecracker_test.go | 55 ++++++++++ go_swagger_layout.yaml | 6 ++ machine.go | 43 +++----- machine_test.go | 70 ++++--------- opts.go | 2 +- swagger.go | 5 +- templates/client/client.gotmpl | 97 +++++++++++++++++ templates/client/facade.gotmpl | 132 +++++++++++++++++++++++ templates/mockclient.gotmpl | 28 +++++ utils.go | 29 +++++ 14 files changed, 677 insertions(+), 98 deletions(-) create mode 100644 fctesting/firecracker_mock_client.go create mode 100644 firecracker_test.go create mode 100644 go_swagger_layout.yaml create mode 100644 templates/client/client.gotmpl create mode 100644 templates/client/facade.gotmpl create mode 100644 templates/mockclient.gotmpl create mode 100644 utils.go 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 + } + } + } +}