Repository: basti-app/basti (language: python)

Issue:
basti cleanup not respecting region flag
## Bug Description

When having multiple setups of basti in different regions such as eu-central-1 and us-west-2 basti cleanup doesn't respect the --aws-region flag when it comes to IAM resources.

## Steps to Reproduce

1. Run `basti init --aws-region eu-central-1`
2. Run `basti init --aws-region us-west-2`
3. Verify basti is working in both regions & that an IAM role / instance profile exists for each region
4. Run `basti cleanup --aws-region us-west-2`

## Expected Behavior

After the command in step 4 all basti resources wich were deployed in step 2 should have been removed and all basti resources which were deployed in step 1 should stil be existing.

## Current Behavior

Unfortunately basti cleans _ALL_ IAM resources including the role / instance profile that belongs to the eu-central-1 setup wich essentially renders that setup unusable as a connection to the instance via Session Manger is no longer possible.

## Possible Solution (Optional)

Somehow make basti aware of the region of the setup which IAM resource belongs to so that basti cleanup does not remove resources which it shouldn't remove.



Apart from that thanks for proving such an elegant solution! :)



Discussion / hints:
Hey @andreas-mueller-bb, thanks for reporting this! I'll take a look soon! Meanwhile, would you consider contributing to Basti?
Hi @BohdanPetryshyn 
I would. But unfortunately I lack the necessary programming skills to do so 😅 

