Code Monkey home page Code Monkey logo

helix-universal's Introduction

Helix Universal

Serverless adapters for the universal runtime.

Status

codecov CircleCI GitHub license GitHub issues LGTM Code Quality Grade: JavaScript semantic-release

Installation

$ npm install @adobe/helix-universal

Development

Build

$ npm install

Test

$ npm test

Lint

$ npm run lint

helix-universal's People

Contributors

alexkli avatar dominique-pfister avatar justinedelson avatar maxakuru avatar maximilianvoss avatar renovate-bot avatar renovate[bot] avatar semantic-release-bot avatar stefan-guggisberg avatar trieloff avatar tripodsan avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

helix-universal's Issues

Catch SyntaxError and return 400 as well

In:

if (e instanceof TypeError && e.code === 'ERR_INVALID_CHAR') {
// eslint-disable-next-line no-console
console.error('invalid request header', e.message);
return {
statusCode: 400,
headers: {
'content-type': 'text/plain',
'x-invocation-id': context.awsRequestId,
},
body: e.message,
};
}

we could additionally check whether e is of type SyntaxError, which would indicate that JSON parsing of the request body failed in body-data-wrapper.js (see adobe/helix-shared#593)

Potential downside: some genuine parsing problem elsewhere in the code might be treated as bad request.

it must be possible to add custom params to presignURL

for example ContentType is included in the signature computation:

<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message><AWSAccessKeyId>AKIARXE2QZFCSXL7IA4R</AWSAccessKeyId><StringToSign>PUT

application/json
1622618896
/h3d4b1d4ea6d84b0229bce7cf6806b0bb3470489ab8205a13f75cfe518fa7/live/data-embed-unit-tests/data-embed-no-helix.json</StringToSign><SignatureProvided>01F7yGPBahNDioaR5A/AIfWn6Q0=<

suggest to include blob params like this:

class AWSStorage extends Storage {
  static async presignURL(bucket, path, blobParams = {}, method = 'GET', expires = 60) {
    if (!AWS) {
      // eslint-disable-next-line global-require, import/no-extraneous-dependencies
      AWS = require('aws-sdk');

      AWS.config.update({
        region: process.env.AWS_REGION || 'us-east-1',
        logger: console,
      });
    }

    const s3 = new AWS.S3();

    const operation = method === 'PUT' ? 'putObject' : 'getObject';
    const params = {
      Bucket: bucket,
      Key: path.startsWith('/') ? path.substring(1) : path,
      Expires: expires,
      ...blobParams,
    };

    return s3.getSignedUrl(operation, params);
  }
}

Support for functions triggered by Amazon SQS

In AWS, one can create a trigger that invokes a Lambda function whenever a queue in Amazon SQS receives input. Technically, the function is invoked with a Records array-type property in its event argument, containing the messages available in the SQS queue.

In order to support this kind of invocation for a function that can also be called manually with parameters, it would be ideal if the AWS adapter would inspect that argument and fill the provided information into the request body.

Minimal Storage Abstraction

We have a couple of actions now that need access to S3 or equivalent storage locations. Instead of creating a full-blown storage abstraction, what about starting with a minimal set of:

  • context.storage.presignURL(bucketname: string, path: string, method?: string = 'GET', expires?: number = 60): generates a pre-signed URL for the specified bucket and path. This URL can then be used in a helix-fetch request to upload or download from storage.

For Adobe I/O Runtime we may be able to use https://github.com/adobe/aio-lib-files which has support for pre-signed URLs, but that requires the presence of @adobe/aio-lib-files in the container image.

For all others, there are libraries that can even take the credentials from the container, so we should be covered.

Passing a parameter value as JSON to an OpenWhisk action makes it appear as [object Object]

In Working with parameters it is shown that one can invoke an OpenWhisk action and pass JSON as parameter directly:

wsk action invoke --result hello -p person '{"name": "Dorothy", "place": "Kansas"}'

Passing this to a universal action ends up with a wrong parameter value:

person: [object Object]

Reason is:

Object.entries(rest).forEach(([key, value]) => {
if (key.match(/^[A-Z0-9_]+$/)) {
env[key] = value;
} else {
url.searchParams.append(key, value);
}
});

where parameters are added as search parameters to the request URL regardless of their type.

A concrete example where this is used is in helix-index-files, where an observation message is passed via trigger as a JSON object to the action.

Merge context type declarations for wrappers

Right now using universal in Typescript means you need to manually set main()'s parameter types. This is expected, not much we can do about it, but the pain point is when you're using wrappers that extend the context.

This works great:

import { UniversalContext, Request } from '@adobe/helix-universal';

export function main (req: Request, ctx: UniversalContext) {
  ...
}

This doesn't:

import { UniversalContext, Request } from '@adobe/helix-universal';
import wrap from '@adobe/helix-shared-wrap';
import { logger } from '@adobe/helix-universal-logger';

function _main(req: Request, ctx: UniversalContext) {
  ctx.log.info("woo"); // Type error, ctx.log doesn't exist
}

export const main = wrap(_main).with(logger);

Ignoring the last line which has its own type errors, the context doesn't "know" about the logger wrapper. Workaround could be to import each context and make a custom interface merging them, but that would be repeated every time it's used.

The context APIs seem pretty useful to expose, so I'm proposing a namespace that would be exposed, allowing wrapper functions to extend the context as they need.

Assuming @adobe/helix-universal-logger adds an extension to the namespace like this:

// index.d.ts
declare module '@adobe/helix-universal' {
  namespace HelixUniversal {
    export interface UniversalContext {
      info: (...msgs: any[]) => void;
      // ...
    }
  }
}

TS clients could then use:

import { HelixUniversal, Request } from '@adobe/helix-universal';
import wrap from '@adobe/helix-shared-wrap';
import { logger } from '@adobe/helix-universal-logger';

async function _main(req: Request, ctx: HelixUniversal.UniversalContext) {
  ctx.log.info("woo"); // ๐Ÿ‘ 
}

export const main = wrap(_main).with(logger);

wdyt @tripodsan @trieloff ?

[AWS] Allow functions to force non-HTTP mode

Is your feature request related to a problem? Please describe.

The way that helix-universal detects that the response should be returned as-is is based on the absence of event.requestContext (see

const nonHttp = (!event.requestContext);
). However, this does not work properly when a function is used as an API Gateway Authorizer.

When used as an Authorizer, event.requestContext is set. But the response needs to be returned in the raw form provided by helix-universal when nonHttp is true.

Describe the solution you'd like

Allow functions to explicitly ask for non-HTTP mode to be used in the response by setting a header, e.g. force-non-http: true

Describe alternatives you've considered

Not using helix-universal for this use case but that would create other problems.

Additional context
Add any other context or screenshots about the feature request here.

Invoking AWS function without alias causes TypeError

When invoking the latest version of an AWS function, the functionAlias in the ARN is undefined:

// parse ARN
// arn:partition:service:region:account-id:resource-type:resource-id
// eg: arn:aws:lambda:us-east-1:118435662149:function:dump:4_2_1
const [/* 'arn' */, /* 'aws' */, /* 'lambda' */,
region,
accountId, /* 'function' */,
functionName,
functionAlias,
] = context.invokedFunctionArn.split(':');

Further down, a replace on that functionAlias is attempted:

func: {
name,
package: packageName,
version: functionAlias.replace(/_/g, '.'),
fqn: context.invokedFunctionArn,
app: event.requestContext.apiId,
},

which causes a TypeError.

TypeError when creating GET/HEAD Request with non-empty body

When preparing the request:

const request = new Request(`https://${host}${path}${queryString ? '?' : ''}${queryString}`, {
method: event.requestContext?.http?.method,
headers,
body: event.isBase64Encoded ? Buffer.from(event.body, 'base64') : event.body,
});

the code should check whether the HTTP method supports non-empty body at all, and reject it with a 400 otherwise - currently, it reports a 500.

process.env are set too late

the process.env are set before invoking the function, which is a problem because the main module is imported before that. so any global checks that depend on the process.env will fails.

eg:

// use HTTP1 if we run serverless. otherwise the open http/2 connections might hang the process.
process.env.HELIX_FETCH_FORCE_HTTP1 = process.env.HELIX_UNIVERSAL_RUNTIME;

Suggestion

  • require the main after the process.env is set.

AWS: handle rate limits errors when fetching secrets

Description
A rate limit error while fetching the parameters should either:

  • abort the function with a 500
  • or wait and retry later
2021-04-14T09:01:36.298Z	7a3a96f0-350b-435a-8f04-5923ebfe4972	ERROR	unable to get parameters ThrottlingException: Rate exceeded
    at Request.extractError (/var/runtime/node_modules/aws-sdk/lib/protocol/json.js:52:27)
    at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:106:20)
    at Request.emit (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:78:10)
    at Request.emit (/var/runtime/node_modules/aws-sdk/lib/request.js:688:14)
    at Request.transition (/var/runtime/node_modules/aws-sdk/lib/request.js:22:10)
    at AcceptorStateMachine.runTo (/var/runtime/node_modules/aws-sdk/lib/state_machine.js:14:12)
    at /var/runtime/node_modules/aws-sdk/lib/state_machine.js:26:10
    at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:38:9)
    at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:690:12)
    at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:116:18) {
  code: 'ThrottlingException',
  time: 2021-04-14T09:01:36.297Z,
  requestId: '0014473a-2458-445e-a5f7-3ddca2dff603',
  statusCode: 400,
  retryable: true
}

