Skip to content
This repository was archived by the owner on Dec 2, 2020. It is now read-only.

Commit 5b337d4

Browse files
committed
Adding support for OCI archives
Fixes #1
1 parent 185f973 commit 5b337d4

File tree

5 files changed

+174
-17
lines changed

5 files changed

+174
-17
lines changed

README.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ If a layer is already published to Lambda (same layer name, SHA256 digest, and s
1919
+ [Binaries](#binaries)
2020
+ [From Source](#from-source)
2121
- [Permissions](#permissions)
22-
- [Example](#example)
22+
- [Examples](#examples)
23+
+ [Docker Example](#docker-example)
24+
+ [OCI Example](#oci-example)
25+
+ [Deploy Manually](#deploy-manually)
26+
+ [Deploy with AWS Serverless Application Model (SAM)](#deploy-with-aws-serverless-application-model-sam)
27+
+ [Deploy with Serverless Framework](#deploy-with-serverless-framework)
2328
- [License Summary](#license-summary)
2429
- [Security Disclosures](#security-disclosures)
2530

@@ -32,7 +37,8 @@ USAGE:
3237
img2lambda [options]
3338
3439
GLOBAL OPTIONS:
35-
--image value, -i value Name of the source container image. For example, 'my-docker-image:latest'. The Docker image must be pulled locally already.
40+
--image value, -i value Name or path of the source container image. For example, 'my-docker-image:latest' or './my-oci-image-archive'. The image must be pulled locally already.
41+
--image-type value, -t value Type of the source container image. Valid values: 'docker' (Docker image from the local Docker daemon), 'oci' (OCI image archive at the given path and optional tag) (default: "docker")
3642
--region value, -r value AWS region (default: "us-east-1")
3743
--profile value, -p value AWS credentials profile. Credentials will default to the same chain as the AWS CLI: environment variables, default profile, container credentials, EC2 instance credentials
3844
--output-directory value, -o value Destination directory for command output (default: "./output")
@@ -114,7 +120,9 @@ For example:
114120
}
115121
```
116122

117-
## Example
123+
## Examples
124+
125+
### Docker Example
118126

119127
Build the example Docker image to create a PHP Lambda custom runtime:
120128
```
@@ -135,6 +143,22 @@ Run the tool to create and publish Lambda layers that contain the PHP custom run
135143
../bin/local/img2lambda -i lambda-php:latest -r us-east-1 -o ./output
136144
```
137145

146+
### OCI Example
147+
148+
Create an OCI image from the example Dockerfile:
149+
```
150+
cd example
151+
152+
podman build --format oci -t lambda-php .
153+
154+
podman push lambda-php oci-archive:./lambda-php-oci
155+
```
156+
157+
Run the tool to create and publish Lambda layers that contain the PHP custom runtime:
158+
```
159+
../bin/local/img2lambda -i ./lambda-php-oci -t oci -r us-east-1 -o ./output
160+
```
161+
138162
### Deploy Manually
139163
Create a PHP function that uses the layers:
140164
```

img2lambda/cli/main.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"log"
99
"os"
10+
"strings"
1011

1112
"github.com/awslabs/aws-lambda-container-image-converter/img2lambda/extract"
1213
"github.com/awslabs/aws-lambda-container-image-converter/img2lambda/publish"
@@ -28,14 +29,20 @@ func createApp() (*cli.App, *types.CmdOptions) {
2829
opts.CompatibleRuntimes = c.StringSlice("cr")
2930

3031
validateCliOptions(&opts, c)
31-
return repackImageAction(&opts)
32+
return repackImageAction(&opts, c)
3233
}
3334
app.Flags = []cli.Flag{
3435
cli.StringFlag{
3536
Name: "image, i",
36-
Usage: "Name of the source container image. For example, 'my-docker-image:latest'. The Docker image must be pulled locally already.",
37+
Usage: "Name or path of the source container image. For example, 'my-docker-image:latest' or './my-oci-image-archive'. The image must be pulled locally already.",
3738
Destination: &opts.Image,
3839
},
40+
cli.StringFlag{
41+
Name: "image-type, t",
42+
Usage: "Type of the source container image. Valid values: 'docker' (Docker image from the local Docker daemon), 'oci' (OCI image archive at the given path)",
43+
Value: "docker",
44+
Destination: &opts.ImageType,
45+
},
3946
cli.StringFlag{
4047
Name: "region, r",
4148
Usage: "AWS region",
@@ -101,8 +108,24 @@ func validateCliOptions(opts *types.CmdOptions, context *cli.Context) {
101108
}
102109
}
103110

104-
func repackImageAction(opts *types.CmdOptions) error {
105-
layers, err := extract.RepackImage("docker-daemon:"+opts.Image, opts.OutputDir)
111+
func repackImageAction(opts *types.CmdOptions, context *cli.Context) error {
112+
var imageTransport string
113+
switch opts.ImageType {
114+
case "docker":
115+
imageTransport = "docker-daemon:"
116+
case "oci":
117+
imageTransport = "oci-archive:"
118+
default:
119+
fmt.Println("ERROR: Image type must be one of the supported image types")
120+
cli.ShowAppHelpAndExit(context, 1)
121+
}
122+
123+
imageLocation := imageTransport + opts.Image
124+
if opts.ImageType == "docker" && strings.Count(imageLocation, ":") == 1 {
125+
imageLocation += ":latest"
126+
}
127+
128+
layers, err := extract.RepackImage(imageLocation, opts.OutputDir)
106129
if err != nil {
107130
return err
108131
}

img2lambda/extract/repack_image.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package extract
44

55
import (
66
"archive/tar"
7+
"compress/gzip"
78
"context"
89
"fmt"
910
"io"
@@ -24,7 +25,7 @@ import (
2425

2526
// Converts container image to Lambda layer archive files
2627
func RepackImage(imageName string, layerOutputDir string) (layers []types.LambdaLayer, retErr error) {
27-
log.Printf("Parsing the docker image %s", imageName)
28+
log.Printf("Parsing the image %s", imageName)
2829

2930
// Get image's layer data from image name
3031
ref, err := alltransports.ParseImageName(imageName)
@@ -104,9 +105,21 @@ func repackImage(opts *repackOptions) (layers []types.LambdaLayer, retErr error)
104105
}
105106
defer layerStream.Close()
106107

107-
fileCreated, err := repackLayer(lambdaLayerFilename, layerStream)
108+
fileCreated, err := repackLayer(lambdaLayerFilename, layerStream, false)
108109
if err != nil {
109-
return nil, err
110+
tarErr := err
111+
112+
// tar extraction failed, try tar.gz
113+
layerStream, _, err = opts.rawImageSource.GetBlob(opts.ctx, layerInfo, opts.cache)
114+
if err != nil {
115+
return nil, err
116+
}
117+
defer layerStream.Close()
118+
119+
fileCreated, err = repackLayer(lambdaLayerFilename, layerStream, true)
120+
if err != nil {
121+
return nil, fmt.Errorf("could not read layer with tar nor tar.gz: %v, %v", err, tarErr)
122+
}
110123
}
111124

112125
if fileCreated {
@@ -126,10 +139,21 @@ func repackImage(opts *repackOptions) (layers []types.LambdaLayer, retErr error)
126139
// Converts container image layer archive (tar) to Lambda layer archive (zip).
127140
// Filters files from the source and only writes a new archive if at least
128141
// one file in the source matches the filter (i.e. does not create empty archives).
129-
func repackLayer(outputFilename string, layerContents io.Reader) (created bool, retError error) {
142+
func repackLayer(outputFilename string, layerContents io.Reader, isGzip bool) (created bool, retError error) {
130143
t := archiver.NewTar()
144+
contentsReader := layerContents
145+
var err error
146+
147+
if isGzip {
148+
gzr, err := gzip.NewReader(layerContents)
149+
if err != nil {
150+
return false, fmt.Errorf("could not create gzip reader for layer: %v", err)
151+
}
152+
defer gzr.Close()
153+
contentsReader = gzr
154+
}
131155

132-
err := t.Open(layerContents, 0)
156+
err = t.Open(contentsReader, 0)
133157
if err != nil {
134158
return false, fmt.Errorf("opening layer tar: %v", err)
135159
}

img2lambda/extract/repack_image_test.go

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"archive/zip"
77
"bufio"
88
"bytes"
9+
"context"
910
"io"
1011
"io/ioutil"
1112
"os"
@@ -20,12 +21,10 @@ import (
2021
"github.com/stretchr/testify/assert"
2122
)
2223

23-
func createImageLayer(t *testing.T,
24-
rawSource *mocks.MockImageSource,
24+
func CreateLayerData(t *testing.T,
2525
filename string,
2626
fileContents string,
27-
digest string) *imgtypes.BlobInfo {
28-
27+
digest string) *bytes.Buffer {
2928
tar := archiver.NewTar()
3029

3130
layerFile, err := ioutil.TempFile("", "")
@@ -60,6 +59,17 @@ func createImageLayer(t *testing.T,
6059
err = os.Remove(layerFile.Name())
6160
assert.Nil(t, err)
6261

62+
return &tarContents
63+
}
64+
65+
func createImageLayer(t *testing.T,
66+
rawSource *mocks.MockImageSource,
67+
filename string,
68+
fileContents string,
69+
digest string) *imgtypes.BlobInfo {
70+
71+
tarContents := CreateLayerData(t, filename, fileContents, digest)
72+
6373
blobInfo := imgtypes.BlobInfo{Digest: godigest.Digest(digest)}
6474

6575
rawSource.EXPECT().GetBlob(gomock.Any(),
@@ -69,6 +79,40 @@ func createImageLayer(t *testing.T,
6979
return &blobInfo
7080
}
7181

82+
func createGzipImageLayer(t *testing.T,
83+
rawSource *mocks.MockImageSource,
84+
filename string,
85+
fileContents string,
86+
digest string) *imgtypes.BlobInfo {
87+
88+
tarContents := CreateLayerData(t, filename, fileContents, digest)
89+
90+
var targzContents bytes.Buffer
91+
bufWriter := bufio.NewWriter(&targzContents)
92+
93+
gz := archiver.NewGz()
94+
err := gz.Compress(bytes.NewReader(tarContents.Bytes()), bufWriter)
95+
assert.Nil(t, err)
96+
err = bufWriter.Flush()
97+
assert.Nil(t, err)
98+
99+
blobInfo := imgtypes.BlobInfo{Digest: godigest.Digest(digest)}
100+
101+
contentsBytes := targzContents.Bytes()
102+
103+
rawSource.EXPECT().GetBlob(gomock.Any(),
104+
blobInfo,
105+
gomock.Any()).
106+
DoAndReturn(
107+
func(context context.Context, blobInfo imgtypes.BlobInfo, cache imgtypes.BlobInfoCache) (io.ReadCloser, int64, error) {
108+
// checks whatever
109+
return ioutil.NopCloser(bytes.NewReader(contentsBytes)), int64(0), nil
110+
}).
111+
Times(2)
112+
113+
return &blobInfo
114+
}
115+
72116
func validateLambdaLayer(t *testing.T,
73117
layer *types.LambdaLayer,
74118
expectedFilename string,
@@ -126,7 +170,7 @@ func TestRepack(t *testing.T) {
126170
blobInfos = append(blobInfos, *blobInfo1)
127171

128172
// Second matching file
129-
blobInfo2 := createImageLayer(t, rawSource, "opt/hello/file2", "hello world 2", "digest2")
173+
blobInfo2 := createGzipImageLayer(t, rawSource, "opt/hello/file2", "hello world 2", "digest2")
130174
blobInfos = append(blobInfos, *blobInfo2)
131175

132176
// Irrelevant file
@@ -161,3 +205,44 @@ func TestRepack(t *testing.T) {
161205
err = os.Remove(dir)
162206
assert.Nil(t, err)
163207
}
208+
209+
func TestRepackFailure(t *testing.T) {
210+
ctrl := gomock.NewController(t)
211+
defer ctrl.Finish()
212+
213+
source := mocks.NewMockImageCloser(ctrl)
214+
rawSource := mocks.NewMockImageSource(ctrl)
215+
216+
// Create layer tar files
217+
var blobInfos []imgtypes.BlobInfo
218+
219+
// Add garbage data to layer
220+
blobInfo := imgtypes.BlobInfo{Digest: godigest.Digest("digest1")}
221+
rawSource.EXPECT().GetBlob(gomock.Any(),
222+
blobInfo,
223+
gomock.Any()).
224+
Return(ioutil.NopCloser(bytes.NewReader([]byte("hello world"))), int64(0), nil).
225+
Times(2)
226+
blobInfos = append(blobInfos, blobInfo)
227+
228+
source.EXPECT().LayerInfos().Return(blobInfos)
229+
230+
dir, err := ioutil.TempDir("", "")
231+
assert.Nil(t, err)
232+
233+
layers, err := repackImage(&repackOptions{
234+
ctx: nil,
235+
cache: nil,
236+
imageSource: source,
237+
rawImageSource: rawSource,
238+
imageName: "test-image",
239+
layerOutputDir: dir,
240+
})
241+
242+
assert.Nil(t, layers)
243+
assert.NotNil(t, err)
244+
assert.Equal(t, err.Error(), "could not read layer with tar nor tar.gz: could not create gzip reader for layer: EOF, opening next file in layer tar: unexpected EOF")
245+
246+
err = os.Remove(dir)
247+
assert.Nil(t, err)
248+
}

img2lambda/types/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type LambdaLayer struct {
1414

1515
type CmdOptions struct {
1616
Image string // Name of the container image
17+
ImageType string // Type of the container image
1718
Region string // AWS region
1819
Profile string // AWS credentials profile
1920
OutputDir string // Output directory for the Lambda layers

0 commit comments

Comments
 (0)