Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aws-iam): add aws iam instance profile: fixes #1223 #1224

Closed
wants to merge 11 commits into from
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-dynamodb/lib/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ export class Table extends Construct {
private makeScalingRole(): iam.IRole {
// Use a Service Linked Role.
return iam.Role.import(this, 'ScalingRole', {
roleName: "DynamoDbAutoScalingRole",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert

roleArn: cdk.ArnUtils.fromComponents({
// https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-service-linked-roles.html
service: 'iam',
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-ecs/lib/base/base-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ export abstract class BaseService extends cdk.Construct
private makeAutoScalingRole(): iam.IRole {
// Use a Service Linked Role.
return iam.Role.import(this, 'ScalingRole', {
roleName: 'ECSServiceAutoScalingRole',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert

roleArn: cdk.ArnUtils.fromComponents({
service: 'iam',
resource: 'role/aws-service-role/ecs.application-autoscaling.amazonaws.com',
Expand Down
23 changes: 21 additions & 2 deletions packages/@aws-cdk/aws-iam/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ Managed policies can be attached using `xxx.attachManagedPolicy(arn)`:

### Features

* Policy name uniqueness is enforced. If two policies by the same name are attached to the same
* Policy name uniqueness is enforced. If two policies by the same name are attached to the same
principal, the attachment will fail.
* Policy names are not required - the CDK logical ID will be used and ensured to be unique.
* Policy names are not required - the CDK logical ID will be used and ensured to be unique.

### Instance Profile

[AWS::IAM::InstanceProfile](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-instanceprofile.html)
cmaurer marked this conversation as resolved.
Show resolved Hide resolved

```ts
import iam = require('@aws-cdk/aws-iam')

const testRole = new iam.Role(stack, 'TestRole', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com')
});

const instanceProfile = new iam.InstanceProfile(stack, 'InstanceProfile', {
instanceProfileName: "InstanceProfileName",
role: testRole,
path: "/"
});

```
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-iam/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export * from './policy';
export * from './user';
export * from './group';
export * from './lazy-role';
export * from './instance-profile';
export * from './instance-profile-ref';

// AWS::IAM CloudFormation Resources:
export * from './iam.generated';
110 changes: 110 additions & 0 deletions packages/@aws-cdk/aws-iam/lib/instance-profile-ref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import cdk = require('@aws-cdk/cdk');
import { PolicyStatement } from './policy-document';
import { IRole } from './role';

export interface InstanceProfileRefProps {

/**
* The path associated with this instance profile. For information about IAM Instance Profiles, see
* Friendly Names and Paths in IAM User Guide.
* @link http://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html#Identifiers_FriendlyNames
* @default / By default, AWS CloudFormation specifies '/' for the path.
*/
path: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't that be optional?


/**
* The name of an existing IAM role to associate with this instance profile.
* Currently, you can assign a maximum of one role to an instance profile.
* @default Role a default Role with ServicePrincipal(ec2.amazonaws.com).
*/
role: IRole;

/**
* The name of the instance profile that you want to create.
* This parameter allows (per its regex pattern) a string consisting of
* upper and lowercase alphanumeric characters with no spaces.
* You can also include any of the following characters: = , . @ -
* @default none instance profile name does not have a default value.
*/
instanceProfileName?: string;
cmaurer marked this conversation as resolved.
Show resolved Hide resolved

}

export abstract class InstanceProfileRef extends cdk.Construct {

/**
* Creates an IAM Instance Profile object which represents an
* instance profile not defined within this stack.
*
* `InstanceProfile.import(this, 'MyImportedInstanceProfile', {})`
*
* @param parent The parent construct
* @param name The name for the CloudFormation InstanceProfile Element. Note: this is not the same as the
* InstanceProfileName.
* @param ref A reference to an Instance Profile. Can be created manually (see example above) or
* obtained through a call to `instanceProfile.export()`
* @returns InstanceProfileRef
*/
public static import(parent: cdk.Construct, name: string, ref: InstanceProfileRefProps): InstanceProfileRef {
return new InstanceProfileRefImport(parent, name, ref);
}

/**
* Path for the Instance Profile
* @link http://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html#Identifiers_FriendlyNames
* @default / If a path is not provided, CloudFormation defaults the path to '/'.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it cfn that provides the default?

*/
public abstract readonly path: string;

/**
* The name of an existing IAM role to associate with this instance profile.
* Currently, you can assign a maximum of one role to an instance profile.
* @default Role a default Role with ServicePrincipal(ec2.amazonaws.com).
*/
public abstract readonly role: IRole;

/**
* The name of the instance profile that you want to create.
* This parameter allows (per its regex pattern) a string consisting of
* upper and lowercase alphanumeric characters with no spaces.
* You can also include any of the following characters: = , . @ -
* @default none instance profile name does not have a default value.
*/
public abstract readonly instanceProfileName?: string;

/**
* Adds a PolicyStatement to the Role associated with this InstanceProfile
* @param statement the statement to add
*/
public abstract addToRolePolicy(statement: PolicyStatement): void;

/**
* Exports this InstanceProfile
*/
public export(): InstanceProfileRefProps {
return {
path: this.path,
role: this.role,
instanceProfileName: this.instanceProfileName
};
}

}

class InstanceProfileRefImport extends InstanceProfileRef {
public readonly path: string;
public readonly role: IRole;
public readonly instanceProfileName?: string;

constructor(parent: cdk.Construct, name: string, props: InstanceProfileRefProps) {
super(parent, name);
this.path = props.path;
this.role = props.role;
this.instanceProfileName = props.instanceProfileName;
}

public addToRolePolicy(_statement: PolicyStatement) {
// FIXME: Add warning that we're ignoring this
}

}
73 changes: 73 additions & 0 deletions packages/@aws-cdk/aws-iam/lib/instance-profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Construct } from '@aws-cdk/cdk';
import { cloudformation } from './iam.generated';
import { InstanceProfileRef } from './instance-profile-ref';
import { PolicyStatement, ServicePrincipal } from './policy-document';
import { IRole, Role } from './role';
export interface InstanceProfileProps {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newline


/**
* The path associated with this instance profile. For information about IAM Instance Profiles, see
* Friendly Names and Paths in IAM User Guide.
* @link http://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html#Identifiers_FriendlyNames
* @default / By default, AWS CloudFormation specifies '/' for the path.
*/
path?: string;

/**
* The name of an existing IAM role to associate with this instance profile.
* Currently, you can assign a maximum of one role to an instance profile.
* @default Role a default Role with ServicePrincipal(ec2.amazonaws.com).
*/
role?: IRole;

/**
* The name of the instance profile that you want to create.
* This parameter allows (per its regex pattern) a string consisting of
* upper and lowercase alphanumeric characters with no spaces.
* You can also include any of the following characters: = , . @ -
* @default none instance profile name does not have a default value.
*/
instanceProfileName?: string;
cmaurer marked this conversation as resolved.
Show resolved Hide resolved

}

/**
* IAM Instance Profile
*
* Defines an IAM Instance Profile that can be used with IAM roles for EC2 instances.
* @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-instanceprofile.html
*/
export class InstanceProfile extends InstanceProfileRef {

public readonly path: string;
public readonly role: IRole;
public readonly instanceProfileName?: string;
cmaurer marked this conversation as resolved.
Show resolved Hide resolved

constructor(parent: Construct, name: string, props: InstanceProfileProps) {
super(parent, name);

this.role = props.role || new Role(this, 'EC2Role', {
assumedBy: new ServicePrincipal('ec2.amazonaws.com')
});

this.path = props.path || "/";
const resource = new cloudformation.InstanceProfileResource(this, 'Resource', {
roles: [ this.role.roleName ],
path: this.path,
instanceProfileName: props.instanceProfileName
});
this.instanceProfileName = resource.instanceProfileName;
}

/**
* Adds a PolicyStatement to the Role associated with this InstanceProfile
* @param statement the statement to add
*/
public addToRolePolicy(statement: PolicyStatement) {
if (!this.role) {
return;
}
this.role.addToPolicy(statement);
}

cmaurer marked this conversation as resolved.
Show resolved Hide resolved
}
7 changes: 7 additions & 0 deletions packages/@aws-cdk/aws-iam/lib/lazy-role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ export class LazyRole extends cdk.Construct implements IRole {
return this.instantiate().roleArn;
}

/**
* Returns the Name of this role.
*/
public get roleName(): string {
return this.instantiate().roleName;
}

/**
* Returns a Principal object representing the ARN of this role.
*/
Expand Down
44 changes: 41 additions & 3 deletions packages/@aws-cdk/aws-iam/lib/role.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Construct, IDependable } from '@aws-cdk/cdk';
import { ArnUtils, Construct, IDependable, Output, unresolved } from '@aws-cdk/cdk';
import { cloudformation } from './iam.generated';
import { IPrincipal, Policy } from './policy';
import { ArnPrincipal, PolicyDocument, PolicyPrincipal, PolicyStatement } from './policy-document';
Expand Down Expand Up @@ -182,6 +182,14 @@ export class Role extends Construct implements IRole {
this.attachedPolicies.attach(policy);
policy.attachToRole(this);
}

public export(): ImportedRoleProps {
return {
roleArn: new Output(this, 'RoleArn', { value: this.roleArn }).makeImportValue().toString(),
roleName: new Output(this, 'RoleName', { value: this.roleName }).makeImportValue().toString()
};
}

}

/**
Expand All @@ -192,6 +200,11 @@ export interface IRole extends IPrincipal, IDependable {
* Returns the ARN of this role.
*/
readonly roleArn: string;

/**
* Returns this role's Name.
*/
readonly roleName: string;
}

function createAssumeRolePolicy(principal: PolicyPrincipal) {
Expand All @@ -218,20 +231,45 @@ export interface ImportedRoleProps {
/**
* The role's ARN
*/
roleArn: string;
roleArn?: string;

/**
* The role's Name
*/
roleName?: string;
}

/**
* A role that already exists
*/
class ImportedRole extends Construct implements IRole {
public readonly roleArn: string;
public readonly roleName: string;
public readonly principal: PolicyPrincipal;
public readonly dependencyElements: IDependable[] = [];

constructor(parent: Construct, id: string, props: ImportedRoleProps) {
super(parent, id);
this.roleArn = props.roleArn;
if (props.roleArn) {
this.roleArn = props.roleArn;
} else {
if (!props.roleName) {
throw new Error('If "roleArn" is not specified, you must specify "roleName"');
}
this.roleArn = ArnUtils.fromComponents({
service: 'iam',
resource: 'role'
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is resourceName?

}

if (props.roleName) {
this.roleName = props.roleName;
} else {
if (unresolved(this.roleArn)) {
throw new Error('roleArn is a late-bound value, and therefore roleName is required');
}
this.roleName = this.roleArn.split('/').slice(1).join('/');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ArnUtils.parse

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eladb
this.roleName is required (which I am pretty sure is correct), but its optional on ArnComponents, so I am getting the typescript error 'type string | undefined is not assignable...'. Thats why I used the split/slice/join. So, I am not sure what I should do here if the name is not supplied. undefined seems wrong, '' seems wrong, and making up a name seems wrong. I am probably missing something small, here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nevermind, I think I figured it out.

}
this.principal = new ArnPrincipal(this.roleArn);
}

Expand Down
Loading