I'm trying to build a dynamic resource + provider....
# typescript
j
I'm trying to build a dynamic resource + provider. The lifecycle of the resource itself works just fine - (at least as far as I've implemented it): The respective objects get created/updated in my AWS account. However what I'm having trouble is making the inputs/outputs available on the resource object within my pulumi program:
Copy code
// USAGE EXAMPLE
  const childModel = new AssetModel("test-child-1", {
    name: "some-child-model",
    description: "some-child-model",
    externalId: "some-child-model",
  })

  const assetModel = new AssetModel("test-1", {
    name: "some-asset-model",
    description: "some-asset-model",
    externalId: "some-asset-model",
    properties: [
      {
        name: "prop-2",
        externalId: "SAM-prop-1",
        dataType: PropertyDataType.DOUBLE,
        unit: "someNewUnit",
      },
    ],
    children: [
      {
        name: "asset-model-child",
        externalId: "asset-model-child",
        // !!! The next line is what causes the failure: `childModel.externalId` is `undefined` when pulumi runs the program
        modelId: childModel.externalId.apply(externalId),
      },
    ],
  })
And here's the implementation my resource + provider (some implementation details omitted):
Copy code
export interface AssetModelProviderArgs {
  /** The name of the asset model */
  name: string
  /** An external id to reference this asset model */
  externalId: string
  /** The description of the asset model */
  description: string
  /** The properties of the asset model */
  properties?: AssetModelProperty[]
  /** The child asset model associations of the asset model */
  children?: AssetModelChild[]
}

export interface AssetModelProperty {
  /** The name of the asset model property */
  name: string
  /** An external id to reference this asset model property */
  externalId: string
  /** The data type of the asset model property */
  dataType: PropertyDataType
  /** The unit of the asset model property */
  unit?: string
}

export interface AssetModelChild {
  /** The name of the child asset model association */
  name: string
  /** An external id to reference this child asset model association */
  externalId: string
  /** The external id of the child asset model */
  modelId: SitewiseId
}

export interface AssetModelProviderOuts extends AssetModelProviderArgs {
  arn: string
}

const readAssetModelOuts = async (
  id: string,
): Promise<AssetModelProviderOuts | undefined> => {
  let readResponse: DescribeAssetModelResponse | null = null
  try {
    readResponse = await getSitewiseClient().assetModel.read({
      assetModelId: id,
    })
  } catch (error) {
    if (error instanceof AxiosError && error.response?.status === 404) {
      return undefined
    }

    throw error
  }

  const assetModel: AssetModelProviderArgs = {
    name: readResponse.assetModelName!,
    externalId: readResponse.assetModelExternalId!,
    description: readResponse.assetModelDescription!,
    properties: (readResponse.assetModelProperties ?? []).map((property) => ({
      name: property.name!,
      externalId: property.externalId!,
      dataType: property.dataType!,
      unit: property.unit,
    })),
    children: (readResponse.assetModelHierarchies ?? []).map((hierarchy) => ({
      name: hierarchy.name!,
      externalId: hierarchy.externalId!,
      modelId: uuid(hierarchy.childAssetModelId!),
    })),
  }

  return {
    ...assetModel,
    arn: readResponse.assetModelArn!,
  }
}

export interface AssetModelArgs {
  name: Input<string>
  description: Input<string>
  externalId: Input<string>
  properties?: Input<AssetModelProviderArgs["properties"]>
  children?: AsInput<AssetModelChild>[]
}

export const assetModelProvider: dynamic.ResourceProvider<
  AssetModelProviderArgs,
  AssetModelProviderOuts
> = {
  async create(args) {
    // ... details omitted

    return {
      id,
      outs: await readAssetModelOuts(id),
    }
  },
  async read(id, outs) {
    return {
      id,
      props: outs ? await readAssetModelOuts(id) : undefined,
    }
  },
  async diff(_, oldOuts, newArgs) {
    // ... details omitted

    return {
      changes: !deepEqual(relevantOuts, relevantArgs),
      stables: ["id", "arn", "externalId"],
    }
  },
  async update(id, _, newArgs) {
    // ... details omitted

    return {
      outs: await readAssetModelOuts(id),
    }
  },
  async delete(id) {
    // Note: This is not sufficient to delete models that contribute to a property formula in a parent.
    await getSitewiseClient().assetModel.delete({ assetModelId: id })
  },
}

