2

in my scenario, I would like to trigger an Google Cloud Function based on HTTP endpoint during a Google Cloud Build. The HTTP request is done using a step with a python:3.7-slim container.

Based on this and this examples from the documentation, I wanted to use the following code:

REGION = 'us-central1'
PROJECT_ID = 'name-of-project'
RECEIVING_FUNCTION = 'my-cloud-function'

function_url = f'https://{REGION}-{PROJECT_ID}.cloudfunctions.net/{RECEIVING_FUNCTION}'

metadata_server_url = 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience='
token_full_url = metadata_server_url + function_url
token_headers = {'Metadata-Flavor': 'Google'}

token_response = requests.get(token_full_url, headers=token_headers)
jwt = token_response.text
print(jwt)

r = requests.post(url=function_url, headers=function_headers, json=payload)

Surprisingly, the code fails because jwt is Not Found (according to the print statement). I already tested the code and IAM settings by hard coding a valid identity token and also tested the exact same fetching mechanism on a test VM inside the same project. The problem seems to be that the meta data fetching some is not working inside cloud build.

Am I missing something? Thank you for any help!

winwin
  • 384
  • 6
  • 20
  • 1) Where are you running this code? 2) If the error is actually `Not Found` then the URL is incorrect. What is the value of `r.status_code`? – John Hanley Oct 07 '20 at 22:07
  • Regarding 1) the Code runs inside a Cloud Build Pipeline in a python:3.7-slim container. Regarding 2) it is indeed a 404 Not Found error, that is pretty strange, since the exact same code running in a VM (same project) is working properly. – winwin Oct 08 '20 at 07:22
  • 2
    Agree, I got the same result with a curl in Cloud Build. The same curl works in Cloud Shell and answer Not Found on Cloud Build. I guess it's a limitation... – guillaume blaquiere Oct 08 '20 at 07:32
  • According to this [github Issue](https://github.com/GoogleCloudPlatform/cloud-builders/issues/80) more people having the problem. Unfortunately, according to the following [issue](https://issuetracker.google.com/issues/139011732) it is an intended behavior... If this is true, any ideas how to call a Cloud Function via HTTP in Cloud Build? – winwin Oct 08 '20 at 08:03
  • Hi @winwin, if my answer helped, you can upvote it as well. Thank you! – Joss Baron Oct 15 '20 at 13:34

2 Answers2

7

The solution is to use a new IAM api to generate an ID_TOKEN, on a service account with an access token, if the requester (this one who generate the access token) has the role Service Account Token Creator on the service account (or widely on the project).

This first example use direct API calls

 - name: gcr.io/cloud-builders/gcloud
   entrypoint: "bash"
   args:
    - "-c"
    - |
        curl -X POST -H "content-type: application/json" \
        -H "Authorization: Bearer $(gcloud auth print-access-token)" \
        -d '{"audience": "YOUR AUDIENCE"}' \
         "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/YOUR SERVICE ACCOUNT:generateIdToken"
        # Use Cloud Build Service Account
        # service_account_email=$(gcloud config get-value account) 

And here the Python code version

- name: python:3.7
          entrypoint: "bash"
          args:
            - "-c"
            - |
                    pip3 install google-auth requests
                    python3 extract-token.py

And extract-token.py content the following code

REGION = 'us-central1'
PROJECT_ID = 'name-of-project'
RECEIVING_FUNCTION = 'my-cloud-function'
function_url = f'https://{REGION}-{PROJECT_ID}.cloudfunctions.net/{RECEIVING_FUNCTION}'

import google.auth
credentials, project_id = google.auth.default(scopes='https://www.googleapis.com/auth/cloud-platform')

# To use the Cloud Build service account email
service_account_email = credentials.service_account_email
#service_account_email = "YOUR OWN SERVICE ACCOUNT"

metadata_server_url = f'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{service_account_email}:generateIdToken'
token_headers = {'content-type': 'application/json'}

from google.auth.transport.requests import AuthorizedSession
authed_session = AuthorizedSession(credentials)
import json
body = json.dumps({'audience': function_url})

token_response = authed_session.request('POST',metadata_server_url, data=body, headers=token_headers)
jwt = token_response.json()
print(jwt['token'])

Don't hesitate if you need more details.

I think I will write an article on this on Medium, if you want I name you, let me know

guillaume blaquiere
  • 66,369
  • 2
  • 47
  • 76
  • Hi guillaume blaquiere, thanks for the answer, sounds like a good solution. In the meantime, I already implemented the pipeline via Pub/Sub as suggested by Joss Baron. Therefore I can not test you solution immediately, but will try it out later. Writing a medium post is a good idea, I think this is a real blindspot at the moment. Thanks for the offer for dropping my name, feel free to do so: @Fabian.Max. – winwin Oct 09 '20 at 08:24
  • @guillaume blaquiere, thanks a lot for this answer. literally scavenged the docs & internet for days to achieve this!! – jefe23984 Jul 06 '22 at 08:35
  • Also, I had to set scopes to `['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/iam']` to get access, Otherwise it resulted with 401 status code. – jefe23984 Jul 06 '22 at 08:37
  • Thanks for the answer. I keep getting a 403 code with both the curl and python version. I made sure the cloud build SA has the service account token creator role. Any ideas? – tsabsch Aug 17 '22 at 06:30
1

The best here is to create a Feature Request (FR) in the Public Issue Tracker. There is a difference between filing an issue and a FR. The FR gives visibility to the Engineering team of the real needs; according to the number of users which are being affected by that, they prioritize the development of them. I suggest also to create a guthub repo so they can easily replicate it and make reference to the issues aforementioned.

On the other hand as a workaround, you can create a topic in Pub/Sub to receive build notifications:

gcloud pubsub topics create cloud-builds

Each time you submit a build, a message will be pushed to the topic, then you can create a PubSub Cloud Function and call your HTTP CF from there.

I used this example from github, mentioned in the docs Authenticating Function to function

const {get} = require('axios');

// TODO(developer): set these values
const REGION = 'us-central1';
const PROJECT_ID = 'YOUR PROJECTID';
const RECEIVING_FUNCTION = 'FUNCTION TO TRIGGER';

// Constants for setting up metadata server request
// See https://cloud.google.com/compute/docs/instances/verifying-instance-identity#request_signature
const functionURL = `https://${REGION}-${PROJECT_ID}.cloudfunctions.net/${RECEIVING_FUNCTION}`;
const metadataServerURL =
  'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=';
const tokenUrl = metadataServerURL + functionURL;

exports.helloPubSub = async (event, context) => {
// Fetch the token
  const message = event.data
    ? Buffer.from(event.data, 'base64').toString()
    : 'Hello, World';
    
  const tokenResponse = await get(tokenUrl, {
    headers: {
      'Metadata-Flavor': 'Google',
    },
  });
  const token = tokenResponse.data;

  // Provide the token in the request to the receiving function
  try {
    const functionResponse = await get(functionURL, {
      headers: {Authorization: `bearer ${token}`},
    });
    console.log(message);
  } catch (err) {
    console.error(err);
  }
};

Finally, when the ClouBuild submits, your PubSub CF will be triggered and you can call your CF inside it.

Joss Baron
  • 1,441
  • 10
  • 13