简体   繁体   中英

Pulumi Outputs - create an external config file

I'm working with Cloudrun in GCP and would like to summarise the created APIs with API Gateway. This requires a Swagger/OpenAPI v2 document to be created containing the google generated URLs for the Cloudrun Services.

How can I get these from the Pulumi Output into the swagger file to upload as the API Config?

I was initially hoping to use Moustache. But now I'm trying to work with raw JSON and still no success. I've tried the apply() and interpolate but always [object Object] never the url of the deployed service.

"x-google-backend": {
  "address": {
    "[object Object]":null
  },
  "path_translation":"APPEND_PATH_TO_ADDRESS"
}

What did I miss?

Is it possible to get this working directly with moustache, without the bother of working with the deserialized Swagger?

Code to recreate:

A Basic Swagger File my-spec.yaml

swagger: '2.0'

info:
  title: Demo for URL Replacement
  version: 0.0.1

schemes:
  - https

paths:
  /users:
      post:
        summary: Registers a new user to the service
        operationId: createUser
        x-google-backend:
          address: {{{apiUsersUrl}}} # Output URL from CloudRun, in moustache format
          path_translation: APPEND_PATH_TO_ADDRESS
        responses:
          201:
            description: Success
        security:
        - api_key: []

securityDefinitions:
  api_key:
    type: apiKey
    name: x-api-key
    in: header

My file to write the new API Config.

import * as pulumi from '@pulumi/pulumi';
import * as yaml from 'js-yaml';
import * as fs from 'fs';
import { Spec } from '../types/Swagger';

export function buildApiSpecJS(usersUrl: pulumi.Output<string>) {
  const specFile = fs.readFileSync(`${__dirname}/my-spec.yaml`, 'utf8');
  const specTemplate= yaml.load(specFile) as Spec;

  usersUrl.apply((url) => {
    let pathName: keyof typeof specTemplate.paths;
    // eslint-disable-next-line guard-for-in
    for (pathName in specTemplate.paths) {
      const path = specTemplate.paths[pathName];
      let operation: keyof typeof path;
      for (operation in path) {
        if (path[operation] !== '$ref') {
          const address: string = path[operation]['x-google-backend']?.address;
          if (address === '{{{apiUsersUrl}}}') {
            path[operation]['x-google-backend'].address = pulumi.interpolate`${url}`;
          }
        }
      }
    }

    fs.writeFileSync(`${__dirname}/testfile.json`, JSON.stringify(specTemplate));
  });
}

The Spec from types is copied and amended from DefinitelyTyped Definition

// Changes:
//  - Operation is edited to add the Google Backend

export interface GoogleBackend {
  address: string;
  jwt_audience?: string;
  disable_auth?: boolean;
  path_translation?: string; // [ APPEND_PATH_TO_ADDRESS | CONSTANT_ADDRESS ]
  deadline?: string;
  protocol?: string; // [ http/1.1 | h2 ]
};

//
///// .... Copied Types ....
//

// ----------------------------- Operation -----------------------------------
export interface Operation {
  responses: { [responseName: string]: Response | Reference };
  summary?: string | undefined;
  description?: string | undefined;
  externalDocs?: ExternalDocs | undefined;
  operationId?: string | undefined;
  produces?: string[] | undefined;
  consumes?: string[] | undefined;
  parameters?: Array<Parameter | Reference> | undefined;
  schemes?: string[] | undefined;
  deprecated?: boolean | undefined;
  security?: Array<{ [securityDefinitionName: string]: string[] }> | undefined;
  tags?: string[] | undefined;
  'x-google-backend': GoogleBackend | undefined;  // Added
}

//
///// Copied Types continue....
//

The solution I landed on was DomainMapping and DNS. I used DomainMapping to create a predictable name in each environment for the Cloud Run service. eg api-users.dev.example.com .

Then as I know that name ahead of time, I can use it for the Moustache replacements in the API Spec.

I solved this with the following code - I have a master template file that's copied to a stack specific OpenAPI .yaml file.

import * as pulumi from '@pulumi/pulumi';
import * as gcp from '@pulumi/gcp';
import * as fs from 'fs';
import { helloWorldUrl } from '../cloud-run/hello-world';
import { Output } from '@pulumi/pulumi/output';
import * as Mustache from 'mustache';


const config = new pulumi.Config();
const region = config.get('region');

const google_beta = new gcp.Provider('provider', {
    project: 'temp-22223'
});

// overwrite file

(async () => {
    try {
        helloWorldUrl.apply(url => {

            let gatewayOpenAPI = fs.readFileSync('./api-gateway/open-api/gateway.yaml').toString();
            gatewayOpenAPI = Mustache.render(gatewayOpenAPI, { helloWorldUrl: url });    
            fs.writeFileSync(`./api-gateway/open-api/gateway-${pulumi.getStack()}.yaml`, gatewayOpenAPI);
        });
        
    } catch (e) {
        console.error(e);
    }

})();

// always create new config
const apiCfgApi = new gcp.apigateway.Api('api', { apiId: `api-cfg${new Date().getTime()}` },
    {
        provider: google_beta
    });

const gatewayOpenAPI = fs.readFileSync(`./api-gateway/open-api/gateway-${pulumi.getStack()}.yaml`).toString('base64');

const apiCfgApiConfig = new gcp.apigateway.ApiConfig('apiCfg', {
    api: apiCfgApi.apiId,
    apiConfigId: 'cfg',
    project: 'temp-22223',
    openapiDocuments: [{
        document: {
            path: 'gateway.yaml',
            contents: gatewayOpenAPI
        },
    }],
},
    {
        provider: google_beta,
    });

const apiGwGateway = new gcp.apigateway.Gateway("apiGw", {
    apiConfig: apiCfgApiConfig.id,
    gatewayId: "api-gw",
    region: region
},
    {
        provider: google_beta,
    });

export let gatewayUrl = apiGwGateway.defaultHostname;

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM