From cbaa0bcf2f05aebfdb39e0af562662902995d40c Mon Sep 17 00:00:00 2001 From: Andy Pickering Date: Mon, 25 Sep 2017 18:17:32 +0900 Subject: [PATCH] frontend: Add IAM Role inputs for AWS master and worker nodes --- installer/api/api.go | 1 + installer/api/handlers_aws.go | 23 +++++++++++++++++ installer/frontend/aws-actions.js | 1 + installer/frontend/aws-api.js | 2 +- installer/frontend/cluster-config.js | 7 ++++++ .../components/aws-cloud-credentials.jsx | 2 +- .../frontend/components/aws-define-nodes.jsx | 18 ++++++++++++- installer/frontend/components/etcd.jsx | 4 +-- .../frontend/components/make-node-form.jsx | 25 ++++++++++++++++++- installer/frontend/reducer.js | 1 + 10 files changed, 78 insertions(+), 6 deletions(-) diff --git a/installer/api/api.go b/installer/api/api.go index 59718e2438..4297d8a727 100644 --- a/installer/api/api.go +++ b/installer/api/api.go @@ -80,6 +80,7 @@ func New(config *Config) (http.Handler, error) { mux.Handle("/aws/vpcs", logRequests(httpHandler("POST", ctx, awsGetVPCsHandler))) mux.Handle("/aws/vpcs/subnets", logRequests(httpHandler("POST", ctx, awsGetVPCsSubnetsHandler))) mux.Handle("/aws/ssh-key-pairs", logRequests(httpHandler("POST", ctx, awsGetKeyPairsHandler))) + mux.Handle("/aws/iam-roles", logRequests(httpHandler("POST", ctx, awsGetIamRolesHandler))) mux.Handle("/aws/zones", logRequests(httpHandler("POST", ctx, awsGetZonesHandler))) mux.Handle("/aws/domain", logRequests(httpHandler("POST", ctx, awsGetDomainInfoHandler))) diff --git a/installer/api/handlers_aws.go b/installer/api/handlers_aws.go index ae3042989a..14181a2c33 100644 --- a/installer/api/handlers_aws.go +++ b/installer/api/handlers_aws.go @@ -15,6 +15,7 @@ import ( "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/route53" "github.com/aws/aws-sdk-go/service/route53domains" @@ -227,6 +228,28 @@ func awsGetKeyPairsHandler(w http.ResponseWriter, req *http.Request, _ *Context) return writeJSONResponse(w, req, http.StatusOK, response) } +// awsGetIamRolesHandler returns the list of IAM roles. +func awsGetIamRolesHandler(w http.ResponseWriter, req *http.Request, _ *Context) error { + awsSession, err := awsSessionFromRequest(req) + if err != nil { + return fromAWSErr(err) + } + + iamSvc := iam.New(awsSession) + resp, err := iamSvc.ListRoles(&iam.ListRolesInput{}) + if err != nil { + return fromAWSErr(err) + } + + roles := make([]string, len(resp.Roles)) + for i, role := range resp.Roles { + roles[i] = aws.StringValue(role.RoleName) + } + sort.Strings(roles) + + return writeJSONResponse(w, req, http.StatusOK, roles) +} + // awsGetZonesHandler returns the list of Route53 Hosted Zones. func awsGetZonesHandler(w http.ResponseWriter, req *http.Request, _ *Context) error { diff --git a/installer/frontend/aws-actions.js b/installer/frontend/aws-actions.js index dee019158a..75a6a42dc5 100644 --- a/installer/frontend/aws-actions.js +++ b/installer/frontend/aws-actions.js @@ -99,6 +99,7 @@ const createAction = (name, fn, shouldReject = false) => (body, creds, isNow) => export const getVpcs = createAction('availableVpcs', awsApis.getVpcs); export const getVpcSubnets = createAction('availableVpcSubnets', awsApis.getVpcSubnets); export const getSsh = createAction('availableSsh', awsApis.getSsh, true); +export const getIamRoles = createAction('availableIamRoles', awsApis.getIamRoles); export const getRegions = createAction('availableRegions', awsApis.getRegions, true); export const getZones = createAction('availableR53Zones', awsApis.getZones, true); export const getDomainInfo = createAction('domainInfo', awsApis.getDomainInfo); diff --git a/installer/frontend/aws-api.js b/installer/frontend/aws-api.js index 02e6632b32..c39db0dc69 100644 --- a/installer/frontend/aws-api.js +++ b/installer/frontend/aws-api.js @@ -45,9 +45,9 @@ const TFPostJSON = url => (body = {}, creds = {}, platform = 'aws') => { ); }; - export const getRegions = postJSON('/aws/regions'); export const getSsh = postJSON('/aws/ssh-key-pairs'); +export const getIamRoles = postJSON('/aws/iam-roles'); export const getVpcs = postJSON('/aws/vpcs'); export const getVpcSubnets = postJSON('/aws/vpcs/subnets'); export const getZones = postJSON('/aws/zones'); diff --git a/installer/frontend/cluster-config.js b/installer/frontend/cluster-config.js index f0083cb31e..60def5e4f0 100644 --- a/installer/frontend/cluster-config.js +++ b/installer/frontend/cluster-config.js @@ -61,6 +61,7 @@ export const ADMIN_PASSWORD2 = 'adminPassword2'; export const POD_CIDR = 'podCIDR'; export const SERVICE_CIDR = 'serviceCIDR'; +export const IAM_ROLE = 'iamRole'; export const NUMBER_OF_INSTANCES = 'numberOfInstances'; export const INSTANCE_TYPE = 'instanceType'; export const STORAGE_SIZE_IN_GIB = 'storageSizeInGiB'; @@ -70,6 +71,7 @@ export const STORAGE_IOPS = 'storageIOPS'; export const RETRY = 'retry'; // FORMS: +export const AWS_CREDS = 'AWSCreds'; export const AWS_ETCDS = 'aws_etcds'; export const AWS_VPC_FORM = 'aws_vpc'; export const AWS_CONTROLLERS = 'aws_controllers'; @@ -92,6 +94,9 @@ const EXTERNAL = 'external'; const PROVISIONED = 'provisioned'; export const ETCD_OPTIONS = { EXTERNAL, PROVISIONED }; +// String that would be an invalid IAM role name +export const IAM_ROLE_CREATE_OPTION = '%create%'; + export const toVPCSubnet = (region, subnets, deselected) => { const vpcSubnets = {}; _.each(subnets, (v, availabilityZone) => { @@ -229,10 +234,12 @@ export const toAWS_TF = (cc, FORMS) => { tectonic_aws_region: cc[AWS_REGION], tectonic_admin_email: cc[ADMIN_EMAIL], tectonic_aws_master_ec2_type: controllers[INSTANCE_TYPE], + tectonic_aws_master_iam_role_name: controllers[IAM_ROLE] === IAM_ROLE_CREATE_OPTION ? undefined : controllers[IAM_ROLE], tectonic_aws_master_root_volume_iops: controllers[STORAGE_TYPE] === 'io1' ? controllers[STORAGE_IOPS] : undefined, tectonic_aws_master_root_volume_size: controllers[STORAGE_SIZE_IN_GIB], tectonic_aws_master_root_volume_type: controllers[STORAGE_TYPE], tectonic_aws_worker_ec2_type: workers[INSTANCE_TYPE], + tectonic_aws_worker_iam_role_name: workers[IAM_ROLE] === IAM_ROLE_CREATE_OPTION ? undefined : workers[IAM_ROLE], tectonic_aws_worker_root_volume_iops: workers[STORAGE_TYPE] === 'io1' ? controllers[STORAGE_IOPS] : undefined, tectonic_aws_worker_root_volume_size: workers[STORAGE_SIZE_IN_GIB], tectonic_aws_worker_root_volume_type: workers[STORAGE_TYPE], diff --git a/installer/frontend/components/aws-cloud-credentials.jsx b/installer/frontend/components/aws-cloud-credentials.jsx index 648c4ec536..977478d291 100644 --- a/installer/frontend/components/aws-cloud-credentials.jsx +++ b/installer/frontend/components/aws-cloud-credentials.jsx @@ -14,6 +14,7 @@ import { TectonicGA } from '../tectonic-ga'; import { AWS_ACCESS_KEY_ID, AWS_CONTROLLER_SUBNETS, + AWS_CREDS, AWS_WORKER_SUBNETS, AWS_CONTROLLER_SUBNET_IDS, AWS_WORKER_SUBNET_IDS, @@ -44,7 +45,6 @@ const REGION_NAMES = { }; const { batchSetIn } = configActions; -const AWS_CREDS = 'AWSCreds'; const awsCredsForm = new Form(AWS_CREDS, [ new Field(STS_ENABLED, {default: false}), diff --git a/installer/frontend/components/aws-define-nodes.jsx b/installer/frontend/components/aws-define-nodes.jsx index 9ad09e2654..6cc389532d 100644 --- a/installer/frontend/components/aws-define-nodes.jsx +++ b/installer/frontend/components/aws-define-nodes.jsx @@ -5,6 +5,8 @@ import { connect } from 'react-redux'; import { AWS_CONTROLLERS, AWS_WORKERS, + IAM_ROLE, + IAM_ROLE_CREATE_OPTION, INSTANCE_TYPE, NUMBER_OF_INSTANCES, STORAGE_IOPS, @@ -40,13 +42,27 @@ const IOPs = connect( ); +const IamRoles = connect( + ({clusterConfig}) => ({roles: _.get(clusterConfig, ['extra', IAM_ROLE], [])}) +)( + ({roles, type}) => + + + + +); + const Errors = connect( ({clusterConfig}, {type}) => ({ error: _.get(clusterConfig, toError(type)) || _.get(clusterConfig, toAsyncError(type)), }) )(props => props.error ?
{props.error}
: ); -export const DefineNode = ({type, max}) =>
+export const DefineNode = ({type, max, withIamRole = true}) =>
+ {withIamRole && } diff --git a/installer/frontend/components/etcd.jsx b/installer/frontend/components/etcd.jsx index 7d33c8af0b..ff86ac9ee4 100644 --- a/installer/frontend/components/etcd.jsx +++ b/installer/frontend/components/etcd.jsx @@ -23,7 +23,7 @@ const fields = [ new Field(ETCD_OPTION, { default: ETCD_OPTIONS.PROVISIONED, }), - makeNodeForm(AWS_ETCDS, value => one2Nine(value) || validate.isOdd(value), { + makeNodeForm(AWS_ETCDS, false, value => one2Nine(value) || validate.isOdd(value), { dependencies: [ETCD_OPTION], ignoreWhen: cc => cc[ETCD_OPTION] !== ETCD_OPTIONS.PROVISIONED, }), @@ -107,7 +107,7 @@ export const Etcd = connect(({clusterConfig}) => ({ {isAWS && etcdOption === ETCD_OPTIONS.PROVISIONED &&
} {isAWS && etcdOption === ETCD_OPTIONS.PROVISIONED &&
- +
}
diff --git a/installer/frontend/components/make-node-form.jsx b/installer/frontend/components/make-node-form.jsx index ab61e4976f..1ae385db12 100644 --- a/installer/frontend/components/make-node-form.jsx +++ b/installer/frontend/components/make-node-form.jsx @@ -1,6 +1,10 @@ import _ from 'lodash'; +import * as awsActions from '../aws-actions'; import { + AWS_CREDS, + IAM_ROLE, + IAM_ROLE_CREATE_OPTION, INSTANCE_TYPE, NUMBER_OF_INSTANCES, STORAGE_IOPS, @@ -13,7 +17,18 @@ import { validate } from '../validate'; const toKey = (name, field) => `${name}-${field}`; -export const makeNodeForm = (name, instanceValidator = validate.int({min: 1, max: 999}), opts) => { +// Use this single dummy form / field to trigger loading the IAM roles list. Then IAM role fields can set this as their +// dependency, which avoids triggering a separate API request for each field. +new Form('DUMMY_NODE_FORM', [ + new Field(IAM_ROLE, { + default: 'DUMMY_VALUE', + name: IAM_ROLE, + dependencies: [AWS_CREDS], + getExtraStuff: (dispatch, isNow) => dispatch(awsActions.getIamRoles(null, null, isNow)), + }), +]); + +export const makeNodeForm = (name, withIamRole = true, instanceValidator = validate.int({min: 1, max: 999}), opts) => { const storageType = toKey(name, STORAGE_TYPE); // all fields must have a unique name! @@ -47,6 +62,14 @@ export const makeNodeForm = (name, instanceValidator = validate.int({min: 1, max }), ]; + if (withIamRole) { + fields.unshift(new Field(toKey(name, IAM_ROLE), { + default: IAM_ROLE_CREATE_OPTION, + name: IAM_ROLE, + dependencies: [IAM_ROLE], + })); + } + const validator = (data) => { const type = data[STORAGE_TYPE]; const size = data[STORAGE_SIZE_IN_GIB]; diff --git a/installer/frontend/reducer.js b/installer/frontend/reducer.js index 8c34c25ce1..89b8e6c46e 100644 --- a/installer/frontend/reducer.js +++ b/installer/frontend/reducer.js @@ -29,6 +29,7 @@ const DEFAULT_AWS = { availableVpcs: UNLOADED_AWS_VALUE, availableVpcSubnets: UNLOADED_AWS_VALUE, availableSsh: UNLOADED_AWS_VALUE, + availableIamRoles: UNLOADED_AWS_VALUE, availableRegions: UNLOADED_AWS_VALUE, availableKms: UNLOADED_AWS_VALUE, availableR53Zones: UNLOADED_AWS_VALUE,