export class AssetModel extends dynamic.Resource {
  // args
  public readonly name!: Output<AssetModelProviderOuts["name"]>
  public readonly description!: Output<AssetModelProviderOuts["description"]>
  public readonly externalId!: Output<AssetModelProviderOuts["externalId"]>
  // outs
  public readonly arn!: Output<AssetModelProviderOuts["arn"]>

  constructor(
    name: string,
    args: AssetModelArgs,
    opts?: CustomResourceOptions,
  ) {
    super(assetModelProvider, `xxx:asset-model:${name}`, args, opts)
  }
}
What's the issue here? (I have checked the pulumi state JSON file and the
externalId
for the child model is set at some point.)
m
I think assetModel should depend on childModel if you want to ensure it is created and the externalID is populated. Something like:
Copy code
const assetModel = new AssetModel("test-1", {
    name: "some-asset-model",
    description: "some-asset-model",
    externalId: "some-asset-model",
    properties: [
      {
        name: "prop-2",
        externalId: "SAM-prop-1",
        dataType: PropertyDataType.DOUBLE,
        unit: "someNewUnit",
      },
    ],
    children: [
      {
        name: "asset-model-child",
        externalId: "asset-model-child",
        // !!! The next line is what causes the failure: `childModel.externalId` is `undefined` when pulumi runs the program
        modelId: childModel.externalId.apply(externalId),
      },
    ],
  },
  { dependsOn: [ childModel ] },
)
j
That seems to not be the issue. It's still
undefined
even when adding an explicit dependency, (Besides: I was expecting that pulumi would be able to derive the dependency from the usage of an output as part of the input of a resource?)
m
I ran into something similar when creating dynamic docker images. The way I worked around it was by nesting the apply statements of the needed properties which ensured that acrName and registry were available when I created the docker images. I think that Pulumi may not be able to handle the depends on for a resource that it is not yet aware of. for instance I did this:
Copy code
import * as pulumi from '@pulumi/pulumi';
import * as dockerbuild from '@pulumi/docker-build';
import { marketingcr, acrUsername, acrPassword, registryUrl } from './registries/acrRegistry';

const config = new pulumi.Config();
const imageTag = config.get('imageTag') || 'latest';
const imageNames = [
  'marketing-nginx',
  'marketing-mautic_web',
];
const imageBuilds: { [key: string]: dockerbuild.Image } = {};

// Wait for the ACR to be created before proceeding
marketingcr.name.apply(acrName => {
  registryUrl.apply(registry => {
    for (const imageName of imageNames) {
      const fullImageName = pulumi.interpolate`${registry}/${imageName}:${imageTag}`;
      // Define the dockerbuild.Image resource
      imageBuilds[imageName] = new dockerbuild.Image(
        imageName,
        {
          context: {
            location: '../mautic',
          },
          push: true,
          dockerfile: {
            location: `../mautic/${imageName}.dockerfile`,
          },
          platforms: ['linux/amd64'],

          registries: [{
            address: registry,
            username: acrUsername,
            password: acrPassword,
          }],
          tags: [fullImageName],
    });
    }
  });
});

// Export the image builds so they can be used elsewhere in the Pulumi stack
export { imageBuilds };
So maybe you could nest the parent in the child like:
Copy code
childModel.externalId.apply(externalId => {
    const assetModel = new AssetModel("test-1", {
        name: "some-asset-model",
        description: "some-asset-model",
        externalId: "some-asset-model",
        properties: [
          {
            name: "prop-2",
            externalId: "SAM-prop-1",
            dataType: PropertyDataType.DOUBLE,
            unit: "someNewUnit",
          },
        ],
        children: [
          {
            name: "asset-model-child",
            externalId: "asset-model-child",
            // !!! The next line is what causes the failure: `childModel.externalId` is `undefined` when pulumi runs the program
            modelId: externalId,
          },
        ],
      },
  },)
)
I believe that If externalID is still undefined this way, then it doesn't exist before running this statement.