Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Composer: Move 'triggering DAGs' sample to GitHub. #716

Merged
merged 3 commits into from
Aug 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions functions/composer-storage-trigger/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// [START composer_trigger]
'use strict';

const fetch = require('node-fetch');
const FormData = require('form-data');

/**
* Triggered from a message on a Cloud Storage bucket.
*
* IAP authorization based on:
* https://stackoverflow.com/questions/45787676/how-to-authenticate-google-cloud-functions-for-access-to-secure-app-engine-endpo
* and
* https://cloud.google.com/iap/docs/authentication-howto
*
* @param {!Object} event The Cloud Functions event.
* @param {!Function} callback The callback function.
*/
exports.triggerDag = function triggerDag (event, callback) {
// Fill in your Composer environment information here.

// The project that holds your function
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit - typically:

  1. we only show the contents of a function within a sample (i.e. we omit the function signature)
  2. we pass in any required values as parameters
  3. we include definitions of those values in the body of the function, but they are commented out

Apply this comment throughout your PR (if you choose to).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even for a cloud functions sample? This sample is intended to be copy-pasted into the cloud functions web UI, so I believe the function signature is necessary.

const PROJECT_ID = 'your-project-id';
// Navigate to your webserver's login page and get this from the URL
const CLIENT_ID = 'your-iap-client-id';
// This should be part of your webserver's URL:
// {tenant-project-id}.appspot.com
const WEBSERVER_ID = 'your-tenant-project-id';
// The name of the DAG you wish to trigger
const DAG_NAME = 'composer_sample_trigger_response_dag';

// Other constants
const WEBSERVER_URL = `https://${WEBSERVER_ID}.appspot.com/api/experimental/dags/${DAG_NAME}/dag_runs`;
const USER_AGENT = 'gcf-event-trigger';
const BODY = {'conf': JSON.stringify(event.data)};

// Make the request
authorizeIap(CLIENT_ID, PROJECT_ID, USER_AGENT)
.then(function iapAuthorizationCallback (iap) {
makeIapPostRequest(WEBSERVER_URL, BODY, iap.idToken, USER_AGENT, iap.jwt);
})
.then(_ => callback(null))
.catch(callback);
};

/**
* @param {string} clientId The client id associated with the Composer webserver application.
* @param {string} projectId The id for the project containing the Cloud Function.
* @param {string} userAgent The user agent string which will be provided with the webserver request.
*/
function authorizeIap (clientId, projectId, userAgent) {
const SERVICE_ACCOUNT = `${projectId}@appspot.gserviceaccount.com`;
const JWT_HEADER = Buffer.from(JSON.stringify({alg: 'RS256', typ: 'JWT'}))
.toString('base64');

var jwt = '';
var jwtClaimset = '';

// Obtain an Oauth2 access token for the appspot service account
return fetch(
`http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/${SERVICE_ACCOUNT}/token`,
{
headers: {'User-Agent': userAgent, 'Metadata-Flavor': 'Google'}
})
.then(res => res.json())
.then(function obtainAccessTokenCallback (tokenResponse) {
var accessToken = tokenResponse.access_token;
var iat = Math.floor(new Date().getTime() / 1000);
var claims = {
iss: SERVICE_ACCOUNT,
aud: 'https://www.googleapis.com/oauth2/v4/token',
iat: iat,
exp: iat + 60,
target_audience: clientId
};
jwtClaimset = Buffer.from(JSON.stringify(claims)).toString('base64');
var toSign = [JWT_HEADER, jwtClaimset].join('.');

return fetch(
`https://iam.googleapis.com/v1/projects/${projectId}/serviceAccounts/${SERVICE_ACCOUNT}:signBlob`,
{
method: 'POST',
body: JSON.stringify({'bytesToSign': Buffer.from(toSign).toString('base64')}),
headers: {
'User-Agent': userAgent,
'Authorization': `Bearer ${accessToken}`
}
});
})
.then(res => res.json())
.then(function signJsonClaimCallback (body) {
// Request service account signature on header and claimset
var jwtSignature = body.signature;
jwt = [JWT_HEADER, jwtClaimset, jwtSignature].join('.');
var form = new FormData();
form.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
form.append('assertion', jwt);
return fetch(
'https://www.googleapis.com/oauth2/v4/token', {
method: 'POST',
body: form
});
})
.then(res => res.json())
.then(function returnJwt (body) {
return {
jwt: jwt,
idToken: body.id_token
};
});
}

/**
* @param {string} url The url that the post request targets.
* @param {string} body The body of the post request.
* @param {string} idToken Bearer token used to authorize the iap request.
* @param {string} userAgent The user agent to identify the requester.
* @param {string} jwt A Json web token used to authenticate the request.
*/
function makeIapPostRequest (url, body, idToken, userAgent, jwt) {
var form = new FormData();
form.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
form.append('assertion', jwt);

return fetch(
url, {
method: 'POST',
body: form
})
.then(function makeIapPostRequestCallback () {
return fetch(url, {
method: 'POST',
headers: {
'User-Agent': userAgent,
'Authorization': `Bearer ${idToken}`
},
body: JSON.stringify(body)
});
});
}
// [END composer_trigger]
24 changes: 24 additions & 0 deletions functions/composer-storage-trigger/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "nodejs-docs-samples-functions-composer-storage-trigger",
"version": "0.0.1",
"dependencies": {
"form-data": "^2.3.2",
"node-fetch": "^2.2.0"
},
"engines": {
"node": ">=4.3.2"
},
"private": true,
"license": "Apache-2.0",
"author": "Google Inc.",
"repository": {
"type": "git",
"url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git"
},
"devDependencies": {
"@google-cloud/nodejs-repo-tools": "^2.2.5"
},
"scripts": {
"lint": "repo-tools lint"
}
}