Large Response Support

With the wrapper and universal gateway in place, we can now support large responses in the following way:

  1. The wrapper detects that the response size exceeds a certain limit
  2. The wrapper stores the response in an S3 bucket or equivalent storage โ€“ ideally in a manner that does not require additional dependencies that need to be packaged or credentials that need to be configured
  3. The wrapper returns a 307 status with Location pointing to the stored response body
  4. The gateway intercepts the 307 status, RESTARTs and delivers the body from the Location
  5. The wrapper or some asynchrone job cleans up the response body storage

The response cleanup could also be done by the wrapper in the next request.

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Awaiting Schedule

These updates are awaiting their schedule. Click on a checkbox to get an update now.

  • chore(deps): update external fixes (@google-cloud/storage, aws-sdk)

Detected dependencies

github-actions
.github/workflows/main.yaml
  • actions/checkout v4
  • actions/setup-node v4
  • codecov/codecov-action v4
  • actions/checkout v4
  • actions/setup-node v4
.github/workflows/semver-check.yaml
npm
package.json
  • @adobe/fetch 4.1.2
  • aws4 1.12.0
  • @adobe/eslint-config-helix 2.0.6
  • @google-cloud/secret-manager 5.5.0
  • @google-cloud/storage 7.10.2
  • @semantic-release/changelog 6.0.3
  • @semantic-release/git 10.0.1
  • aws-sdk 2.1613.0
  • c8 9.1.0
  • eslint 8.57.0
  • esmock 2.6.5
  • husky 9.0.11
  • junit-report-builder 3.2.1
  • lint-staged 15.2.2
  • mocha 10.4.0
  • mocha-multi-reporters ^1.5.1
  • nock 13.5.4
  • semantic-release 23.0.8

  • Check this box to trigger a request for Renovate to run again on this repository

