Repository: aws-samples/cdk-bootstrapless-synthesizer (language: python)

Issue:
The ECR image URI is not expected to have a hash suffix
The current ECR image URI has a hash suffix. Other programs cannot get the URI because they do not have the list permission.   
If there is no hash suffix, other programs can splice the image URI.   
To be compatible with previous programs, the hash suffix is used by default. When there is a special parameter, the hash suffix is not used.


Discussion / hints:

Fix patch:
diff --git a/API.md b/API.md
index 7b72375..49c44eb 100644
--- a/API.md
+++ b/API.md
@@ -27,7 +27,8 @@ const bootstraplessStackSynthesizerProps: BootstraplessStackSynthesizerProps = {
 | [`imageAssetPublishingRoleArn`](#cdkbootstraplesssynthesizerbootstraplessstacksynthesizerpropspropertyimageassetpublishingrolearn) | `string` | The role to use to publish image assets to the ECR repository in this environment. |
 | [`imageAssetRegionSet`](#cdkbootstraplesssynthesizerbootstraplessstacksynthesizerpropspropertyimageassetregionset) | `string`[] | Override the ECR repository region of the Docker Image assets. |
 | [`imageAssetRepositoryName`](#cdkbootstraplesssynthesizerbootstraplessstacksynthesizerpropspropertyimageassetrepositoryname) | `string` | Name of the ECR repository to hold Docker Image assets. |
-| [`imageAssetTagPrefix`](#cdkbootstraplesssynthesizerbootstraplessstacksynthesizerpropspropertyimageassettagprefix) | `string` | Override the tag of the Docker Image assets. |
+| [`imageAssetTagPrefix`](#cdkbootstraplesssynthesizerbootstraplessstacksynthesizerpropspropertyimageassettagprefix) | `string` | Override the tag prefix of the Docker Image assets. |
+| [`imageAssetTagSuffixType`](#cdkbootstraplesssynthesizerbootstraplessstacksynthesizerpropspropertyimageassettagsuffixtype) | [`cdk-bootstrapless-synthesizer.ImageAssetTagSuffixType`](#cdk-bootstrapless-synthesizer.ImageAssetTagSuffixType) | Override the tag suffix of the Docker Image assets. |
 | [`templateBucketName`](#cdkbootstraplesssynthesizerbootstraplessstacksynthesizerpropspropertytemplatebucketname) | `string` | Override the name of the S3 bucket to hold Cloudformation template. |
 
 ---
@@ -157,7 +158,20 @@ public readonly imageAssetTagPrefix: string;
 - *Type:* `string`
 - *Default:* process.env.BSS_IMAGE_ASSET_TAG_PREFIX
 
-Override the tag of the Docker Image assets.
+Override the tag prefix of the Docker Image assets.
+
+---
+
+##### `imageAssetTagSuffixType`<sup>Optional</sup> <a name="cdk-bootstrapless-synthesizer.BootstraplessStackSynthesizerProps.property.imageAssetTagSuffixType" id="cdkbootstraplesssynthesizerbootstraplessstacksynthesizerpropspropertyimageassettagsuffixtype"></a>
+
+```typescript
+public readonly imageAssetTagSuffixType: ImageAssetTagSuffixType;
+```
+
+- *Type:* [`cdk-bootstrapless-synthesizer.ImageAssetTagSuffixType`](#cdk-bootstrapless-synthesizer.ImageAssetTagSuffixType)
+- *Default:* HASH or process.env.BSS_IMAGE_ASSET_TAG_SUFFIX_TYPE
+
+Override the tag suffix of the Docker Image assets.
 
 ---
 
@@ -561,3 +575,23 @@ public visit(construct: IConstruct)
 
 
 
+## Enums <a name="Enums" id="enums"></a>
+
+### ImageAssetTagSuffixType <a name="ImageAssetTagSuffixType" id="imageassettagsuffixtype"></a>
+
+| **Name** | **Description** |
+| --- | --- |
+| [`NONE`](#cdkbootstraplesssynthesizerimageassettagsuffixtypenone) | *No description.* |
+| [`HASH`](#cdkbootstraplesssynthesizerimageassettagsuffixtypehash) | *No description.* |
+
+---
+
+#### `NONE` <a name="cdk-bootstrapless-synthesizer.ImageAssetTagSuffixType.NONE" id="cdkbootstraplesssynthesizerimageassettagsuffixtypenone"></a>
+
+---
+
+
+#### `HASH` <a name="cdk-bootstrapless-synthesizer.ImageAssetTagSuffixType.HASH" id="cdkbootstraplesssynthesizerimageassettagsuffixtypehash"></a>
+
+---
+
diff --git a/src/index.ts b/src/index.ts
index 6dcc799..fa0af4a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -9,6 +9,11 @@ export * from './aspect';
 const REGION_PLACEHOLDER = '${AWS::Region}';
 const ERR_MSG_CALL_BIND_FIRST = 'You must call bind() first';
 
+export enum ImageAssetTagSuffixType {
+  NONE = 'NONE',
+  HASH = 'HASH',
+}
+
 /**
  * Configuration properties for BootstraplessStackSynthesizer
  */
@@ -92,12 +97,19 @@ export interface BootstraplessStackSynthesizerProps {
   readonly templateBucketName?: string;
 
   /**
-   * Override the tag of the Docker Image assets
+   * Override the tag prefix of the Docker Image assets
    *
    * @default - process.env.BSS_IMAGE_ASSET_TAG_PREFIX
    */
   readonly imageAssetTagPrefix?: string;
 
+  /**
+   * Override the tag suffix of the Docker Image assets
+   *
+   * @default - HASH or process.env.BSS_IMAGE_ASSET_TAG_SUFFIX_TYPE
+   */
+  readonly imageAssetTagSuffixType?: ImageAssetTagSuffixType;
+
   /**
    * Override the ECR repository region of the Docker Image assets
    *
@@ -129,7 +141,8 @@ export class BootstraplessStackSynthesizer extends StackSynthesizer {
   private fileAssetPrefix?: string;
   private fileAssetRegionSet?: string[];
   private templateBucketName?: string;
-  private imageAssetTagPrefix?: string;
+  private imageAssetTagPrefix: string;
+  private imageAssetTagSuffixType: ImageAssetTagSuffixType;
   private imageAssetRegionSet?: string[];
   private imageAssetAccountId?: string;
 
@@ -151,9 +164,11 @@ export class BootstraplessStackSynthesizer extends StackSynthesizer {
 
       BSS_TEMPLATE_BUCKET_NAME,
       BSS_IMAGE_ASSET_TAG_PREFIX,
+      BSS_IMAGE_ASSET_TAG_SUFFIX_TYPE,
       BSS_IMAGE_ASSET_REGION_SET,
       BSS_IMAGE_ASSET_ACCOUNT_ID,
     } = process.env;
+    /* eslint-disable max-len */
     this.bucketName = props.fileAssetBucketName ?? BSS_FILE_ASSET_BUCKET_NAME;
     this.repositoryName = props.imageAssetRepositoryName ?? BSS_IMAGE_ASSET_REPOSITORY_NAME;
     this.fileAssetPublishingRoleArn = props.fileAssetPublishingRoleArn ?? BSS_FILE_ASSET_PUBLISHING_ROLE_ARN;
@@ -162,8 +177,10 @@ export class BootstraplessStackSynthesizer extends StackSynthesizer {
     this.fileAssetRegionSet = props.fileAssetRegionSet ?? commaSplit(BSS_FILE_ASSET_REGION_SET);
     this.templateBucketName = props.templateBucketName ?? BSS_TEMPLATE_BUCKET_NAME;
     this.imageAssetTagPrefix = (props.imageAssetTagPrefix ?? BSS_IMAGE_ASSET_TAG_PREFIX) ?? '';
+    this.imageAssetTagSuffixType = validateImageAssetTagSuffixType((props.imageAssetTagSuffixType ?? BSS_IMAGE_ASSET_TAG_SUFFIX_TYPE) ?? ImageAssetTagSuffixType.HASH);
     this.imageAssetRegionSet = props.imageAssetRegionSet ?? commaSplit(BSS_IMAGE_ASSET_REGION_SET);
     this.imageAssetAccountId = props.imageAssetAccountId ?? BSS_IMAGE_ASSET_ACCOUNT_ID;
+    /* eslint-enable max-len */
   }
 
   public bind(stack: Stack): void {
@@ -257,7 +274,11 @@ export class BootstraplessStackSynthesizer extends StackSynthesizer {
     assertNotNull(this.repositoryName, 'The repositoryName is null');
     validateDockerImageAssetSource(asset);
 
-    const imageTag = this.imageAssetTagPrefix + asset.sourceHash;
+    let imageTag = this.imageAssetTagPrefix;
+    if (this.imageAssetTagSuffixType === ImageAssetTagSuffixType.HASH) {
+      imageTag = this.imageAssetTagPrefix + asset.sourceHash;
+    }
+
     const destinations: { [id: string]: cxschema.DockerImageDestination } = {};
 
     if (this.imageAssetRegionSet?.length) {
@@ -462,6 +483,13 @@ function commaSplit(v?: string): string[] | undefined {
   return undefined;
 }
 
+function validateImageAssetTagSuffixType(s: string): ImageAssetTagSuffixType {
+  if (!Object.values(ImageAssetTagSuffixType).includes(s as ImageAssetTagSuffixType)) {
+    throw new Error(`Invalid ImageAssetTagSuffixType: ${s}, must be in ${Object.values(ImageAssetTagSuffixType)}`);
+  }
+  return s as ImageAssetTagSuffixType;
+}
+
 function validateFileAssetSource(asset: FileAssetSource) {
   if (!!asset.executable === !!asset.fileName) {
     throw new Error(`Exactly one of 'fileName' or 'executable' is required, got: ${JSON.stringify(asset)}`);


Test patch:
diff --git a/test/BootstraplessStackSynthesizer.test.ts b/test/BootstraplessStackSynthesizer.test.ts
index 0013c45..6253bb3 100644
--- a/test/BootstraplessStackSynthesizer.test.ts
+++ b/test/BootstraplessStackSynthesizer.test.ts
@@ -2,7 +2,7 @@ import * as fs from 'fs';
 import * as cxschema from 'aws-cdk-lib/cloud-assembly-schema';
 import { FileAssetPackaging, Stack, App } from 'aws-cdk-lib/core';
 import * as cxapi from 'aws-cdk-lib/cx-api';
-import { BootstraplessStackSynthesizer, BootstraplessStackSynthesizerProps } from '../src';
+import { BootstraplessStackSynthesizer, BootstraplessStackSynthesizerProps, ImageAssetTagSuffixType } from '../src';
 
 beforeEach(() => {
   delete process.env.BSS_FILE_ASSET_BUCKET_NAME;
@@ -13,6 +13,7 @@ beforeEach(() => {
   delete process.env.BSS_FILE_ASSET_REGION_SET;
   delete process.env.BSS_TEMPLATE_BUCKET_NAME;
   delete process.env.BSS_IMAGE_ASSET_TAG_PREFIX;
+  delete process.env.BSS_IMAGE_ASSET_TAG_SUFFIX_TYPE;
   delete process.env.BSS_IMAGE_ASSET_REGION_SET;
   delete process.env.BSS_IMAGE_ASSET_ACCOUNT_ID;
 });
@@ -263,6 +264,66 @@ test.each([
   expect(json.files).toEqual({});
 });
 
+test.each([
+  () => mksynthzer({ imageAssetRepositoryName: 'the-repo', imageAssetTagSuffixType: ImageAssetTagSuffixType.HASH }),
+  () => mksynthzer({}, { BSS_IMAGE_ASSET_REPOSITORY_NAME: 'the-repo', BSS_IMAGE_ASSET_TAG_SUFFIX_TYPE: 'HASH' }),
+])('#%# addDockerImageAsset when imageAssetTagSuffixType HASH is specified', (mksynthzerFn) => {
+  const stack = new Stack();
+  const synthesizer = mksynthzerFn();
+  synthesizer.bind(stack);
+  synthesizer.addDockerImageAsset({
+    directoryName: __dirname,
+    sourceHash: 'abcdef',
+  });
+  const json = JSON.parse(synthesizer.dumps());
+
+  expect(json.dockerImages.abcdef.source).toEqual({
+    directory: __dirname,
+  });
+  expect(json.dockerImages.abcdef.destinations).toEqual({
+    'current_account-current_region': {
+      repositoryName: 'the-repo',
+      imageTag: 'abcdef',
+    },
+  });
+  expect(json.files).toEqual({});
+});
+
+test.each([
+  () => mksynthzer({ imageAssetRepositoryName: 'the-repo', imageAssetTagSuffixType: ImageAssetTagSuffixType.NONE }),
+  () => mksynthzer({}, { BSS_IMAGE_ASSET_REPOSITORY_NAME: 'the-repo', BSS_IMAGE_ASSET_TAG_SUFFIX_TYPE: 'NONE' }),
+])('#%# addDockerImageAsset when imageAssetTagSuffixType NONE is specified', (mksynthzerFn) => {
+  const stack = new Stack();
+  const synthesizer = mksynthzerFn();
+  synthesizer.bind(stack);
+  synthesizer.addDockerImageAsset({
+    directoryName: __dirname,
+    sourceHash: 'abcdef',
+  });
+  const json = JSON.parse(synthesizer.dumps());
+
+  expect(json.dockerImages.abcdef.source).toEqual({
+    directory: __dirname,
+  });
+  expect(json.dockerImages.abcdef.destinations).toEqual({
+    'current_account-current_region': {
+      repositoryName: 'the-repo',
+      imageTag: '',
+    },
+  });
+  expect(json.files).toEqual({});
+});
+
+test.each([
+  () => mksynthzer({ imageAssetRepositoryName: 'the-repo', imageAssetTagSuffixType: 'invalid' as ImageAssetTagSuffixType }),
+  () => mksynthzer({}, { BSS_IMAGE_ASSET_REPOSITORY_NAME: 'the-repo', BSS_IMAGE_ASSET_TAG_SUFFIX_TYPE: 'INVALID' }),
+  () => mksynthzer({}, { BSS_IMAGE_ASSET_REPOSITORY_NAME: 'the-repo', BSS_IMAGE_ASSET_TAG_SUFFIX_TYPE: '' }),
+])('#%# addDockerImageAsset when imageAssetTagSuffixType is invalid', (mksynthzerFn) => {
+  expect(() => {
+    mksynthzerFn();
+  }).toThrow(/Invalid ImageAssetTagSuffixType/);
+});
+
 
 test.each([
   () => mksynthzer({ imageAssetRepositoryName: 'the-repo', imageAssetRegionSet: ['us-west-1'] }),
@@ -399,7 +460,7 @@ function readAssetManifest(asm: cxapi.CloudAssembly): cxschema.AssetManifest {
   return JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' }));
 }
 
-function mksynthzer(props: BootstraplessStackSynthesizerProps, env?: {[key: string]: string}): BootstraplessStackSynthesizer {
+function mksynthzer(props: BootstraplessStackSynthesizerProps, env?: { [key: string]: string }): BootstraplessStackSynthesizer {
   if (env) {
     for (const key in env) {
       process.env[key] = env[key];