mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-11-03 01:33:37 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 375200a26b |
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
@@ -18,6 +19,9 @@ import (
|
||||
// codeURLTTL is how long the presigned code location stays valid.
|
||||
const codeURLTTL = 10 * time.Minute
|
||||
|
||||
// maxListItems is the largest MaxItems a list action accepts.
|
||||
const maxListItems = 10000
|
||||
|
||||
func notFound(region, accountID, name string) error {
|
||||
return awserrors.Errorf(awserrors.ErrorResourceNotFoundException, "Function not found: %s",
|
||||
arn.FormatLambdaFunction(region, accountID, name))
|
||||
@@ -48,8 +52,8 @@ func (d Deps) lookup(ctx context.Context, c *call, query string) (function.Recor
|
||||
|
||||
// loadConfiguration resolves the requested $LATEST function and renders its
|
||||
// configuration; it is a metadata read and never touches the code store.
|
||||
func (d Deps) loadConfiguration(ctx context.Context, c *call) (function.Record, *lambda.FunctionConfiguration, error) {
|
||||
rec, revision, err := d.lookup(ctx, c, c.Query.Get("Qualifier"))
|
||||
func (d Deps) loadConfiguration(ctx context.Context, c *call, query string) (function.Record, *lambda.FunctionConfiguration, error) {
|
||||
rec, revision, err := d.lookup(ctx, c, query)
|
||||
if err != nil {
|
||||
return function.Record{}, nil, err
|
||||
}
|
||||
@@ -73,7 +77,7 @@ func (d Deps) loadConfiguration(ctx context.Context, c *call) (function.Record,
|
||||
|
||||
// getFunctionConfiguration implements GetFunctionConfiguration for $LATEST.
|
||||
func (d Deps) getFunctionConfiguration(ctx context.Context, c *call) (reply, error) {
|
||||
_, cfg, err := d.loadConfiguration(ctx, c)
|
||||
_, cfg, err := d.loadConfiguration(ctx, c, c.Query.Get("Qualifier"))
|
||||
if err != nil {
|
||||
return reply{}, err
|
||||
}
|
||||
@@ -93,9 +97,29 @@ func (d Deps) getFunctionCodeSigningConfig(ctx context.Context, c *call) (reply,
|
||||
return reply{Status: http.StatusOK, Body: noCodeSigning}, nil
|
||||
}
|
||||
|
||||
// listVersionsByFunction implements ListVersionsByFunction while $LATEST is
|
||||
// the only version. Paging is refused rather than guessed at.
|
||||
func (d Deps) listVersionsByFunction(ctx context.Context, c *call) (reply, error) {
|
||||
if raw, ok := c.Query["MaxItems"]; ok {
|
||||
n, err := strconv.Atoi(raw[0])
|
||||
if err != nil || n < 1 || n > maxListItems {
|
||||
return reply{}, invalid("MaxItems must be between 1 and %d", maxListItems)
|
||||
}
|
||||
}
|
||||
if c.Query.Has("Marker") {
|
||||
return reply{}, unsupported("Marker")
|
||||
}
|
||||
rec, cfg, err := d.loadConfiguration(ctx, c, "")
|
||||
if err != nil {
|
||||
return reply{}, err
|
||||
}
|
||||
cfg.FunctionArn = aws.String(arn.FormatLambdaFunction(rec.Region, rec.AccountID, rec.FunctionName) + ":$LATEST")
|
||||
return reply{Status: http.StatusOK, Body: &lambda.ListVersionsByFunctionOutput{Versions: []*lambda.FunctionConfiguration{cfg}}}, nil
|
||||
}
|
||||
|
||||
// getFunction implements GetFunction for the $LATEST version.
|
||||
func (d Deps) getFunction(ctx context.Context, c *call) (reply, error) {
|
||||
rec, cfg, err := d.loadConfiguration(ctx, c)
|
||||
rec, cfg, err := d.loadConfiguration(ctx, c, c.Query.Get("Qualifier"))
|
||||
if err != nil {
|
||||
return reply{}, err
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -662,7 +663,8 @@ func TestAuthorization(t *testing.T) {
|
||||
|
||||
func TestRegistration_ResolvesActionsAndInventory(t *testing.T) {
|
||||
reg := awsapi.NewRegistration(awsapi.Deps{})
|
||||
assert.Equal(t, []string{"CreateFunction", "GetFunction", "GetFunctionCodeSigningConfig", "GetFunctionConfiguration"}, reg.Inventory.Registered)
|
||||
assert.Equal(t, []string{"CreateFunction", "GetFunction", "GetFunctionCodeSigningConfig", "GetFunctionConfiguration", "ListVersionsByFunction"}, reg.Inventory.Registered)
|
||||
assert.Equal(t, "ListVersionsByFunction", reg.ResolveAction(httptest.NewRequest(http.MethodGet, "/2015-03-31/functions/x/versions", nil)))
|
||||
assert.Equal(t, "GetFunctionCodeSigningConfig", reg.ResolveAction(httptest.NewRequest(http.MethodGet, "/2020-06-30/functions/x/code-signing-config", nil)))
|
||||
assert.Equal(t, "CreateFunction", reg.ResolveAction(httptest.NewRequest(http.MethodPost, "/2015-03-31/functions", nil)))
|
||||
assert.Equal(t, "GetFunction", reg.ResolveAction(httptest.NewRequest(http.MethodGet, "/2015-03-31/functions/x", nil)))
|
||||
@@ -870,3 +872,107 @@ func TestGetFunctionCodeSigningConfig_Authorization(t *testing.T) {
|
||||
require.NoError(t, get(setup(t, gwtest.Statement("Allow", "lambda:GetFunctionCodeSigningConfig", arn))))
|
||||
})
|
||||
}
|
||||
|
||||
func TestListVersionsByFunction_ListsOnlyLatest(t *testing.T) {
|
||||
e := newEnv(t)
|
||||
_, err := e.do(http.MethodPost, "/2015-03-31/functions", createBody("hello", func(b map[string]any) {
|
||||
b["Environment"] = map[string]any{"Variables": map[string]string{"MODE": "prod"}}
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
rec, err := e.do(http.MethodGet, "/2015-03-31/functions/hello/configuration", nil)
|
||||
require.NoError(t, err)
|
||||
var cfg lambda.FunctionConfiguration
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &cfg))
|
||||
|
||||
arn := "arn:aws:lambda:" + gwtest.Region + ":" + gwtest.AccountID + ":function:hello"
|
||||
for _, path := range []string{
|
||||
"/2015-03-31/functions/hello/versions",
|
||||
"/2015-03-31/functions/hello/versions?MaxItems=10000",
|
||||
"/2015-03-31/functions/hello/versions?MaxItems=1",
|
||||
"/2015-03-31/functions/" + url.PathEscape(arn) + "/versions",
|
||||
} {
|
||||
rec, err := e.do(http.MethodGet, path, nil)
|
||||
require.NoError(t, err, path)
|
||||
require.Equal(t, http.StatusOK, rec.Code, path)
|
||||
assert.NotContains(t, rec.Body.String(), "NextMarker", path)
|
||||
var out lambda.ListVersionsByFunctionOutput
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &out), path)
|
||||
require.Len(t, out.Versions, 1, path)
|
||||
v := out.Versions[0]
|
||||
assert.Equal(t, arn+":$LATEST", *v.FunctionArn, path)
|
||||
assert.Equal(t, "$LATEST", *v.Version, path)
|
||||
assert.Equal(t, "prod", *v.Environment.Variables["MODE"], path)
|
||||
v.FunctionArn = cfg.FunctionArn
|
||||
assert.Equal(t, cfg, *v, "apart from the qualified ARN it is the configuration read")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListVersionsByFunction_RefusesBadMaxItemsAndAnyMarker(t *testing.T) {
|
||||
e := newEnv(t)
|
||||
_, err := e.do(http.MethodPost, "/2015-03-31/functions", createBody("hello", nil))
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, q := range []string{"MaxItems=0", "MaxItems=10001", "MaxItems=-1", "MaxItems=ten", "MaxItems=", "Marker=not-a-marker", "Marker="} {
|
||||
_, err := e.do(http.MethodGet, "/2015-03-31/functions/hello/versions?"+q, nil)
|
||||
requireCode(t, err, awserrors.ErrorInvalidParameterValueException)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListVersionsByFunction_MissingFunctionIsNotFound(t *testing.T) {
|
||||
e := newEnv(t)
|
||||
_, err := e.do(http.MethodPost, "/2015-03-31/functions", createBody("hello", nil))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = e.do(http.MethodGet, "/2015-03-31/functions/ghost/versions", nil)
|
||||
requireCode(t, err, awserrors.ErrorResourceNotFoundException)
|
||||
_, getErr := e.do(http.MethodGet, "/2015-03-31/functions/ghost", nil)
|
||||
assert.Equal(t, getErr.Error(), err.Error())
|
||||
|
||||
e.h.Region = "us-west-2"
|
||||
_, err = e.do(http.MethodGet, "/2015-03-31/functions/hello/versions", nil)
|
||||
requireCode(t, err, awserrors.ErrorResourceNotFoundException)
|
||||
}
|
||||
|
||||
func TestListVersionsByFunction_DoesNotNeedTheCodeStore(t *testing.T) {
|
||||
e := newEnv(t)
|
||||
_, err := e.do(http.MethodPost, "/2015-03-31/functions", createBody("hello", nil))
|
||||
require.NoError(t, err)
|
||||
e.deps.Codes = func(context.Context) (awsapi.CodeStore, error) { return nil, errors.New("code store down") }
|
||||
_, err = e.do(http.MethodGet, "/2015-03-31/functions/hello/versions", nil)
|
||||
require.NoError(t, err)
|
||||
e.deps.Codes = nil
|
||||
_, err = e.do(http.MethodGet, "/2015-03-31/functions/hello/versions", nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestListVersionsByFunction_Authorization(t *testing.T) {
|
||||
arn := "arn:aws:lambda:" + gwtest.Region + ":" + gwtest.AccountID + ":function:hello"
|
||||
setup := func(t *testing.T, statements ...iamdomain.Statement) *env {
|
||||
e := newEnv(t, statements...)
|
||||
_, err := e.meta.Create(t.Context(), gwtest.AccountID, gwtest.Region, function.Record{
|
||||
FunctionID: "f1", FunctionName: "hello", Role: roleARN, Runtime: "provided.al2023", Handler: "h",
|
||||
Timeout: 3, MemorySize: 128, Code: function.Code{RevisionID: "r1"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return e
|
||||
}
|
||||
list := func(e *env) error {
|
||||
_, err := e.do(http.MethodGet, "/2015-03-31/functions/hello/versions", nil)
|
||||
return err
|
||||
}
|
||||
|
||||
t.Run("lambda:GetFunction alone is not enough", func(t *testing.T) {
|
||||
gwtest.AssertDenied(t, list(setup(t, gwtest.Statement("Allow", "lambda:GetFunction", "*"))))
|
||||
})
|
||||
t.Run("a deny on the function ARN applies", func(t *testing.T) {
|
||||
gwtest.AssertDenied(t, list(setup(t,
|
||||
gwtest.Statement("Allow", "lambda:*", "*"),
|
||||
gwtest.Statement("Deny", "lambda:ListVersionsByFunction", arn))))
|
||||
})
|
||||
t.Run("an allow on another function ARN does not apply", func(t *testing.T) {
|
||||
gwtest.AssertDenied(t, list(setup(t, gwtest.Statement("Allow", "lambda:ListVersionsByFunction", arn+"-other"))))
|
||||
})
|
||||
t.Run("an allow on the function ARN passes", func(t *testing.T) {
|
||||
require.NoError(t, list(setup(t, gwtest.Statement("Allow", "lambda:ListVersionsByFunction", arn))))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func (d Deps) serve(w http.ResponseWriter, inv dispatch.Invocation) error {
|
||||
if decodeErr == nil {
|
||||
resource = functionARN(inv.Region, inv.AccountID, aws.StringValue(c.Create.FunctionName))
|
||||
}
|
||||
case "GetFunction", "GetFunctionConfiguration", "GetFunctionCodeSigningConfig":
|
||||
case "GetFunction", "GetFunctionConfiguration", "GetFunctionCodeSigningConfig", "ListVersionsByFunction":
|
||||
if name, _, err := resolveFunctionName(inv.Region, inv.AccountID, params[0]); err == nil {
|
||||
resource = functionARN(inv.Region, inv.AccountID, name)
|
||||
}
|
||||
@@ -134,7 +134,8 @@ func (d Deps) serve(w http.ResponseWriter, inv dispatch.Invocation) error {
|
||||
}
|
||||
}
|
||||
// Only GetFunction presigns code; the other reads are metadata only.
|
||||
needsCodes := action != "GetFunctionConfiguration" && action != "GetFunctionCodeSigningConfig"
|
||||
needsCodes := action != "GetFunctionConfiguration" && action != "GetFunctionCodeSigningConfig" &&
|
||||
action != "ListVersionsByFunction"
|
||||
if d.Meta == nil || (d.Codes == nil && needsCodes) {
|
||||
return errors.New(awserrors.ErrorServiceException)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@ var routes = []route{
|
||||
Handler: func(ctx context.Context, d Deps, c *call) (reply, error) {
|
||||
return d.getFunctionConfiguration(ctx, c)
|
||||
}},
|
||||
{Method: "GET", Pattern: "/2015-03-31/functions/{FunctionName}/versions", Action: "ListVersionsByFunction",
|
||||
Handler: func(ctx context.Context, d Deps, c *call) (reply, error) {
|
||||
return d.listVersionsByFunction(ctx, c)
|
||||
}},
|
||||
{Method: "GET", Pattern: "/2020-06-30/functions/{FunctionName}/code-signing-config", Action: "GetFunctionCodeSigningConfig",
|
||||
Handler: func(ctx context.Context, d Deps, c *call) (reply, error) {
|
||||
return d.getFunctionCodeSigningConfig(ctx, c)
|
||||
|
||||
@@ -138,6 +138,13 @@ func TestLambda_SignedCreateAndGetDownloadsTheStoredCode(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, signing.CodeSigningConfigArn)
|
||||
require.Nil(t, signing.FunctionName)
|
||||
versions, err := cli.ListVersionsByFunction(&lambda.ListVersionsByFunctionInput{FunctionName: aws.String("e2e-fn"), MaxItems: aws.Int64(10000)})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, versions.Versions, 1)
|
||||
require.Equal(t, aws.StringValue(got.Configuration.FunctionArn)+":$LATEST", aws.StringValue(versions.Versions[0].FunctionArn))
|
||||
require.Equal(t, "$LATEST", aws.StringValue(versions.Versions[0].Version))
|
||||
require.Nil(t, versions.NextMarker)
|
||||
|
||||
_, err = cli.GetFunctionCodeSigningConfig(&lambda.GetFunctionCodeSigningConfigInput{FunctionName: aws.String("e2e-missing")})
|
||||
var aerr awserr.Error
|
||||
require.ErrorAs(t, err, &aerr)
|
||||
|
||||
Reference in New Issue
Block a user