in src/network.ts [221:393]
constructor(scope: constructs.Construct, id: string, props: HyperledgerFabricNetworkProps) {
super(scope, id);
// Collect metadata on the stack
const partition = cdk.Stack.of(this).partition;
const region = cdk.Stack.of(this).region;
const account = cdk.Stack.of(this).account;
// Populate instance variables from input properties, using defaults if values not provided
this.networkName = props.networkName;
this.networkDescription = props.networkDescription ?? props.networkName;
this.memberName = props.memberName;
this.memberDescription = props.memberDescription ?? props.memberName;
this.frameworkVersion = props.frameworkVersion ?? FrameworkVersion.VERSION_1_4;
this.networkEdition = props.networkEdition ?? NetworkEdition.STANDARD;
this.proposalDurationInHours = props.proposalDurationInHours ?? 24;
this.thresholdPercentage = props.thresholdPercentage ?? 50;
this.thresholdComparator = props.thresholdComparator ?? ThresholdComparator.GREATER_THAN;
this.enableCaLogging = props.enableCaLogging ?? true;
// Ensure the parameters captured above are valid, so we don't
// need to wait until deployment time to discover an error
utilities.validateRegion(region);
if (!utilities.validateString(this.networkName, 1, 64)) {
throw new Error('Network name is invalid or not provided. It can be up to 64 characters long.');
}
if (!utilities.validateString(this.networkDescription, 0, 128)) {
throw new Error('Network description is invalid. It can be up to 128 characters long.');
}
if (!utilities.validateString(this.memberName, 1, 64, /^(?!-|[0-9])(?!.*-$)(?!.*?--)[a-zA-Z0-9-]+$/)) {
throw new Error('Member name is invalid or not provided. It can be up to 64 characters long, and can have alphanumeric characters and hyphen(s). It cannot start with a number, or start and end with a hyphen (-), or have two consecutive hyphens. The member name must also be unique across the network.');
}
if (!utilities.validateString(this.memberDescription, 0, 128)) {
throw new Error('Member description is invalid. It can be up to 128 characters long.');
}
if (!utilities.validateInteger(this.proposalDurationInHours, 1, 168)) {
throw new Error('Voting policy proposal duration must be between 1 and 168 hours.');
}
if (!utilities.validateInteger(this.thresholdPercentage, 0, 100)) {
throw new Error('Voting policy threshold percentage must be between 0 and 100.');
}
// Per the Managed Blockchain documentation, the admin password must be at least eight
// characters long and no more than 32 characters. It must contain at least one uppercase
// letter, one lowercase letter, and one digit. It cannot have a single quotation mark (‘),
// a double quotation marks (“), a forward slash(/), a backward slash(\), @, or a space;
// several other characters are exluded here to make the password easier to use in scripts
const passwordRequirements = {
passwordLength: 32,
requireEachIncludedType: true,
excludeCharacters: '\'"/\\@ &{}<>*|',
};
this.adminPasswordSecret = new secretsmanager.Secret(this, 'AdminPassword', { generateSecretString: passwordRequirements });
// The initially enrolled admin user credentials will be stored in these secrets
this.adminPrivateKeySecret = new secretsmanager.Secret(this, 'AdminPrivateKey');
this.adminSignedCertSecret = new secretsmanager.Secret(this, 'AdminSignedCert');
// Build out the Cloudformation construct for the network/member
const networkConfiguration = {
name: this.networkName,
description: this.networkDescription,
framework: 'HYPERLEDGER_FABRIC',
frameworkVersion: this.frameworkVersion,
networkFrameworkConfiguration: {
networkFabricConfiguration: {
edition: this.networkEdition,
},
},
votingPolicy: {
approvalThresholdPolicy: {
proposalDurationInHours: this.proposalDurationInHours,
thresholdPercentage: this.thresholdPercentage,
thresholdComparator: this.thresholdComparator,
},
},
};
const memberConfiguration = {
name: this.memberName,
description: this.memberDescription,
memberFrameworkConfiguration: {
memberFabricConfiguration: {
adminUsername: 'admin',
adminPassword: this.adminPasswordSecret.secretValue.toString(),
},
},
};
const network = new managedblockchain.CfnMember(this, 'Network', { networkConfiguration, memberConfiguration });
// Capture data included in the Cloudformation output in instance variables
this.networkId = network.getAtt('NetworkId').toString();
this.memberId = network.getAtt('MemberId').toString();
// Build out the associated node constructs
this.nodes = node.HyperledgerFabricNode.constructNodes(this, props.nodes);
// Due to a race condition in CDK custom resources (https://github.com/aws/aws-cdk/issues/18237),
// the necessary permissions for all SDK calls in the stack need to be added here, even though
// the calls in this construct don't need access to the nodes; this also means node constructs
// can't populate their outputs fully until later, which is annoying
const nodeIds = this.nodes.map(n => n.nodeId);
const nodeArns = nodeIds.map(i => `arn:${partition}:managedblockchain:${region}:${account}:nodes/${i}`);
const sdkCallPolicy = customresources.AwsCustomResourcePolicy.fromSdkCalls({
resources: [
`arn:${partition}:managedblockchain:${region}::networks/${this.networkId}`,
`arn:${partition}:managedblockchain:${region}:${account}:members/${this.memberId}`,
...nodeArns,
],
});
// Cloudformation doesn't include all the network and member attributes
// needed to use Hyperledger Fabric, so use SDK calls to fetch said data
const networkDataSdkCall = {
service: 'ManagedBlockchain',
action: 'getNetwork',
parameters: { NetworkId: this.networkId },
physicalResourceId: customresources.PhysicalResourceId.of('Id'),
};
const memberDataSdkCall = {
service: 'ManagedBlockchain',
action: 'getMember',
parameters: { NetworkId: this.networkId, MemberId: this.memberId },
physicalResourceId: customresources.PhysicalResourceId.of('Id'),
};
// Data items need fetching on creation and updating; nothing needs doing on deletion
const networkData = new customresources.AwsCustomResource(this, 'NetworkDataResource', {
policy: sdkCallPolicy,
onCreate: networkDataSdkCall,
onUpdate: networkDataSdkCall,
});
const memberData = new customresources.AwsCustomResource(this, 'MemberDataResource', {
policy: sdkCallPolicy,
onCreate: memberDataSdkCall,
onUpdate: memberDataSdkCall,
});
// Cloudformation doesn't include logging configuration
// so use SDK call to configure
const logPublishingConfiguration = {
Fabric: {
CaLogs: {
Cloudwatch: { Enabled: this.enableCaLogging },
},
},
};
const configureCaLogSdkCall = {
service: 'ManagedBlockchain',
action: 'updateMember',
parameters: { NetworkId: this.networkId, MemberId: this.memberId, LogPublishingConfiguration: logPublishingConfiguration },
physicalResourceId: customresources.PhysicalResourceId.of('Id'),
};
new customresources.AwsCustomResource(this, 'ConfigureCaLogResource', {
policy: sdkCallPolicy,
onCreate: configureCaLogSdkCall,
onUpdate: configureCaLogSdkCall,
});
// Grab items out of the above return values and stick them in output properties
this.vpcEndpointServiceName = networkData.getResponseField('Network.VpcEndpointServiceName');
this.ordererEndpoint = networkData.getResponseField('Network.FrameworkAttributes.Fabric.OrderingServiceEndpoint');
this.caEndpoint = memberData.getResponseField('Member.FrameworkAttributes.Fabric.CaEndpoint');
// As stated earlier, node constructs can't populate all their properties
// until after the above network and member SDK calls succeed; thus the
// function calls below where the fetches are split out and logging is configured
for (const n of this.nodes) {
n.configureLogging(sdkCallPolicy);
n.fetchData(sdkCallPolicy);
}
}