Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions cmd/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,21 @@ package cmd

import (
"fmt"
config2 "github.com/imposter-project/imposter-cli/internal/config"
"github.com/imposter-project/imposter-cli/internal/engine"
"github.com/spf13/cobra"
"os"
"path/filepath"
"time"

config2 "github.com/imposter-project/imposter-cli/internal/config"
"github.com/imposter-project/imposter-cli/internal/engine"
"github.com/imposter-project/imposter-cli/internal/engine/awslambda"
"github.com/spf13/cobra"
)

var bundleFlags = struct {
engineType string
engineVersion string
output string
architecture string
}{}

// bundleCmd represents the bundle command
Expand Down Expand Up @@ -58,7 +61,7 @@ If CONFIG_DIR is not specified, the current working directory is used.`,
// Search for CLI config files in the mock config dir.
config2.MergeCliConfigIfExists(configDir)

engineType := engine.GetConfiguredType(bundleFlags.engineType)
engineType := engine.GetConfiguredTypeWithVersion(bundleFlags.engineType, bundleFlags.engineVersion)
lib := engine.GetLibrary(engineType)

if lib.IsSealedDistro() {
Expand All @@ -75,6 +78,7 @@ func init() {
bundleCmd.Flags().StringVarP(&bundleFlags.output, "output", "o", "", "The destination to write the bundle to. If using the 'docker' engine type, this must be a valid image name. Otherwise, this must be a path to a writeable file. If not specified, a name is generated.")
bundleCmd.Flags().StringVarP(&bundleFlags.engineType, "engine-type", "t", "", "Imposter engine type (valid: awslambda,docker,jvm)")
bundleCmd.Flags().StringVarP(&bundleFlags.engineVersion, "version", "v", "", "Imposter engine version (default \"latest\")")
bundleCmd.Flags().StringVarP(&bundleFlags.architecture, "architecture", "a", awslambda.DefaultLambdaArch, "Target CPU architecture for the awslambda engine bundle (amd64 or arm64). Ignored by other engine types.")

_ = bundleCmd.MarkFlagRequired("engine-type")
registerEngineTypeCompletions(bundleCmd, engine.EngineTypeAwsLambda)
Expand Down Expand Up @@ -107,6 +111,9 @@ func getBundleDest(engineType engine.EngineType) string {

func bundle(lib *engine.EngineLibrary, version string, configDir string, dest string) {
provider := (*lib).GetProvider(version)
if lambdaProv, ok := provider.(*awslambda.LambdaProvider); ok {
lambdaProv.Architecture = bundleFlags.architecture
}
logger.Debugf("creating %s bundle %s using version %s", provider.GetEngineType(), configDir, version)

err := provider.Provide(engine.PullIfNotPresent)
Expand Down
2 changes: 1 addition & 1 deletion cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ If CONFIG_DIR is not specified, the current working directory is used.`,
pullPolicy = engine.PullIfNotPresent
}

engineType := engine.GetConfiguredType(upFlags.engineType)
engineType := engine.GetConfiguredTypeWithVersion(upFlags.engineType, upFlags.engineVersion)
lib := engine.GetLibrary(engineType)

var version string
Expand Down
159 changes: 149 additions & 10 deletions internal/engine/awslambda/binary.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,84 @@
package awslambda

import (
"archive/zip"
"fmt"
library2 "github.com/imposter-project/imposter-cli/internal/library"
"github.com/spf13/viper"
"io"
"os"
"path/filepath"
)

var downloadConfig = library2.NewDownloadConfig(
"https://github.com/imposter-project/imposter-jvm-engine/releases/latest/download",
"https://github.com/imposter-project/imposter-jvm-engine/releases/download/v%v",
false,
"github.com/imposter-project/imposter-cli/internal/compression"
"github.com/imposter-project/imposter-cli/internal/engine"
library2 "github.com/imposter-project/imposter-cli/internal/library"
"github.com/spf13/viper"
)

