CloudSnorkel / cdk-github-runners

CDK constructs for self-hosted GitHub Actions runners
https://constructs.dev/packages/@cloudsnorkel/cdk-github-runners/
Apache License 2.0
255 stars 37 forks source link

feat: Allow custom `LambdaAccess` implmentations #328

Closed kichik closed 1 year ago

kichik commented 1 year ago

Expose LambdaAccess.bind() so users can implement their own access to webhooks. This can be used to setup a custom domain, for example.

class CustomDomainLambdaAccess extends LambdaAccess {
  bind(scope: Construct, id: string, lambdaFunction: lambda.Function): string {
    const api = new apigateway.LambdaRestApi(scope, id, {
      handler: lambdaFunction,
      proxy: true,
      cloudWatchRole: false,
      domainName: {
        domainName: 'webhook.my-custom-domain.com',
        certificate: acm.Certificate.fromCertificateArn(scope, 'cert', 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012'),
      },
      policy: PolicyDocument.fromJson({
        Version: '2012-10-17',
        Statement: [
          {
            Effect: 'Allow',
            Principal: '*',
            Action: 'execute-api:Invoke',
            Resource: 'execute-api:/*/*/*',
            Condition: {
              IpAddress: {
                'aws:SourceIp': ['1.2.3.4/32'],
              },
            },
          },
        ],
      }),
    });

    new route53.CnameRecord(scope, 'cname', {
      domainName: `${api.restApiId}.execute-api.us-east-1.amazonaws.com`,
      recordName: 'webhook',
      zone: route53.HostedZone.fromHostedZoneId(scope, 'hosted-zone', 'Z1234567890'),
    });

    return 'https://webhook.my-custom-domain.com/prod';
  }
}

new GitHubRunners(this, 'runners', {
  webhookAccess: new CustomDomainLambdaAccess(),
});

Closes #324

quad commented 1 year ago

We actually wrote the following in-house:

class IAMAccess extends LambdaAccess {
    constructor(readonly grantee: IGrantable) {
        super();
    }

    _bind(_construct: Construct, _id: string, lambdaFunction: Function): string {
        let url = lambdaFunction.addFunctionUrl({
            authType: FunctionUrlAuthType.AWS_IAM,
        });
        url.grantInvokeUrl(this.grantee);

        return url.url;
    }
}

new GitHubRunners(this, "runners", {
    setupAccess: new IAMAccess(role_admin),
    statusAccess: new IAMAccess(role_read_only),
    ...
});
kichik commented 1 year ago

Since _bind() is internal and not meant to be used, that will not work in any other language. This Python code will not work as expected:

import aws_cdk as cdk
from cloudsnorkel.cdk_github_runners import GitHubRunners, LambdaAccess
import os

class InternalBind(LambdaAccess):
    def __init__(self):
        super().__init__()

    def _bind(self, scope, id_, func):
        raise RuntimeError('never going to get called')

app = cdk.App()
stack = cdk.Stack(app, 'stack', env={"account": os.environ['CDK_DEFAULT_ACCOUNT'], "region": os.environ['CDK_DEFAULT_REGION']})
GitHubRunners(stack, 'runners', webhook_access=InternalBind())

app.synth()

It fails with:

RuntimeError: access._bind is not a function
pharindoko commented 6 months ago

@kichik Ok I got this working now with my private api gateway + custom domain solution.

setup url: runner.example.com/?token=xxxxxx webhook url: runner.example.com/webhook

When I attach an existing app it works perfectly. When I create a new app and it redirects back to route /complete-new-app, it fails.

My issue is this line that appends a stage to the github redirect url and in my case to the custom domain. I receive a message not found in the browser.

https://github.com/CloudSnorkel/cdk-github-runners/blob/4c4e2541b9e0b43ffed6e205e90c5f7fa7c76d44/src/setup.lambda.ts#L36

in setup.lambda.ts https://github.com/CloudSnorkel/cdk-github-runners/blob/4c4e2541b9e0b43ffed6e205e90c5f7fa7c76d44/src/setup.lambda.ts#L168

Instead of this value /complete-new-app

e.g. this value is set in path /dev/complete-new-app

Do you have any idea how we could ignore custom domains ? One approach would be to add a condition to detect if amazon.com is included in setup.lambda.ts#L36 Another approach would be to take only the last path element in setup.lambda.ts#L168

kichik commented 6 months ago

Do you have any idea how we could ignore custom domains ?

We might be able to find another parameter in both types of context to give us the full path including the stage so it works for both API Gateway and Lambda URL.

pharindoko commented 6 months ago

domainname + path should work as combination...

https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format

const setupBaseUrl = `https://${event.requestContext.domainName}${event.requestContext.path}`;
kichik commented 6 months ago

We would need to make it works with Lambda URL, public API Gateway, private API Gateway, and API Gateway with domain.

pharindoko commented 6 months ago

We would need to make it works with Lambda URL, public API Gateway, private API Gateway, and API Gateway with domain.

Hmm I assume that the event object ApiGatewayEvent is the same at least for the public/private ApiGateway with or without custom domain. Don't know how it is for the Lambda Function url... guess I need to test it.

kichik commented 6 months ago

It's similar but slightly different. It's basically this line.

https://github.com/CloudSnorkel/cdk-github-runners/blob/4c4e2541b9e0b43ffed6e205e90c5f7fa7c76d44/src/setup.lambda.ts#L8

pharindoko commented 6 months ago

It's similar but slightly different. It's basically this line.

https://github.com/CloudSnorkel/cdk-github-runners/blob/4c4e2541b9e0b43ffed6e205e90c5f7fa7c76d44/src/setup.lambda.ts#L8

Ah seems like I ignored this line, sorry. I'll try to figure this out next week.