remove support for epsagon in favour of a pluggable adapter mechanism

Description
in order to reduce dependencies, it would be better to support some kind of pluggable mechanism to install handers.

eg, using a custom adapter:

import { adapter, awsSecrets } from '@adobe/helix-universal';
import { awsEpsagon } from `@adobe/helix-epsagon`

export const lambda = adapter.aws.with(awsEpsagon).with(awsSecrets); 
export const main = adapter.openwhisk.raw;
export const google = adapter.google.raw;

adapter: context.env should not contain process.env

having the process.env variables mixed into the context.env object is kind of redundant and might be confusing.
context.env should only contain the information that is specific to the deployed environment.
an action can still use process.env explicitely.

Support for function invocations not made via API Gateway

When I try invoking my hedy deployed function in the AWS console, I get a TypeError indicating that some information is missing to build the context passed to the universal main, e.g. the event.requestContext:

const request = new Request(`https://${event.requestContext.domainName}${event.rawPath}${event.rawQueryString ? '?' : ''}${event.rawQueryString}`, {

I understand that it is not possible to create a resolver without a requestContext, but in order to easily build an AWS trigger for a function that does not require the resolver, it would be ideal if the missing information is not provided, and using a resolver would fail in that particular situation.

On the other hand, I can also try to create an AWS trigger and feed the expected JSON into the event payload.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.