func checkOrDownloadBinary(version string) (string, error) {
// DefaultLambdaArch is the architecture assumed when a caller does not
// specify one. It matches AWS Lambda's own default architecture (x86_64,
// expressed as the Go arch name "amd64") so unconfigured bundles still
// produce a deployable artefact.
const DefaultLambdaArch = "amd64"

// lambdaBinarySpec materialises a Lambda-ready zip into the local cache for
// a given engine flavour. cacheFile names the on-disk artefact; assemble
// fetches and (where necessary) converts the upstream release into that zip.
type lambdaBinarySpec struct {
cacheFile func(arch, version string) string
assemble func(cachePath, version, arch string) error
}

// jvmLambdaSpec ships a pre-built Lambda zip from imposter-jvm-engine that
// is already arch-agnostic and Lambda-ready, so assembly is a direct
// download.
var jvmLambdaSpec = lambdaBinarySpec{
cacheFile: func(_, version string) string {
return fmt.Sprintf("imposter-awslambda-%s.zip", version)
},
assemble: func(cachePath, version, _ string) error {
dc := library2.NewDownloadConfig(
"https://github.com/imposter-project/imposter-jvm-engine/releases/latest/download",
"https://github.com/imposter-project/imposter-jvm-engine/releases/download/v%v",
false,
)
return library2.DownloadBinary(dc, cachePath, "imposter-awslambda.zip", version)
},
}

// nativeLambdaSpec consumes the per-arch imposter-go release tarball and
// repackages the contained binary as a Lambda custom-runtime zip whose
// single entry is the executable "bootstrap".
var nativeLambdaSpec = lambdaBinarySpec{
cacheFile: func(arch, version string) string {
return fmt.Sprintf("imposter-go-awslambda-%s-%s.zip", arch, version)
},
assemble: assembleNativeLambdaZip,
}

// specForVersion picks the AWS Lambda binary spec for the given engine
// version. 5.x and later use the native (imposter-go) flavour; everything
// else — including the empty/"latest" alias and unparseable values — falls
// back to the JVM flavour, matching the project-wide default engine.
func specForVersion(version string) lambdaBinarySpec {
if engine.DeriveEngineTypeFromVersion(version) == engine.EngineTypeNative {
return nativeLambdaSpec
}
return jvmLambdaSpec
}

func checkOrDownloadBinary(version string, arch string) (string, error) {
if arch == "" {
arch = DefaultLambdaArch
}
binFilePath := viper.GetString("lambda.binary")
if binFilePath == "" {
spec := specForVersion(version)

binCachePath, err := ensureBinCache()
if err != nil {
logger.Fatal(err)
}

binFilePath = filepath.Join(binCachePath, fmt.Sprintf("imposter-awslambda-%v.zip", version))
binFilePath = filepath.Join(binCachePath, spec.cacheFile(arch, version))

if _, err := os.Stat(binFilePath); err != nil {
if !os.IsNotExist(err) {
Expand All @@ -34,14 +90,97 @@ func checkOrDownloadBinary(version string) (string, error) {
return binFilePath, nil
}

if err := library2.DownloadBinary(downloadConfig, binFilePath, "imposter-awslambda.zip", version); err != nil {
if err := spec.assemble(binFilePath, version, arch); err != nil {
return "", fmt.Errorf("failed to fetch lambda binary: %v", err)
}
}
logger.Tracef("using lambda binary at: %v", binFilePath)
return binFilePath, nil
}

// assembleNativeLambdaZip downloads the imposter-go linux release tarball
// for the requested architecture, extracts the imposter-go binary, and
// writes a Lambda-ready zip to cachePath containing a single executable
// "bootstrap" entry (as required by the provided.al2023 custom runtime).
func assembleNativeLambdaZip(cachePath, version, arch string) error {
dc := library2.NewDownloadConfig(
"https://github.com/imposter-project/imposter-go/releases/latest/download",
"https://github.com/imposter-project/imposter-go/releases/download/v%v",
false,
)
remoteFile := fmt.Sprintf("imposter-go_linux_%s.tar.gz", arch)

tempDir, err := os.MkdirTemp("", "imposter-go-lambda-")
if err != nil {
return fmt.Errorf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)

tarballPath := filepath.Join(tempDir, remoteFile)
if err := library2.DownloadBinary(dc, tarballPath, remoteFile, version); err != nil {
return err
}

binaryPath, err := extractGoBinary(tarballPath, tempDir)
if err != nil {
return fmt.Errorf("failed to extract imposter-go binary from %s: %v", tarballPath, err)
}

if err := writeBootstrapZip(binaryPath, cachePath); err != nil {
return fmt.Errorf("failed to write lambda zip %s: %v", cachePath, err)
}
return nil
}

// extractGoBinary extracts the imposter-go release tarball and returns the
// path to the extracted "imposter-go" binary. The release archive currently
// also contains README/CHANGELOG files which are ignored.
func extractGoBinary(tarballPath, destDir string) (string, error) {
if err := compression.ExtractTarGz(tarballPath, destDir); err != nil {
return "", err
}
binaryPath := filepath.Join(destDir, "imposter-go")
if _, err := os.Stat(binaryPath); err != nil {
return "", fmt.Errorf("imposter-go binary not found in archive: %v", err)
}
return binaryPath, nil
}

// writeBootstrapZip writes a zip containing a single "bootstrap" entry with
// the contents of binaryPath. The entry is marked executable so AWS
// Lambda's provided.al2023 runtime will run it.
func writeBootstrapZip(binaryPath, zipPath string) error {
src, err := os.Open(binaryPath)
if err != nil {
return err
}
defer src.Close()

out, err := os.Create(zipPath)
if err != nil {
return err
}
defer out.Close()

zw := zip.NewWriter(out)
defer zw.Close()

header := &zip.FileHeader{
Name: "bootstrap",
Method: zip.Deflate,
}
header.SetMode(0755)

w, err := zw.CreateHeader(header)
if err != nil {
return err
}
if _, err := io.Copy(w, src); err != nil {
return err
}
return nil
}

func ensureBinCache() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
Expand Down
11 changes: 8 additions & 3 deletions internal/engine/awslambda/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (
)

func (p *LambdaProvider) Bundle(configDir string, dest string) error {
deploymentPackage, err := CreateDeploymentPackage(p.Version, configDir)
deploymentPackage, err := CreateDeploymentPackage(p.Version, configDir, p.Architecture)
if err != nil {
return fmt.Errorf("failed to create bundle: %v", err)
}
Expand All @@ -44,8 +44,13 @@ func (p *LambdaProvider) Bundle(configDir string, dest string) error {
return nil
}

func CreateDeploymentPackage(version string, dir string) (*[]byte, error) {
binaryPath, err := checkOrDownloadBinary(version)
// CreateDeploymentPackage assembles the AWS Lambda zip for the given engine
// version, embedding the configuration directory contents. arch selects the
// underlying binary's CPU architecture (Go-style: "amd64" or "arm64") for
// engine flavours whose binaries are arch-specific (the native engine);
// callers may pass "" to fall back to DefaultLambdaArch.
func CreateDeploymentPackage(version string, dir string, arch string) (*[]byte, error) {
binaryPath, err := checkOrDownloadBinary(version, arch)
if err != nil {
return nil, err
}
Expand Down
6 changes: 6 additions & 0 deletions internal/engine/awslambda/library_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ type LambdaLibrary struct{}

type LambdaProvider struct {
engine.EngineMetadata

// Architecture selects the target CPU architecture for the bundled
// binary (Go-style: "amd64" or "arm64"). It is only consulted by engine
// flavours that ship per-architecture binaries (the native engine). If
// left empty, CreateDeploymentPackage falls back to DefaultLambdaArch.
Architecture string
}

var logger = logging.GetLogger()
Expand Down
31 changes: 27 additions & 4 deletions internal/engine/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,35 @@ func GetConfiguredType(override string) EngineType {
return GetConfiguredTypeWithDefault(override, defaultEngineType)
}

// GetConfiguredTypeWithVersion is like GetConfiguredType but also takes an
// engine-version override so that a pinned version can imply an engine type
// when no explicit one is configured. CLI commands that expose a --version
// flag should pass its value as versionOverride.
func GetConfiguredTypeWithVersion(typeOverride string, versionOverride string) EngineType {
return getConfiguredType(typeOverride, versionOverride, defaultEngineType)
}

func GetConfiguredTypeWithDefault(override string, defaultType EngineType) EngineType {
return normaliseEngineType(EngineType(stringutil.GetFirstNonEmpty(
override,
return getConfiguredType(override, "", defaultType)
}

func getConfiguredType(typeOverride string, versionOverride string, defaultType EngineType) EngineType {
explicit := stringutil.GetFirstNonEmpty(
typeOverride,
viper.GetString("engine"),
string(defaultType),
)))
)
if explicit != "" {
return normaliseEngineType(EngineType(explicit))
}
// No explicit engine type configured. If the user has pinned a specific
// engine version we can sometimes derive the engine type from it (e.g.
// 5.x implies the native engine). "latest" intentionally does not derive
// — callers keep the supplied default until "latest" is re-pointed at v5.
version := stringutil.GetFirstNonEmpty(versionOverride, viper.GetString("version"))
if derived := DeriveEngineTypeFromVersion(version); derived != EngineTypeNone {
return derived
}
return defaultType
}

func GetConfiguredVersion(engineType EngineType, override string, allowCached bool) string {
Expand Down
39 changes: 39 additions & 0 deletions internal/engine/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,45 @@ func TestGetConfiguredType(t *testing.T) {
}
}

func TestGetConfiguredTypeWithVersion(t *testing.T) {
type args struct {
typeOverride string
versionOverride string
}
tests := []struct {
name string
args args
configureType string
configureVersion string
want EngineType
}{
{name: "explicit type wins over version", args: args{typeOverride: "docker", versionOverride: "5.0.0"}, want: EngineTypeDockerCore},
{name: "configured type wins over version", args: args{versionOverride: "5.0.0"}, configureType: "jvm", want: EngineTypeJvmSingleJar},
{name: "5.x version override derives native", args: args{versionOverride: "5.0.0"}, want: EngineTypeNative},
{name: "5.x configured version derives native", configureVersion: "5.2.3", want: EngineTypeNative},
{name: "4.x version keeps default", args: args{versionOverride: "4.9.0"}, want: defaultEngineType},
{name: "latest keeps default", args: args{versionOverride: "latest"}, want: defaultEngineType},
{name: "no version, no type, returns default", want: defaultEngineType},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.configureType != "" {
viper.Set("engine", tt.configureType)
}
if tt.configureVersion != "" {
viper.Set("version", tt.configureVersion)
}
t.Cleanup(func() {
viper.Set("engine", nil)
viper.Set("version", nil)
})
if got := GetConfiguredTypeWithVersion(tt.args.typeOverride, tt.args.versionOverride); got != tt.want {
t.Errorf("GetConfiguredTypeWithVersion() = %v, want %v", got, tt.want)
}
})
}
}

func TestSanitiseVersionOutput(t *testing.T) {
type args struct {
s string
Expand Down
Loading
Loading