Fix patch:
diff --git a/package-lock.json b/package-lock.json
index cca3820..a9d3c6a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22914,7 +22914,7 @@
       }
     },
     "packages/basti": {
-      "version": "1.6.1",
+      "version": "1.6.2",
       "license": "MIT",
       "dependencies": {
         "@aws-sdk/client-ec2": "3.231.0",
diff --git a/packages/basti-cdk/src/basti-constants.ts b/packages/basti-cdk/src/basti-constants.ts
index 9a4f903..6d48845 100644
--- a/packages/basti-cdk/src/basti-constants.ts
+++ b/packages/basti-cdk/src/basti-constants.ts
@@ -5,8 +5,9 @@ export const BASTION_INSTANCE_SECURITY_GROUP_NAME_PREFIX =
 
 export const TARGET_ACCESS_SECURITY_GROUP_NAME_PREFIX = 'basti-access';
 
-export const BASTION_INSTANCE_ROLE_PATH = `/basti/`;
-export const BASTION_INSTANCE_PROFILE_PATH = BASTION_INSTANCE_ROLE_PATH;
+export const BASTION_INSTANCE_ROLE_PATH_PREFIX = `/basti`;
+export const BASTION_INSTANCE_PROFILE_PATH_PREFIX =
+  BASTION_INSTANCE_ROLE_PATH_PREFIX;
 
 export const BASTION_INSTANCE_ID_TAG_NAME = 'basti:id';
 export const BASTION_INSTANCE_IN_USE_TAG_NAME = 'basti:in-use';
diff --git a/packages/basti-cdk/src/basti-instance.ts b/packages/basti-cdk/src/basti-instance.ts
index 150084b..26b6fda 100644
--- a/packages/basti-cdk/src/basti-instance.ts
+++ b/packages/basti-cdk/src/basti-instance.ts
@@ -9,6 +9,7 @@ import {
   BASTION_INSTANCE_IN_USE_TAG_NAME,
   BASTION_INSTANCE_NAME_PREFIX,
   BASTION_INSTANCE_ROLE_NAME_PREFIX,
+  BASTION_INSTANCE_ROLE_PATH_PREFIX,
   BASTION_INSTANCE_SECURITY_GROUP_NAME_PREFIX,
 } from './basti-constants';
 
@@ -108,6 +109,8 @@ export class BastiInstance extends Construct implements IBastiInstance {
   constructor(scope: Construct, id: string, props: BastiInstanceProps) {
     super(scope, id);
 
+    const region = Stack.of(this).region;
+
     this.vpc = props.vpc;
 
     this.bastiId = props.bastiId ?? generateShortId(id);
@@ -139,6 +142,7 @@ export class BastiInstance extends Construct implements IBastiInstance {
     this.role = new aws_iam.Role(this, 'IamRoleBastionInstance', {
       assumedBy: new aws_iam.ServicePrincipal('ec2.amazonaws.com'),
       roleName: `${BASTION_INSTANCE_ROLE_NAME_PREFIX}-${this.bastiId}`,
+      path: `${BASTION_INSTANCE_ROLE_PATH_PREFIX}/${region}/`,
       inlinePolicies: {
         'session-manager-access': sessionManagerPolicy,
       },
diff --git a/packages/basti/README.md b/packages/basti/README.md
index 164e7d2..7b41f24 100644
--- a/packages/basti/README.md
+++ b/packages/basti/README.md
@@ -95,6 +95,14 @@ With [Basti](https://github.com/basti-app/basti), you can securely connect to RD
 
 ## 💻 Installation
 
+Using homebrew
+
+```sh
+brew install basti
+```
+
+Using npm
+
 ```sh
 npm install --global basti
 ```
diff --git a/packages/basti/src/aws/common/aws-client.ts b/packages/basti/src/aws/common/aws-client.ts
index 7eaa5be..66bc850 100644
--- a/packages/basti/src/aws/common/aws-client.ts
+++ b/packages/basti/src/aws/common/aws-client.ts
@@ -6,6 +6,7 @@ export type AwsClientConstructor<T> = new (config: AwsClientConfiguration) => T;
 
 export interface RawAwsClient {
   send: (...args: any[]) => Promise<any>;
+  config: object;
 }
 
 export type AwsClientErrorHandler<TResponse> = (
@@ -41,4 +42,8 @@ export class AwsClient<T extends RawAwsClient> {
     this.errorHandler !== undefined
       ? await this.errorHandler(async () => await this.client.send(...args))
       : await this.client.send(...args);
+
+  get config(): T['config'] {
+    return this.client.config;
+  }
 }
diff --git a/packages/basti/src/aws/ec2/create-ec2-instance.ts b/packages/basti/src/aws/ec2/create-ec2-instance.ts
index 72daca0..4c4204b 100644
--- a/packages/basti/src/aws/ec2/create-ec2-instance.ts
+++ b/packages/basti/src/aws/ec2/create-ec2-instance.ts
@@ -7,7 +7,7 @@ import { retry } from '#src/common/retry.js';
 
 import { handleWaiterError } from '../common/waiter-error.js';
 import { COMMON_WAITER_CONFIG } from '../common/waiter-config.js';
-import { createIamInstanceProfile } from '../iam/create-instance-profile.js';
+import { createIamInstanceProfileCurrentRegion } from '../iam/create-instance-profile.js';
 import { toTagSpecification } from '../tags/utils/to-tag-specification.js';
 
 import { AwsInstanceProfileNotFoundError } from './ec2-errors.js';
@@ -23,7 +23,7 @@ export interface CreateEc2InstanceInput {
   instanceType: string;
 
   roleNames: string[];
-  profilePath: string;
+  profilePathPrefix: string;
 
   subnetId: string;
   assignPublicIp: boolean;
@@ -42,7 +42,7 @@ export async function createEc2Instance({
   name,
   instanceType,
   roleNames,
-  profilePath,
+  profilePathPrefix,
   subnetId,
   assignPublicIp,
   securityGroupIds,
@@ -53,10 +53,10 @@ export async function createEc2Instance({
 }: CreateEc2InstanceInput): Promise<AwsEc2Instance> {
   const tagsWithName = [...tags, { key: 'Name', value: name }];
 
-  const instanceProfile = await createIamInstanceProfile({
+  const instanceProfile = await createIamInstanceProfileCurrentRegion({
     name,
     roleNames,
-    path: profilePath,
+    pathPrefix: profilePathPrefix,
     tags,
   });
 
diff --git a/packages/basti/src/aws/iam/create-iam-role.ts b/packages/basti/src/aws/iam/create-iam-role.ts
index 8fab697..9084993 100644
--- a/packages/basti/src/aws/iam/create-iam-role.ts
+++ b/packages/basti/src/aws/iam/create-iam-role.ts
@@ -16,23 +16,26 @@ export interface InlinePolicyInput {
   document: string;
 }
 
-export interface CreateIamRoleInput {
+export interface CreateIamRoleCurrentRegionInput {
   name: string;
-  path: string;
+  pathPrefix: string;
   principalService: string;
   managedPolicies?: string[];
   inlinePolicies?: InlinePolicyInput[];
   tags?: AwsTag[];
 }
 
-export async function createIamRole({
+export async function createIamRoleCurrentRegion({
   name,
-  path,
+  pathPrefix,
   principalService,
   managedPolicies,
   inlinePolicies,
   tags,
-}: CreateIamRoleInput): Promise<AwsRole> {
+}: CreateIamRoleCurrentRegionInput): Promise<AwsRole> {
+  const region = await iamClient.config.region();
+  const path = `${pathPrefix}/${region}/`;
+
   const { Role } = await iamClient.send(
     new CreateRoleCommand({
       RoleName: name,
diff --git a/packages/basti/src/aws/iam/create-instance-profile.ts b/packages/basti/src/aws/iam/create-instance-profile.ts
index 1061743..de0b8e4 100644
--- a/packages/basti/src/aws/iam/create-instance-profile.ts
+++ b/packages/basti/src/aws/iam/create-instance-profile.ts
@@ -13,19 +13,22 @@ import { parseIamInstanceProfileResponse } from './parse-iam-response.js';
 import type { AwsTag } from '../tags/types.js';
 import type { AwsIamInstanceProfile } from './types.js';
 
-export interface CreateInstanceProfileInput {
+export interface CreateInstanceProfileCurrentRegionInput {
   name: string;
   roleNames: string[];
-  path: string;
+  pathPrefix: string;
   tags: AwsTag[];
 }
 
-export async function createIamInstanceProfile({
+export async function createIamInstanceProfileCurrentRegion({
   name,
   roleNames,
-  path,
+  pathPrefix,
   tags,
-}: CreateInstanceProfileInput): Promise<AwsIamInstanceProfile> {
+}: CreateInstanceProfileCurrentRegionInput): Promise<AwsIamInstanceProfile> {
+  const region = await iamClient.config.region();
+  const path = `${pathPrefix}/${region}/`;
+
   const { InstanceProfile } = await iamClient.send(
     new CreateInstanceProfileCommand({
       InstanceProfileName: name,
diff --git a/packages/basti/src/aws/iam/get-iam-role.ts b/packages/basti/src/aws/iam/get-iam-role.ts
index b53c762..6bca84a 100644
--- a/packages/basti/src/aws/iam/get-iam-role.ts
+++ b/packages/basti/src/aws/iam/get-iam-role.ts
@@ -12,8 +12,8 @@ import {
 
 import type { AwsRole, AwsRoleAttachedPolicy } from './types.js';
 
-export interface GetIamRoleInput {
-  path: string;
+export interface GetIamRolesCurrentRegionInput {
+  pathPrefix: string;
 }
 
 export interface GetIamRolePoliciesInput {
@@ -24,9 +24,12 @@ export interface GetIamRoleAttachedPoliciesInput {
   roleName: string;
 }
 
-export async function getIamRoles({
-  path,
-}: GetIamRoleInput): Promise<AwsRole[]> {
+export async function getIamRolesCurrentRegion({
+  pathPrefix,
+}: GetIamRolesCurrentRegionInput): Promise<AwsRole[]> {
+  const region = await iamClient.config.region();
+  const path = `${pathPrefix}/${region}/`;
+
   const { Roles } = await iamClient.send(
     new ListRolesCommand({
       PathPrefix: path,
diff --git a/packages/basti/src/aws/iam/get-instance-profiles.ts b/packages/basti/src/aws/iam/get-instance-profiles.ts
index 7d0fa2d..797d3c8 100644
--- a/packages/basti/src/aws/iam/get-instance-profiles.ts
+++ b/packages/basti/src/aws/iam/get-instance-profiles.ts
@@ -8,17 +8,20 @@ import { parseIamInstanceProfileResponse } from './parse-iam-response.js';
 
 import type { AwsIamInstanceProfile } from './types.js';
 
-export interface GetInstanceProfilesInput {
-  path: string;
+export interface GetInstanceProfilesCurrentRegionInput {
+  pathPrefix: string;
 }
 
 export interface GetInstanceProfileInput {
   name: string;
 }
 
-export async function getInstanceProfiles({
-  path,
-}: GetInstanceProfilesInput): Promise<AwsIamInstanceProfile[]> {
+export async function getInstanceProfilesCurrentRegion({
+  pathPrefix,
+}: GetInstanceProfilesCurrentRegionInput): Promise<AwsIamInstanceProfile[]> {
+  const region = await iamClient.config.region();
+  const path = `${pathPrefix}/${region}/`;
+
   const { InstanceProfiles } = await iamClient.send(
     new ListInstanceProfilesCommand({
       PathPrefix: path,
diff --git a/packages/basti/src/bastion/bastion.ts b/packages/basti/src/bastion/bastion.ts
index 293348e..9a12f3b 100644
--- a/packages/basti/src/bastion/bastion.ts
+++ b/packages/basti/src/bastion/bastion.ts
@@ -18,8 +18,9 @@ export const BASTION_INSTANCE_NAME_PREFIX = 'basti-instance';
 export const BASTION_INSTANCE_ROLE_NAME_PREFIX = BASTION_INSTANCE_NAME_PREFIX;
 export const BASTION_INSTANCE_SECURITY_GROUP_NAME_PREFIX =
   BASTION_INSTANCE_NAME_PREFIX;
-export const BASTION_INSTANCE_ROLE_PATH = `/basti/`;
-export const BASTION_INSTANCE_PROFILE_PATH = BASTION_INSTANCE_ROLE_PATH;
+export const BASTION_INSTANCE_ROLE_PATH_PREFIX = `/basti`;
+export const BASTION_INSTANCE_PROFILE_PATH_PREFIX =
+  BASTION_INSTANCE_ROLE_PATH_PREFIX;
 
 export const BASTION_INSTANCE_ID_TAG_NAME = 'basti:id';
 export const BASTION_INSTANCE_IN_USE_TAG_NAME = 'basti:in-use';
diff --git a/packages/basti/src/bastion/create-bastion-role.ts b/packages/basti/src/bastion/create-bastion-role.ts
index 2c163fd..a1dbf1e 100644
--- a/packages/basti/src/bastion/create-bastion-role.ts
+++ b/packages/basti/src/bastion/create-bastion-role.ts
@@ -1,11 +1,11 @@
 import { createIamInlinePolicy } from '#src/aws/iam/create-iam-inline-policy.js';
-import { createIamRole } from '#src/aws/iam/create-iam-role.js';
+import { createIamRoleCurrentRegion } from '#src/aws/iam/create-iam-role.js';
 import type { AwsRole } from '#src/aws/iam/types.js';
 import type { AwsTag } from '#src/aws/tags/types.js';
 
 import {
   BASTION_INSTANCE_ROLE_NAME_PREFIX,
-  BASTION_INSTANCE_ROLE_PATH,
+  BASTION_INSTANCE_ROLE_PATH_PREFIX,
 } from './bastion.js';
 
 export interface CreateBastionRoleInput {
@@ -22,9 +22,9 @@ export async function createBastionRole({
   bastionId,
   tags,
 }: CreateBastionRoleInput): Promise<AwsRole> {
-  return await createIamRole({
+  return await createIamRoleCurrentRegion({
     name: `${BASTION_INSTANCE_ROLE_NAME_PREFIX}-${bastionId}`,
-    path: BASTION_INSTANCE_ROLE_PATH,
+    pathPrefix: BASTION_INSTANCE_ROLE_PATH_PREFIX,
     principalService: 'ec2.amazonaws.com',
     tags,
   });
diff --git a/packages/basti/src/bastion/create-bastion.ts b/packages/basti/src/bastion/create-bastion.ts
index 591cd1b..87037b1 100644
--- a/packages/basti/src/bastion/create-bastion.ts
+++ b/packages/basti/src/bastion/create-bastion.ts
@@ -20,7 +20,7 @@ import {
   BASTION_INSTANCE_ID_TAG_NAME,
   BASTION_INSTANCE_IN_USE_TAG_NAME,
   BASTION_INSTANCE_NAME_PREFIX,
-  BASTION_INSTANCE_PROFILE_PATH,
+  BASTION_INSTANCE_PROFILE_PATH_PREFIX,
   BASTION_INSTANCE_SECURITY_GROUP_NAME_PREFIX,
   BASTION_INSTANCE_DEFAULT_INSTANCE_TYPE,
 } from './bastion.js';
@@ -199,7 +199,7 @@ async function createBastionInstance(
       imageId: bastionImageId,
       instanceType: instanceType ?? BASTION_INSTANCE_DEFAULT_INSTANCE_TYPE,
       roleNames: [bastionRole.name],
-      profilePath: BASTION_INSTANCE_PROFILE_PATH,
+      profilePathPrefix: BASTION_INSTANCE_PROFILE_PATH_PREFIX,
       subnetId,
       assignPublicIp: true,
       securityGroupIds: [bastionSecurityGroup.id],
diff --git a/packages/basti/src/cleanup/get-resources-to-cleanup.ts b/packages/basti/src/cleanup/get-resources-to-cleanup.ts
index 2de2a05..0e08307 100644
--- a/packages/basti/src/cleanup/get-resources-to-cleanup.ts
+++ b/packages/basti/src/cleanup/get-resources-to-cleanup.ts
@@ -2,12 +2,12 @@ import { InstanceStateName } from '@aws-sdk/client-ec2';
 
 import { getEc2Instances } from '../aws/ec2/get-ec2-instances.js';
 import { getSecurityGroups } from '../aws/ec2/get-security-groups.js';
-import { getIamRoles } from '../aws/iam/get-iam-role.js';
-import { getInstanceProfiles } from '../aws/iam/get-instance-profiles.js';
+import { getIamRolesCurrentRegion } from '../aws/iam/get-iam-role.js';
+import { getInstanceProfilesCurrentRegion } from '../aws/iam/get-instance-profiles.js';
 import {
   BASTION_INSTANCE_ID_TAG_NAME,
-  BASTION_INSTANCE_PROFILE_PATH,
-  BASTION_INSTANCE_ROLE_PATH,
+  BASTION_INSTANCE_PROFILE_PATH_PREFIX,
+  BASTION_INSTANCE_ROLE_PATH_PREFIX,
   BASTION_INSTANCE_SECURITY_GROUP_NAME_PREFIX,
 } from '../bastion/bastion.js';
 import { ManagedResourceTypes } from '../common/resource-type.js';
@@ -96,13 +96,15 @@ async function getBastionInstances(): Promise<string[]> {
 }
 
 async function getBastionRoles(): Promise<string[]> {
-  const iamRoles = await getIamRoles({ path: BASTION_INSTANCE_ROLE_PATH });
+  const iamRoles = await getIamRolesCurrentRegion({
+    pathPrefix: BASTION_INSTANCE_ROLE_PATH_PREFIX,
+  });
   return iamRoles.map(role => role.name);
 }
 
 async function getBastionInstanceProfiles(): Promise<string[]> {
-  const instanceProfiles = await getInstanceProfiles({
-    path: BASTION_INSTANCE_PROFILE_PATH,
+  const instanceProfiles = await getInstanceProfilesCurrentRegion({
+    pathPrefix: BASTION_INSTANCE_PROFILE_PATH_PREFIX,
   });
   return instanceProfiles.map(profile => profile.name);
 }


Test patch:
diff --git a/packages/basti-cdk/test/cdk-apps/ts/package-lock.json b/packages/basti-cdk/test/cdk-apps/ts/package-lock.json
index 7b12436..09414c4 100644
--- a/packages/basti-cdk/test/cdk-apps/ts/package-lock.json
+++ b/packages/basti-cdk/test/cdk-apps/ts/package-lock.json
@@ -28,7 +28,6 @@
       }
     },
     "../../..": {
-      "name": "basti-cdk",
       "version": "1.0.2",
       "license": "MIT",
       "devDependencies": {