azureinfrastructure-as-codepulumipulumi-azurepulumi-typescript

Pulumi Dynamic Provider with azure-devops-node-api Fails Due to Serialization Error


I am writing a Pulumi dynamic resource provider to control Azure DevOps project pipeline settings using the azure-devops-node-api client. Here's my provider code:

import * as pulumi from '@pulumi/pulumi';
import * as azdev from 'azure-devops-node-api';

export interface ProjectPipelineSettingsResourceInputs {
  organization: pulumi.Input<string>;
  orgServiceUrl: pulumi.Input<string>;
  project: pulumi.Input<string>;
  auditEnforceSettableVar: pulumi.Input<boolean>;
}

interface ProjectPipelineSettingsInputs {
  organization: string;
  orgServiceUrl: string;
  project: string;
  auditEnforceSettableVar: boolean;
}

interface ProjectPipelineSettingsOutputs extends ProjectPipelineSettingsInputs {
  id: string;
}

class ProjectPipelineSettingsProvider implements pulumi.dynamic.ResourceProvider {
  private async getWebApiClient(orgServiceUrl: string): Promise<azdev.WebApi> {
    const token = process.env.AZDO_PERSONAL_ACCESS_TOKEN;
    if (!token) {
      throw new Error('AZDO_PERSONAL_ACCESS_TOKEN is not set');
    }
    const authHandler = azdev.getPersonalAccessTokenHandler(token);
    return new azdev.WebApi(orgServiceUrl, authHandler);
  }

  async create(
    inputs: ProjectPipelineSettingsInputs
  ): Promise<pulumi.dynamic.CreateResult<ProjectPipelineSettingsOutputs>> {
    const connection = await this.getWebApiClient(inputs.orgServiceUrl);
    const buildApiClient = await connection.getBuildApi();
    
    const result = await buildApiClient.updateBuildGeneralSettings(
      { auditEnforceSettableVar: inputs.auditEnforceSettableVar },
      "project"
    );

    let generatedId = `${inputs.organization}-${inputs.project}`.replace(/\s/g, '-').toLowerCase();

    return {
      id: generatedId,
      outs: { id: generatedId, ...inputs, ...result }
    };
  }
}

export class ProjectPipelineSettings extends pulumi.dynamic.Resource {
  readonly organization!: pulumi.Output<string>;
  readonly orgServiceUrl!: pulumi.Output<string>;
  readonly project!: pulumi.Output<string>;
  readonly auditEnforceSettableVar!: pulumi.Output<boolean>;

  constructor(
    name: string,
    args: ProjectPipelineSettingsResourceInputs,
    opts?: pulumi.CustomResourceOptions
  ) {
    super(new ProjectPipelineSettingsProvider(), name, args, opts);
  }
}

I call this resource in index.ts like this:

new ProjectPipelineSettings('project-pipeline-settings', {
  organization: azdoConfig.require('organization'),
  orgServiceUrl: azdoConfig.require('orgServiceUrl'),
  project: azdoConfig.require('project'),
  auditEnforceSettableVar: true
});

However, when I run pulumi up, I get the following error:

Diagnostics:
  pulumi:pulumi:Stack (pulumi-test-dev):
    error: Error serializing '() => provider': index.js(50,43)
    '() => provider': index.js(50,43): captured
      variable 'provider' which indirectly referenced
        function 'ProjectPipelineSettingsProvider': ProjectPipelineSettings.ts(1,196): which referenced
          function 'getWebApiClient': ProjectPipelineSettings.ts(1,309): which captured
            variable 'azdev' which indirectly referenced
              function 'WebApi': WebApi.js(97,15): which could not be serialized because
                Unexpected missing variable in closure environment: window

It seems like azure-devops-node-api references window, which causes problems during serialization. How can I modify my Pulumi dynamic provider to avoid this issue?

Any help would be greatly appreciated!


Solution

  • Unexpected missing variable in closure environment: window

    The error you're encountering is due to Pulumi tries to serialize the dynamic provider, which includes the Azure Devops API client.

    To resolve the issue,

    Instead of using a Dynamic Provider which Pulumi serializes, use a ComponentResource.

    As component resources run locally, so Pulumi does not try to serialize azure-devops-node-api.

    Here's the sample Provider code:

    import * as pulumi from "@pulumi/pulumi";
    import * as azdev from "azure-devops-node-api";
    
    export interface ProjectPipelineSettingsArgs {
      organization: pulumi.Input<string>;
      orgServiceUrl: pulumi.Input<string>;
      project: pulumi.Input<string>;
      auditEnforceSettableVar: pulumi.Input<boolean>;
    }
    
    export class ProjectPipelineSettings extends pulumi.ComponentResource {
      constructor(name: string, args: ProjectPipelineSettingsArgs, opts?: pulumi.ResourceOptions) {
        super("custom:devops:ProjectPipelineSettings", name, {}, opts);
    
        const token = process.env.AZDO_PERSONAL_ACCESS_TOKEN;
        if (!token) {
          throw new Error("AZDO_PERSONAL_ACCESS_TOKEN is not set");
        }
    
        const authHandler = azdev.getPersonalAccessTokenHandler(token);
        const connection = new azdev.WebApi(args.orgServiceUrl, authHandler);
        pulumi.output(connection.getBuildApi()).apply(async (buildApi) => {
          await buildApi.updateBuildGeneralSettings(
            { auditEnforceSettableVar: args.auditEnforceSettableVar },
            args.project
          );
        });
    
        this.registerOutputs({});
      }
    }
    

    Please refer this Doc for better understanding of component resources.