AWSlambda: Let create_function reference ECR via ImageUri (#5688)

This commit is contained in:
Brendan Keane 2022-11-23 05:16:33 -08:00 committed by GitHub
parent 00c0a66d1c
commit 3d95ac0978
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 178 additions and 46 deletions

View File

@ -28,9 +28,11 @@ from moto.core.exceptions import RESTError
from moto.core.utils import unix_time_millis
from moto.iam.models import iam_backends
from moto.iam.exceptions import IAMNotFoundException
from moto.ecr.exceptions import ImageNotFoundException
from moto.logs.models import logs_backends
from moto.moto_api._internal import mock_random as random
from moto.s3.models import s3_backends, FakeKey
from moto.ecr.models import ecr_backends
from moto.s3.exceptions import MissingBucket, MissingKey
from moto import settings
from .exceptions import (
@ -481,10 +483,29 @@ class LambdaFunction(CloudFormationModel, DockerModel):
self.code_size = 0
self.code_sha_256 = ""
elif "ImageUri" in self.code:
self.code_sha_256 = hashlib.sha256(
self.code["ImageUri"].encode("utf-8")
).hexdigest()
self.code_size = 0
if settings.lambda_stub_ecr():
self.code_sha_256 = hashlib.sha256(
self.code["ImageUri"].encode("utf-8")
).hexdigest()
self.code_size = 0
else:
uri, tag = self.code["ImageUri"].split(":")
repo_name = uri.split("/")[-1]
image_id = {"imageTag": tag}
ecr_backend = ecr_backends[self.account_id][self.region]
registry_id = ecr_backend.describe_registry()["registryId"]
images = ecr_backend.batch_get_image(
repository_name=repo_name, image_ids=[image_id]
)["images"]
if len(images) == 0:
raise ImageNotFoundException(image_id, repo_name, registry_id) # type: ignore
else:
manifest = json.loads(images[0]["imageManifest"])
self.code_sha_256 = images[0]["imageId"]["imageDigest"].replace(
"sha256:", ""
)
self.code_size = manifest["config"]["size"]
self.function_arn = make_function_arn(
self.region, self.account_id, self.function_name

View File

@ -249,9 +249,11 @@ class Image(BaseObject):
self.last_scan = None
def _create_digest(self):
image_contents = f"docker_image{int(random.random() * 10**6)}"
image_manifest = json.loads(self.image_manifest)
layer_digests = [layer["digest"] for layer in image_manifest["layers"]]
self.image_digest = (
"sha256:" + hashlib.sha256(image_contents.encode("utf-8")).hexdigest()
"sha256:"
+ hashlib.sha256("".join(layer_digests).encode("utf-8")).hexdigest()
)
def get_image_digest(self):

View File

@ -69,6 +69,13 @@ def allow_unknown_region() -> bool:
return os.environ.get("MOTO_ALLOW_NONEXISTENT_REGION", "false").lower() == "true"
def lambda_stub_ecr() -> bool:
# Whether to stub or mock ecr backend when deploying image based lambdas.
# True => don't requiring image presence in moto ecr backend for `create_function`.
# False => require image presence in moto ecr backend for `create_function`
return os.environ.get("MOTO_LAMBDA_STUB_ECR", "TRUE").lower() != "false"
def moto_server_port() -> str:
return os.environ.get("MOTO_PORT") or "5000"

View File

@ -1,4 +1,7 @@
import base64
import json
import os
from unittest import SkipTest
import botocore.client
import boto3
import hashlib
@ -7,7 +10,8 @@ import pytest
from botocore.exceptions import ClientError
from freezegun import freeze_time
from moto import mock_lambda, mock_s3
from tests.test_ecr.test_ecr_helpers import _create_image_manifest
from moto import mock_lambda, mock_s3, mock_ecr, settings
from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
from uuid import uuid4
from .utilities import (
@ -205,11 +209,29 @@ def test_create_function__with_tracingmode(tracing_mode):
result.should.have.key("TracingConfig").should.equal({"Mode": output})
@pytest.fixture(name="with_ecr_mock")
def ecr_repo_fixture():
with mock_ecr():
os.environ["MOTO_LAMBDA_STUB_ECR"] = "FALSE"
repo_name = "testlambdaecr"
ecr_client = ecr_client = boto3.client("ecr", "us-east-1")
ecr_client.create_repository(repositoryName=repo_name)
ecr_client.put_image(
repositoryName=repo_name,
imageManifest=json.dumps(_create_image_manifest()),
imageTag="latest",
)
yield
ecr_client.delete_repository(repositoryName=repo_name, force=True)
os.environ["MOTO_LAMBDA_STUB_ECR"] = "TRUE"
@mock_lambda
def test_create_function_from_image():
def test_create_function_from_stubbed_ecr():
lambda_client = boto3.client("lambda", "us-east-1")
fn_name = str(uuid4())[0:6]
image_uri = "111122223333.dkr.ecr.us-east-1.amazonaws.com/testlambda:latest"
dic = {
"FunctionName": fn_name,
"Role": get_role_name(),
@ -217,6 +239,7 @@ def test_create_function_from_image():
"PackageType": "Image",
"Timeout": 100,
}
resp = lambda_client.create_function(**dic)
resp.should.have.key("FunctionName").equals(fn_name)
@ -236,6 +259,79 @@ def test_create_function_from_image():
code.should.have.key("ResolvedImageUri").equals(resolved_image_uri)
@mock_lambda
def test_create_function_from_mocked_ecr_image(
with_ecr_mock,
): # pylint: disable=unused-argument
if settings.TEST_SERVER_MODE:
raise SkipTest(
"Envars not easily set in server mode, feature off by default, skipping..."
)
lambda_client = boto3.client("lambda", "us-east-1")
fn_name = str(uuid4())[0:6]
image_uri = f"{ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/testlambdaecr:latest"
dic = {
"FunctionName": fn_name,
"Role": get_role_name(),
"Code": {"ImageUri": image_uri},
"PackageType": "Image",
"Timeout": 100,
}
resp = lambda_client.create_function(**dic)
resp.should.have.key("FunctionName").equals(fn_name)
resp.should.have.key("CodeSize").greater_than(0)
resp.should.have.key("CodeSha256")
resp.should.have.key("PackageType").equals("Image")
result = lambda_client.get_function(FunctionName=fn_name)
result.should.have.key("Configuration")
config = result["Configuration"]
config.should.have.key("CodeSha256").equals(resp["CodeSha256"])
config.should.have.key("CodeSize").equals(resp["CodeSize"])
result.should.have.key("Code")
code = result["Code"]
code.should.have.key("RepositoryType").equals("ECR")
code.should.have.key("ImageUri").equals(image_uri)
image_uri_without_tag = image_uri.split(":")[0]
resolved_image_uri = f"{image_uri_without_tag}@sha256:{config['CodeSha256']}"
code.should.have.key("ResolvedImageUri").equals(resolved_image_uri)
@mock_lambda
def test_create_function_from_mocked_ecr_missing_image(
with_ecr_mock,
): # pylint: disable=unused-argument
if settings.TEST_SERVER_MODE:
raise SkipTest(
"Envars not easily set in server mode, feature off by default, skipping..."
)
lambda_client = boto3.client("lambda", "us-east-1")
fn_name = str(uuid4())[0:6]
image_uri = f"{ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/testlambdaecr:dne"
dic = {
"FunctionName": fn_name,
"Role": get_role_name(),
"Code": {"ImageUri": image_uri},
"PackageType": "Image",
"Timeout": 100,
}
with pytest.raises(ClientError) as exc:
lambda_client.create_function(**dic)
err = exc.value.response["Error"]
err["Code"].should.equal("ImageNotFoundException")
err["Message"].should.equal(
"The image with imageId {'imageTag': 'dne'} does not exist within the repository with name 'testlambdaecr' in the registry with id '123456789012'"
)
@mock_lambda
@mock_s3
@freeze_time("2015-01-01 00:00:00")

View File

@ -1,11 +1,9 @@
import hashlib
import json
from datetime import datetime
import pytest
from freezegun import freeze_time
import os
from random import random
import re
import sure # noqa # pylint: disable=unused-import
@ -19,41 +17,7 @@ from unittest import SkipTest
from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
def _create_image_digest(contents=None):
if not contents:
contents = f"docker_image{int(random() * 10**6)}"
return "sha256:" + hashlib.sha256(contents.encode("utf-8")).hexdigest()
def _create_image_manifest():
return {
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 7023,
"digest": _create_image_digest("config"),
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 32654,
"digest": _create_image_digest("layer1"),
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 16724,
"digest": _create_image_digest("layer2"),
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 73109,
# randomize image digest
"digest": _create_image_digest(),
},
],
}
from tests.test_ecr.test_ecr_helpers import _create_image_manifest
@mock_ecr
@ -1298,7 +1262,9 @@ def test_delete_batch_image_with_multiple_images():
# Populate mock repo with images
for i in range(10):
client.put_image(
repositoryName=repo_name, imageManifest=f"manifest{i}", imageTag=f"tag{i}"
repositoryName=repo_name,
imageManifest=json.dumps(_create_image_manifest()),
imageTag=f"tag{i}",
)
# Pull down image digests for each image in the mock repo

View File

@ -0,0 +1,40 @@
import hashlib
import random
def _generate_random_sha():
return hashlib.sha256(f"{random.randint(0,100)}".encode("utf-8")).hexdigest()
def _create_image_layers(n):
layers = []
for _ in range(n):
layers.append(
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": random.randint(100, 1000),
"digest": f"sha256:{_generate_random_sha()}",
}
)
return layers
def _create_image_digest(layers):
layer_digests = "".join([layer["digest"] for layer in layers])
return hashlib.sha256(f"{layer_digests}".encode("utf-8")).hexdigest()
def _create_image_manifest(image_digest=None):
layers = _create_image_layers(5)
if image_digest is None:
image_digest = _create_image_digest(layers)
return {
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": sum([layer["size"] for layer in layers]),
"digest": image_digest,
},
"layers": layers,
}