diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index ff305179..4054df42 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -6,44 +6,26 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout PR - uses: actions/checkout@v2 - - - name: Cache Maven packages - uses: actions/cache@v2 + uses: actions/checkout@v3 + - name: Setup Java + uses: actions/setup-java@v4 with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - # We use Corretto Java 11 for Lambda: https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html - # Currently amazon-corretto Java is not supported for GitHub actions on Ubuntu-latest. By default we're using Adopt JDK11 - # https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-README.md - # We may want to either switch to using an AL2 container (to give us corretto) or contribute a new JDK to the GitHub runners - # There is an open issue (as of 2021/06/30) requesting Corretto support: https://github.com/actions/setup-java/issues/68 - # - name: Set up JDK 11 - # uses: actions/setup-java@v2 - # with: - # java-version: '11' - # distribution: 'adopt' - + distribution: corretto + java-version: '21' + cache: maven - name: Maven Compile, Test, Install run: mvn install -Dcheckstyle.skip -Dspotbugs.skip - - name: Spotbugs Check run: mvn spotbugs:check - - name: Code Style Check run: mvn checkstyle:check - Build-WebClient: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - + - uses: actions/checkout@v3 - uses: actions/setup-node@v2 with: node-version: 16 - - name: Cache node modules uses: actions/cache@v2 env: @@ -56,7 +38,6 @@ jobs: ${{ runner.os }}-build-${{ env.cache-name }}- ${{ runner.os }}-build- ${{ runner.os }}- - # CI=false is required because GitHub hosted runners set CI=true, which causes Warnings to be treated as Errors when doing yarn build # this is a workaround to allow the build to succeed until we can get around to fixing the warnings generated # https://docs.github.com/en/actions/reference/environment-variables#default-environment-variables diff --git a/.gitignore b/.gitignore index 2ea6e4fc..44e0409e 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ node_modules/ # production /build +resources/api-docs/build npm-debug.log* yarn-debug.log* @@ -69,4 +70,4 @@ jmeter.log *.factorypath installer/*.log security-check/*.log -saas-boost-install*.log* +*install*.log* diff --git a/client/web/buildspec_no_post_build.yaml b/client/web/buildspec_no_post_build.yaml new file mode 100644 index 00000000..e8d1adc8 --- /dev/null +++ b/client/web/buildspec_no_post_build.yaml @@ -0,0 +1,17 @@ +version: 0.2 +phases: + pre_build: + commands: + - if [ "$REACT_APP_AWS_REGION" = "cn-northwest-1" ] || [ "$REACT_APP_AWS_REGION" = "cn-north-1" ]; then npm config set registry https://registry.npm.taobao.org; fi + - npm install --global yarn + - aws s3 cp s3://$SOURCE_BUCKET/${SOURCE_BUCKET_PREFIX}client/web/src.zip src.zip + - unzip src.zip + build: + commands: + - cd ./client/web + - yarn + - yarn build + - cd ../../ +cache: + paths: + - $CODEBUILD_SRC_DIR/client/web/node_modules/**/* diff --git a/client/web/package.json b/client/web/package.json index 4fba0fda..05df1a68 100644 --- a/client/web/package.json +++ b/client/web/package.json @@ -17,10 +17,6 @@ "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", - "amazon-cognito-auth-js": "^1.3.3", - "amazon-cognito-identity-js": "^3.2.7", - "aws-amplify": "^4.3.16", - "aws-amplify-react": "^5.1.9", "aws-sdk": "^2.649.0", "axios": "^0.21.1", "bootstrap": "^5.1.3", diff --git a/client/web/src/_nav.js b/client/web/src/_nav.js index 1004c13d..507a373d 100644 --- a/client/web/src/_nav.js +++ b/client/web/src/_nav.js @@ -17,11 +17,14 @@ import React from 'react' import CIcon from '@coreui/icons-react' import { cilHome, - cilLayers, cilPeople, - cilPowerStandby, - cilSpeedometer, + cilCog, + cilFactory, + cilBarChart, cilUserPlus, + cilGift, + cilShieldAlt, + cilCreditCard } from '@coreui/icons' import { CNavGroup, CNavItem } from '@coreui/react' import { BaseUserRoute } from './users' @@ -29,14 +32,17 @@ import { BaseUserRoute } from './users' const _nav = [ { component: CNavItem, - name: 'Summary', + name: 'Home', to: '/summary', icon: , }, { - component: CNavGroup, - name: 'Dashboard', - icon: , + //component: CNavGroup, + component: CNavItem, + name: 'Metrics', + icon: , + to: '/metrics', + /* items: [ { component: CNavItem, @@ -54,44 +60,62 @@ const _nav = [ to: '/dashboard/accesslogging', }, ], + */ }, { component: CNavItem, - name: 'Application', - to: '/application', - icon: , - badge: { - color: 'danger', - text: 'SETUP', - }, + name: 'Tiers', + to: '/tiers', + icon: , + //disabled: false, }, { component: CNavItem, - name: 'Onboarding', - to: '/onboarding', + name: 'Billing', + to: '/billing', + icon: , + //disabled: false, + }, + { + component: CNavItem, + name: 'Identity', + to: '/providers', icon: , - disabled: true, + //disabled: false, }, { component: CNavItem, name: 'Tenants', to: '/tenants', - icon: , - disabled: true, + icon: , + //disabled: true, }, { component: CNavItem, - name: 'Tiers', - to: '/tiers', - icon: , - disabled: false, + name: 'Application', + to: '/application', + icon: , + /* + badge: { + color: 'danger', + text: 'SETUP', + }, + */ + }, + { + component: CNavItem, + name: 'Onboarding', + to: '/onboarding', + icon: , + //disabled: true, }, { component: CNavItem, name: 'System Users', to: BaseUserRoute, - icon: , - }, + icon: , + } + // AppSidebar.js adds the last menu entry for redirect to api docs ] export default _nav diff --git a/client/web/src/components/AppContent.js b/client/web/src/components/AppContent.js index b7c52481..d8d85d22 100644 --- a/client/web/src/components/AppContent.js +++ b/client/web/src/components/AppContent.js @@ -44,7 +44,8 @@ const AppContent = () => { ) ) })} - + {/* */} + diff --git a/client/web/src/components/AppFooter.js b/client/web/src/components/AppFooter.js index 70bcbf3b..0ee68efc 100644 --- a/client/web/src/components/AppFooter.js +++ b/client/web/src/components/AppFooter.js @@ -26,7 +26,7 @@ const AppFooter = (props) => { const { children, ...attributes } = props const version = useSelector((state) => selectSettingsById(state, SETTINGS.VERSION)) const saasBoostEnvironment = useSelector((state) => - selectSettingsById(state, 'SAAS_BOOST_ENVIRONMENT'), + selectSettingsById(state, 'ENVIRONMENT'), ) const prettyVersion = (versionParameter) => { diff --git a/client/web/src/components/AppSidebar.js b/client/web/src/components/AppSidebar.js index 12e87420..f256ab2a 100644 --- a/client/web/src/components/AppSidebar.js +++ b/client/web/src/components/AppSidebar.js @@ -15,59 +15,74 @@ */ // Based on CoreUI Template https://github.com/coreui/coreui-free-react-admin-template // SPDX-LicenseIdentifier: MIT -import React, { useState } from 'react' +import React, {useState} from 'react' import PropTypes from 'prop-types' import { - CSidebar, - CSidebarBrand, - CSidebarNav, - CSidebarToggler, + CSidebar, + CSidebarBrand, + CSidebarNav, + CSidebarToggler, } from '@coreui/react' - -import { AppSidebarNav } from './AppSidebarNav' - +import {AppSidebarNav} from './AppSidebarNav' import SimpleBar from 'simplebar-react' import 'simplebar/dist/simplebar.min.css' +import appConfig from "../config/appConfig"; +import CIcon from "@coreui/icons-react"; +import {cilSchool} from "@coreui/icons"; + +const {apiUri} = appConfig const AppSidebar = (props) => { - const [sidebarNarrow, setSidebarNarrow] = useState(false) - const { navigation } = props - const getText = (isNarrow) => { - return ( - !isNarrow && ( - <> - + const [sidebarNarrow, setSidebarNarrow] = useState(false) + const {navigation} = props + const APIDocNavItem = () => { + const href = apiUri + '/docs/'; // Make sure you inlude the trailing / or SwaggerUI won't redirect properly + return ( +
  • + + + API Docs + +
  • + ) + } + const getText = (isNarrow) => { + return ( + !isNarrow && ( + <> + AWS -   - SaaS Boost - - ) +   + SaaS Boost + + ) + ) + } + return ( + + + logo + {getText(sidebarNarrow)} + + + + + + + + { + setSidebarNarrow(!sidebarNarrow) + }} + /> + ) - } - return ( - - - logo - {getText(sidebarNarrow)} - - - - - - - { - setSidebarNarrow(!sidebarNarrow) - }} - /> - - ) } AppSidebar.propTypes = { - navigation: PropTypes.array, + navigation: PropTypes.array, } -export default React.memo(AppSidebar) +export default React.memo(AppSidebar) \ No newline at end of file diff --git a/client/web/src/dashboard/DashboardComponent.js b/client/web/src/dashboard/DashboardComponent.js index 0c2d8a2e..18bd632f 100644 --- a/client/web/src/dashboard/DashboardComponent.js +++ b/client/web/src/dashboard/DashboardComponent.js @@ -87,7 +87,7 @@ export const DashboardComponent = (props) => { ) const saasBoostEnvironment = useSelector( - (state) => selectSettingsById(state, 'SAAS_BOOST_ENVIRONMENT')?.value + (state) => selectSettingsById(state, 'ENVIRONMENT')?.value ) const countActiveTenants = useSelector((state) => { diff --git a/client/web/src/identity/ProviderCreateContainer.js b/client/web/src/identity/ProviderCreateContainer.js new file mode 100644 index 00000000..d023491a --- /dev/null +++ b/client/web/src/identity/ProviderCreateContainer.js @@ -0,0 +1,109 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {PropTypes} from 'prop-types' +import React, {Component} from 'react' +import ProviderForm from './ProviderFormComponent' +import {connect} from 'react-redux' +import {withRouter} from 'react-router' +import identityAPI from './api' +import {dismissError, createProviderThunk} from './ducks' + +const mapDispatchToProps = { + createProviderThunk, + dismissError, +} + +const mapStateToProps = (state, props) => { + const {providerId} = props.match.params; + const {providers} = state; + const provider = !!providerId ? providers.entities[providerId] : undefined; + //console.log('mapStateToProps: ', provider); + + return { + providers: providers, + provider: provider + } +} + +class ProviderCreateContainer extends Component { + constructor(props) { + super(props) + this.state = {} + + this.saveProvider = this.saveProvider.bind(this) + this.handleError = this.handleError.bind(this) + } + + async saveProvider(values, {setSubmitting, resetForm}) { + const provider = this.props.provider; + const saveProvider = { + type: provider.type, + metadata: values.metadata + } + console.log('saveProvider: ', saveProvider); + try { + //const createdResponse = await createProviderThunk(saveProvider); + const createdResponse = await identityAPI.create(saveProvider); + if (!createdResponse.error) { + const {history} = this.props + history.goBack() + //history.push(`/providers/${createdResponse.payload.id}`) + } else { + setSubmitting(false) + resetForm({values}) + } + } catch (e) { + setSubmitting(false) + resetForm({values}) + } + } + + handleCancel = () => { + const {history} = this.props + history.goBack() + } + + handleError = () => { + const {dismissError} = this.props + dismissError() + } + + render() { + const {error} = this.props.providers + return ( + + ) + } +} + +ProviderCreateContainer.propTypes = { + createProviderThunk: PropTypes.func, + history: PropTypes.object, + providers: PropTypes.object, + dismissError: PropTypes.func, +} + +export const ProviderCreateContainerWithRouter = connect( + mapStateToProps, + mapDispatchToProps, +)(withRouter(ProviderCreateContainer)) + +export default ProviderCreateContainerWithRouter diff --git a/client/web/src/identity/ProviderFormComponent.js b/client/web/src/identity/ProviderFormComponent.js new file mode 100644 index 00000000..1748f844 --- /dev/null +++ b/client/web/src/identity/ProviderFormComponent.js @@ -0,0 +1,132 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {PropTypes} from 'prop-types' +import React from 'react' +import {Formik, Form} from 'formik' +import * as Yup from 'yup' +import {Row, Col, Card, Button, Alert} from 'react-bootstrap' +import {SaasBoostInput} from '../components/FormComponents' + +const initialProvider = { + id: null, + name: '', + metadata: { + assumedRole: '', + userPoolId: '', + } +} + +const ProviderForm = (props) => { + const { + handleSubmit, + handleCancel, + provider = initialProvider, + error, + dismissError, + } = props + + const showError = (error, dismissError) => { + if (!!error) { + return ( + + + dismissError()} + > +

    Error

    +

    {error}

    +
    + +
    + ) + } + return undefined + } + + return ( + + {(formik) => ( +
    + {provider.id && ( + + )} +
    + {showError(error, dismissError)} + + + + Configure Identity Provider + + + + + + + + + + + +
    +
    + )} +
    + ) +} + +ProviderForm.propTypes = { + handleSubmit: PropTypes.func, + handleCancel: PropTypes.func, + dismissError: PropTypes.func, + provider: PropTypes.object, + error: PropTypes.string, + config: PropTypes.object, +} + +export default ProviderForm diff --git a/client/web/src/identity/ProviderListComponent.js b/client/web/src/identity/ProviderListComponent.js new file mode 100644 index 00000000..98a0db0a --- /dev/null +++ b/client/web/src/identity/ProviderListComponent.js @@ -0,0 +1,148 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {PropTypes} from 'prop-types' +import React from 'react' +import { + Card, CardBody, CardHeader, CardGroup, CardFooter, + Input, + Label, + Form, FormGroup, + Button, + Row, + Col, + Alert, +} from 'reactstrap' +import {ReactComponent as Auth0Logo} from './svg/auth0.svg'; +import {ReactComponent as CognitoLogo} from './svg/cognito.svg'; +import {ReactComponent as KeyCloakLogo} from './svg/keycloak.svg'; + +ProviderListItem.propTypes = { + provider: PropTypes.object, + handleProviderClick: PropTypes.func, +} + +function ProviderListItem({provider, handleProviderClick}) { + return ( + + + +
    + { + handleProviderClick(provider.id) + }} + disabled={provider.type == 'COGNITO' ? false : true}/> + +
    +
    +
    + + {getLogo(provider.type)} + +
    + ); +} + +function showError(error, handleError) { + return ( + handleError()}> +

    Error

    +

    {error}

    +
    + ) +} + +ProviderList.propTypes = { + providers: PropTypes.array, + loading: PropTypes.string, + error: PropTypes.string, + handleProvisionProvider: PropTypes.func, + handleProviderClick: PropTypes.func, + handleCreateProvider: PropTypes.func, + handleRefresh: PropTypes.func, + handleError: PropTypes.func, +} + +const getLogo = (type) => { + let Component = CognitoLogo; + if (type === 'AUTH0') { + Component = Auth0Logo + } else if (type === 'KEYCLOAK') { + Component = KeyCloakLogo + } + return ; +}; + +function ProviderList({ + providers, + loading, + cancel, + error, + handleProviderClick, + handleCreateProvider, + handleRefresh, + handleError, + }) { + + return ( +
    + + {error && showError(error, handleError)} + + + + Identity Providers + + +
    + + {providers.map((provider) => ( + + ))} + +
    +
    + + + + +
    +
    + ) +} + +export default ProviderList diff --git a/client/web/src/identity/ProviderListContainer.js b/client/web/src/identity/ProviderListContainer.js new file mode 100644 index 00000000..f4e3ada4 --- /dev/null +++ b/client/web/src/identity/ProviderListContainer.js @@ -0,0 +1,80 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, {useEffect, Fragment} from 'react' +import {useDispatch, useSelector} from 'react-redux' + +import {fetchProvidersThunk, selectAllProviders, dismissError} from './ducks' +import ProviderListComponent from "./ProviderListComponent"; +import {useHistory} from 'react-router-dom' +import LoadingOverlay from '@ronchalant/react-loading-overlay' + + +export default function ProviderListContainer() { + const dispatch = useDispatch() + const history = useHistory() + const providers = useSelector(selectAllProviders) + const loading = useSelector((state) => state.tiers.loading) + const error = useSelector((state) => state.tiers.error) + let selectedProvider = null; + const handleProviderClick = (id) => { + selectedProvider = id; + //console.log('selectedProvider: ', selectedProvider); + } + const handleCreateProvider = () => { + history.push(`/providers/${selectedProvider}`); + } + + const handleRefresh = () => { + dispatch(fetchProvidersThunk()) + } + + const handleError = () => { + dispatch(dismissError()) + } + + useEffect(() => { + const fetchProviders = dispatch(fetchProvidersThunk()); + //console.log('fetchProviders: ', fetchProviders); + return () => { + if (fetchProviders.PromiseStatus === 'pending') { + console.log('pending....'); + fetchProviders.abort() + } + dispatch(dismissError()) + } + }, [dispatch]) //TODO: Follow up on the use of this dispatch function. + + return ( + + + + + + ) +} diff --git a/client/web/src/identity/api/index.js b/client/web/src/identity/api/index.js new file mode 100644 index 00000000..24664a9a --- /dev/null +++ b/client/web/src/identity/api/index.js @@ -0,0 +1,165 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import axios from 'axios' +import { fetchAccessToken, handleErrorResponse, handleErrorNoResponse } from '../../api' +import appConfig from '../../config/appConfig' +const { apiUri } = appConfig + +const apiServer = axios.create({ + baseURL: `${apiUri}/identity`, + headers: { + common: { + 'Content-Type': 'application/json', + }, + }, + mode: 'cors', +}) +const CancelToken = axios.CancelToken +const source = CancelToken.source() + +apiServer.interceptors.request.use(async (r) => { + //Obtain and pass along Authorization token + const authorizationToken = await fetchAccessToken() + r.headers.Authorization = "Bearer " + authorizationToken + + //Configure the AbortSignal + if (r.signal) { + r.signal.onabort = () => { + source.cancel() + } + } + r.cancelToken = source.token + + return r +}) + +//API Aborted class +class Aborted extends Error { + constructor(message, cause) { + super(message) + this.aborted = true + this.cause = cause + } +} +const identityAPI = { + fetchAll: async (ops) => { + const { signal } = ops + try { + const response = await apiServer.get('providers', { signal }) + return response.data + } catch (err) { + if (axios.isCancel(err)) { + console.log('API call cancelled') + throw new Aborted('Call aborted', err) + } else { + console.error(err) + throw Error('Unable to fetch providers') + } + } + }, + create: async (providerData) => { + //const { signal } = ops + try { + const authorizationToken = await fetchAccessToken(); + const response = await fetch(`${apiUri}/identity/`, { + method: 'POST', + mode: 'cors', + body: JSON.stringify({ + ...providerData, + }), + headers: { + 'Content-Type': 'application/json', + Authorization: "Bearer " + authorizationToken, + }, + }) + return Promise.resolve(response); + //return await handleErrorResponse(response) + } catch (err) { + console.error(err) + throw Error('Unable to create provider') + } + }, + update: async (providerData, ops) => { + const { signal } = ops + + try { + const authorizationToken = await fetchAccessToken() + const response = await fetch(`${apiUri}/providers/${providerData.id}`, { + method: 'PUT', + mode: 'cors', + body: JSON.stringify({ + ...providerData, + }), + headers: { + 'Content-Type': 'application/json', + Authorization: "Bearer " + authorizationToken, + }, + }) + console.log('provider api update response', response) + return await handleErrorResponse(response) + } catch (err) { + console.error(err) + throw Error('Unable to edit provider.') + } + }, + fetchProvider: async (providerId, ops) => { + const { signal } = ops + + try { + const authorizationToken = await fetchAccessToken() + const response = await fetch(`${apiUri}/providers/${providerId}`, { + headers: { + 'Content-Type': 'application/json', + Authorization: "Bearer " + authorizationToken, + }, + }) + return await handleErrorResponse(response) + } catch (err) { + console.error(err) + throw Error(`Unable to fetch provider: ${providerId}`) + } + }, + delete: async (values, ops) => { + const { signal } = ops + const { providerId, history } = values + + try { + const response = await apiServer.delete(`/${providerId}`, { signal }) + history.push('/providers') + return response.data + } catch (err) { + if (axios.isCancel(err)) { + throw new Aborted('Call aborted', err) + } else { + console.error(err) + throw Error(`Unable to delete provider ${providerId}`) + } + } + }, + /** + * Determines if err is from a Cancelled or Aborted request + * @param err + */ + isCancel: (err) => { + if (err.aborted && err.aborted === true) { + return true + } + return false + }, +} + +export default identityAPI diff --git a/client/web/src/identity/ducks/index.js b/client/web/src/identity/ducks/index.js new file mode 100644 index 00000000..f317058d --- /dev/null +++ b/client/web/src/identity/ducks/index.js @@ -0,0 +1,162 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createAsyncThunk, + createSlice, + createEntityAdapter, + createSelector, +} from '@reduxjs/toolkit' + +import {normalize, schema} from 'normalizr' +import identityAPI from '../api' +import React from "react"; + +// Define normalizr entity schemas +const providerEntity = new schema.Entity('providers') +const providerListSchema = [providerEntity] +const providersAdapter = createEntityAdapter() + +const getName = (type) => { + if (type === 'AUTH0') { + return 'Auth0'; + } else if (type === 'KEYCLOAK') { + return 'Keycloak'; + } else { + return 'Amazon Cognito'; + } +}; + +// Thunks +export const fetchProvidersThunk = createAsyncThunk('/providers/fetchAll', async (...[, thunkAPI]) => { + const {signal} = thunkAPI + try { + const response = await identityAPI.fetchAll({signal}); + console.log('fetchProvidersThunk: ', response); + let idFixed = response.map((provider) => { + const name = getName(provider.type); + return { + ...provider, + id: provider.type, + name: name + } + }); + const normalized = normalize(idFixed, providerListSchema); + return normalized.entities + } catch (err) { + if (identityAPI.isCancel(err)) { + return + } else { + console.error(err) + return thunkAPI.rejectWithValue(err.message) + } + } +}) + +export const createProviderThunk = createAsyncThunk('providers/create', + async (providerData, thunkAPI) => { + const {signal} = thunkAPI + console.log('createProviderThunk.....'); + try { + return await identityAPI.create(providerData, {signal}) + } catch (err) { + console.error(err) + return thunkAPI.rejectWithValue(err.message) + } + }, +) + +const rejectedReducer = (state, action) => { + //Handle when thunk was aborted + console.log('rejectedReducer: ', state, action); + if (action.meta.aborted) { + state.error = {} + } + if (action.error) { + state.error = action.payload + } + if (state.loading === 'pending') { + state.loading = 'idle' + } + return state +} + +const pendingReducer = (state, action) => { + state.error = null + if (state.loading === 'idle') { + state.loading = 'pending' + } + return state +} +const initialState = providersAdapter.getInitialState({ + loading: 'idle', + error: null, + detail: null, +}) +// Slices +const providersSlice = createSlice({ + name: 'providers', + initialState, + reducers: { + dismissError(state, error) { + //console.log('fetchProvidersThunk.dismissError: ', state); + state.error = null + return state + }, + }, + extraReducers: { + RESET: (state) => { + return initialState + }, + [fetchProvidersThunk.fulfilled]: (state, action) => { + //console.log('fetchProvidersThunk.fulfilled: ', state, action); + if (action.payload === undefined) { + state.loading = 'idle' + state.error = null + return state + } + // Add identities to state object + + providersAdapter.setAll(state, action.payload.providers ?? []) + + state.loading = 'idle' + state.error = null + return state + }, + [fetchProvidersThunk.pending]: pendingReducer, + [fetchProvidersThunk.rejected]: rejectedReducer, + + [createProviderThunk.fulfilled]: (state, action) => { + console.log('createProviderThunk.fulfilled: ', state); + //tiersAdapter.upsertOne(state, action.payload) + state.loading = 'idle' + state.error = null + return state + }, + [createProviderThunk.pending]: pendingReducer, + [createProviderThunk.rejected]: rejectedReducer, + + }, +}) + +const {actions, reducer} = providersSlice; + +export const {fetchAll, dismissError} = actions +export const {selectAll: selectAllProviders, selectById: selectProviderById} = + providersAdapter.getSelectors((state) => state.providers) + +export default reducer + \ No newline at end of file diff --git a/client/web/src/identity/index.js b/client/web/src/identity/index.js new file mode 100644 index 00000000..a9d6f204 --- /dev/null +++ b/client/web/src/identity/index.js @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react' + +const ProviderListContainer = React.lazy(() => import('./ProviderListContainer')) +const ProviderCreateContainer = React.lazy(() => import('./ProviderCreateContainer')) + +export const IdentityRoutes = [ + { + path: '/providers', + exact: true, + name: 'Providers', + component: ProviderListContainer + }, + { + path: `/providers/:providerId`, + exact: true, + name: 'Create Provider', + component: ProviderCreateContainer + } +] diff --git a/client/web/src/identity/svg/auth0.svg b/client/web/src/identity/svg/auth0.svg new file mode 100644 index 00000000..ebeae3e0 --- /dev/null +++ b/client/web/src/identity/svg/auth0.svg @@ -0,0 +1,14 @@ + + + + + + Canvas 1 + + Layer 1 + + + + + + diff --git a/client/web/src/identity/svg/cognito.svg b/client/web/src/identity/svg/cognito.svg new file mode 100644 index 00000000..8bc8b546 --- /dev/null +++ b/client/web/src/identity/svg/cognito.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + Canvas 1 + + Layer 1 + + Icon-Architecture/64/Arch_Amazon-Cognito_64 + + Icon-Architecture-BG/64/Security-Identity-Compliance + + Rectangle + + + + + Amazon-Cognito_Icon_64_Squid + + + + + + diff --git a/client/web/src/identity/svg/keycloak.svg b/client/web/src/identity/svg/keycloak.svg new file mode 100644 index 00000000..0e23f216 --- /dev/null +++ b/client/web/src/identity/svg/keycloak.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + Canvas 1 + + Layer 1 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/web/src/onboarding/OnboardingCreateContainer.js b/client/web/src/onboarding/OnboardingCreateContainer.js index 0d755f24..82a1c9d8 100644 --- a/client/web/src/onboarding/OnboardingCreateContainer.js +++ b/client/web/src/onboarding/OnboardingCreateContainer.js @@ -28,9 +28,10 @@ import { } from './ducks' import { useDispatch, useSelector } from 'react-redux' import { selectConfig } from '../settings/ducks' -import { selectAllPlans, fetchPlans, selectPlanLoading } from '../billing/ducks' +//import { selectAllPlans, fetchPlans, selectPlanLoading } from '../billing/ducks' import { saveToPresignedBucket } from '../settings/ducks' import { selectAllTiers } from '../tier/ducks' +import {transform} from "framer-motion"; export default function OnboardingCreateContainer() { const dispatch = useDispatch() @@ -39,12 +40,13 @@ export default function OnboardingCreateContainer() { const error = useSelector(selectError) const errorName = useSelector(selectErrorName) const loading = useSelector(selectLoading) - const plans = useSelector(selectAllPlans) - const plansLoading = useSelector(selectPlanLoading) + //const plans = useSelector(selectAllPlans) + //const plansLoading = useSelector(selectPlanLoading) const tiers = useSelector(selectAllTiers) const [file, setFile] = useState({}) + /* useEffect(() => { const fetchPlansThunk = dispatch(fetchPlans()) return () => { @@ -54,6 +56,7 @@ export default function OnboardingCreateContainer() { dispatch(dismissError()) } }, [dispatch]) + */ const nullBlankProps = (obj) => { const ret = { ...obj } @@ -65,16 +68,36 @@ export default function OnboardingCreateContainer() { }) return ret } - + const dataTransform = (data)=> { + const transform = { + "name": data.name, + "tier": data.tier, + "subdomain": data.subdomain, + "adminUsers": [ + { + "username": data.username, + "email": data.email, + "phoneNumber": data.phoneNumber, + "givenName": data.givenName, + "familyName": data.familyName + } + ] + }; + return transform; + }; const submitOnboardingRequestForm = async ( values, { resetForm, setSubmitting } ) => { - const { hasDomain, hasBilling, ...rest } = values - const valsToSend = nullBlankProps(rest) - let onboardingResponse + //const { hasDomain, hasBilling, ...rest } = values + const { hasDomain, ...rest } = values + const valsToSend = nullBlankProps(rest); + const data = dataTransform(valsToSend); + + let onboardingResponse; + console.log('data: ', JSON.stringify(data)); try { - onboardingResponse = await dispatch(createOnboarding(valsToSend)) + onboardingResponse = await dispatch(createOnboarding(data)) const presignedS3url = onboardingResponse.payload.zipFile if (presignedS3url && !!file && file.name) { await dispatch( @@ -105,8 +128,8 @@ export default function OnboardingCreateContainer() { return ( tier.defaultTier)[0].name || '', subdomain: '', - billingPlan: '', - hasBilling: hasBilling, + //billingPlan: '', + //hasBilling: hasBilling, hasDomain: hasDomain, } @@ -105,6 +107,7 @@ export default function OnboardingFormComponent(props) { ) } + /* const getBillingUi = (plans, hasBilling) => { const options = plans.map((plan) => { return ( @@ -131,6 +134,7 @@ export default function OnboardingFormComponent(props) { ) ) } + */ const getDomainUi = (domainName, hasDomain) => { return hasDomain ? ( @@ -172,11 +176,17 @@ export default function OnboardingFormComponent(props) { otherwise: Yup.string(), }) .max(25, 'Must be 25 characters or less.'), + username: Yup.string() + .max(100, 'Must be 100 characters or less.') + .required('Required'), + email: Yup.string().email() + .required('Required'), + /* billingPlan: Yup.string().when('hasBilling', { is: true, then: Yup.string().required('Billing plan is a required field'), otherwise: Yup.string(), - }), + }),*/ }) return ( @@ -205,7 +215,49 @@ export default function OnboardingFormComponent(props) { /> {getTiers(tiers, formik.values.tier)} {getDomainUi(domainName, hasDomain)} - {getBillingUi(billingPlans, hasBilling)} + {/* {getBillingUi(billingPlans, hasBilling)} */} + Admin User
    + + + + + + + + + + + { {!!error && showError(error, dismissError)} - + {/* Onboarding tenants requires an application image to be uploaded for each service. If you haven't done so, view the upload instructions for each service   here. - - + */} +
    @@ -200,7 +198,7 @@ TenantContainer.propTypes = { detail: PropTypes.object, loading: PropTypes.string, config: PropTypes.object, - plans: PropTypes.array, + //plans: PropTypes.array, } export const TenantContainerWithRouter = connect( diff --git a/client/web/src/tenant/api/index.js b/client/web/src/tenant/api/index.js index 18af710d..1e423786 100644 --- a/client/web/src/tenant/api/index.js +++ b/client/web/src/tenant/api/index.js @@ -55,25 +55,25 @@ class Aborted extends Error { } } const tenantAPI = { - fetchAll: async () => { - try { - const authorizationToken = await fetchAccessToken() - const response = await fetch(`${apiUri}/tenants/provisioned`, { - method: 'GET', - mode: 'cors', - headers: { - 'Content-Type': 'application/json', - Authorization: "Bearer " + authorizationToken, - }, - }) + // fetchAll: async () => { + // try { + // const authorizationToken = await fetchAccessToken() + // const response = await fetch(`${apiUri}/tenants/provisioned`, { + // method: 'GET', + // mode: 'cors', + // headers: { + // 'Content-Type': 'application/json', + // Authorization: "Bearer " + authorizationToken, + // }, + // }) - const responseJSON = await handleErrorResponse(response) - return responseJSON - } catch (err) { - console.error(err) - throw Error('Unable to fetch tenants') - } - }, + // const responseJSON = await handleErrorResponse(response) + // return responseJSON + // } catch (err) { + // console.error(err) + // throw Error('Unable to fetch tenants') + // } + // }, fetchAllAxios: async (ops) => { const { signal } = ops diff --git a/client/web/yarn.lock b/client/web/yarn.lock index 274ae772..24422c15 100644 --- a/client/web/yarn.lock +++ b/client/web/yarn.lock @@ -24,2109 +24,6 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@aws-amplify/analytics@5.2.31": - version "5.2.31" - resolved "https://registry.yarnpkg.com/@aws-amplify/analytics/-/analytics-5.2.31.tgz#8a8a786110c880a8d5de15353f884ccf1552c600" - integrity sha512-u2j5qZRTDGD7d1TpbKU3D7928VFJK602537TWDuUibUCQWafCDLzPj1IJCiC6UdZ1yShqEmexa02/cqtq+gbwg== - dependencies: - "@aws-amplify/cache" "4.0.66" - "@aws-amplify/core" "4.7.15" - "@aws-sdk/client-firehose" "3.6.1" - "@aws-sdk/client-kinesis" "3.6.1" - "@aws-sdk/client-personalize-events" "3.6.1" - "@aws-sdk/client-pinpoint" "3.6.1" - "@aws-sdk/util-utf8-browser" "3.6.1" - lodash "^4.17.20" - uuid "^3.2.1" - -"@aws-amplify/api-graphql@2.3.28": - version "2.3.28" - resolved "https://registry.yarnpkg.com/@aws-amplify/api-graphql/-/api-graphql-2.3.28.tgz#d0f2f75eb8cb4bfb9be1b6b3045599caa442d1c5" - integrity sha512-n/8dwUx2i9sojcAnK1vITamx/FODGPmDM08lTfZNwpTVJ1aXB/bcA9GitF7gWa4jstVACDgQAKmTAr7j2d0tGw== - dependencies: - "@aws-amplify/api-rest" "2.0.64" - "@aws-amplify/auth" "4.6.17" - "@aws-amplify/cache" "4.0.66" - "@aws-amplify/core" "4.7.15" - "@aws-amplify/pubsub" "4.5.14" - graphql "15.8.0" - uuid "^3.2.1" - zen-observable-ts "0.8.19" - -"@aws-amplify/api-rest@2.0.64": - version "2.0.64" - resolved "https://registry.yarnpkg.com/@aws-amplify/api-rest/-/api-rest-2.0.64.tgz#ccf7ffd2d2fb1b7194c07a0ddfd5dfd21aa6638d" - integrity sha512-hS+ImRnkyjGJj5gTet+Gd979Vnsp1lKTmiUngt3MXY/0b6CeUgMAACxnIQ628J00frvguUcgmOlZ502jeHsiKQ== - dependencies: - "@aws-amplify/core" "4.7.15" - axios "0.26.0" - -"@aws-amplify/api@4.0.64": - version "4.0.64" - resolved "https://registry.yarnpkg.com/@aws-amplify/api/-/api-4.0.64.tgz#20c9d89dce4092a8735ccd70b2f9a16071dfb964" - integrity sha512-nhg7Z+TQcEnLR5ZotxvKnJgqNwDtUYVBcNuktsHgUVszkKT/Oj2vC28xv8RufdljIofrXFsBDeERviwSpVXiFA== - dependencies: - "@aws-amplify/api-graphql" "2.3.28" - "@aws-amplify/api-rest" "2.0.64" - -"@aws-amplify/auth@4.6.17": - version "4.6.17" - resolved "https://registry.yarnpkg.com/@aws-amplify/auth/-/auth-4.6.17.tgz#5030e515467d2f9469eaa388a46370c7d5648772" - integrity sha512-KIWHP6qODphwtzyJ6jmcSQewH0a8dOOsQ35OtAALwmPNEaftGmoUjm8wMHAtyH3EwWv1iknhPwMVzmGylr+l1A== - dependencies: - "@aws-amplify/cache" "4.0.66" - "@aws-amplify/core" "4.7.15" - amazon-cognito-identity-js "5.2.14" - crypto-js "^4.1.1" - -"@aws-amplify/cache@4.0.66": - version "4.0.66" - resolved "https://registry.yarnpkg.com/@aws-amplify/cache/-/cache-4.0.66.tgz#16977bd9a3d7740c4b98101173e4bf31983a360f" - integrity sha512-dG5TSx1VbUMnIchqwoT+Pa5W+PdPTZVcXfg/4bjpv0HJ0s3LUeYMI93cpQGg0DlegKNvwV5Ib+B7UqXlWp/JEQ== - dependencies: - "@aws-amplify/core" "4.7.15" - -"@aws-amplify/core@4.7.15": - version "4.7.15" - resolved "https://registry.yarnpkg.com/@aws-amplify/core/-/core-4.7.15.tgz#b19c65c0ea8b2b52f53e15343a374bf2751c261d" - integrity sha512-upRxT6MN90pQZnJw2VwGdA7vHO6tGY1c3qLrXkq+x5XT45KrfGjbSSHmYBo7PkjWQYAUMGuX4KYwmPBuI58svg== - dependencies: - "@aws-crypto/sha256-js" "1.0.0-alpha.0" - "@aws-sdk/client-cloudwatch-logs" "3.6.1" - "@aws-sdk/client-cognito-identity" "3.6.1" - "@aws-sdk/credential-provider-cognito-identity" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/util-hex-encoding" "3.6.1" - universal-cookie "^4.0.4" - zen-observable-ts "0.8.19" - -"@aws-amplify/datastore@3.14.7": - version "3.14.7" - resolved "https://registry.yarnpkg.com/@aws-amplify/datastore/-/datastore-3.14.7.tgz#d4d683d6aa238179fd45ac899fd547c02c3a7b17" - integrity sha512-nzZHK0LXOsvmZzeBHL8VL/nrTm9dmBYdOWZOf7zSrbZBVaLEMim2l2os3DUx0+1u44XPr166QSF8OXLpl+56+w== - dependencies: - "@aws-amplify/api" "4.0.64" - "@aws-amplify/auth" "4.6.17" - "@aws-amplify/core" "4.7.15" - "@aws-amplify/pubsub" "4.5.14" - amazon-cognito-identity-js "5.2.14" - idb "5.0.6" - immer "9.0.6" - ulid "2.3.0" - uuid "3.3.2" - zen-observable-ts "0.8.19" - zen-push "0.2.1" - -"@aws-amplify/geo@1.3.27": - version "1.3.27" - resolved "https://registry.yarnpkg.com/@aws-amplify/geo/-/geo-1.3.27.tgz#b28b472022298a26070289b599f3dc83cdfb4102" - integrity sha512-7ytYD0M3EJxq9aiqJVQSRoXXUYf/bp7MU2Bb+UvKjqxOb29theJp3RJ7yJnqjxAV+6K7+jRpjoqH8lR+y3zkwQ== - dependencies: - "@aws-amplify/core" "4.7.15" - "@aws-sdk/client-location" "3.186.0" - "@turf/boolean-clockwise" "6.5.0" - camelcase-keys "6.2.2" - -"@aws-amplify/interactions@4.1.12": - version "4.1.12" - resolved "https://registry.yarnpkg.com/@aws-amplify/interactions/-/interactions-4.1.12.tgz#b4e953c335b2638890459f66a58b8484f914186f" - integrity sha512-MQjq4wdGuA7DNRywMrlwjbWZ/b5VFP0ASZdMYWSGVVkjPpHKR+/iCy/kkJvUFXIl8kEXHlFQTidv4RiNd4sYdQ== - dependencies: - "@aws-amplify/core" "4.7.15" - "@aws-sdk/client-lex-runtime-service" "3.186.0" - "@aws-sdk/client-lex-runtime-v2" "3.186.0" - base-64 "1.0.0" - fflate "0.7.3" - pako "2.0.4" - -"@aws-amplify/predictions@4.0.64": - version "4.0.64" - resolved "https://registry.yarnpkg.com/@aws-amplify/predictions/-/predictions-4.0.64.tgz#edfa0e916982d1a42a20484310d71c55e9b52cba" - integrity sha512-EcRwCqf0xFGoJLAzns7TIgKZxKZUlXubVPMTGIm9imVT/ZuF7ELX/YhIygzR33M+75rzLJxQcx5OOTFj6df/1Q== - dependencies: - "@aws-amplify/core" "4.7.15" - "@aws-amplify/storage" "4.5.17" - "@aws-sdk/client-comprehend" "3.6.1" - "@aws-sdk/client-polly" "3.6.1" - "@aws-sdk/client-rekognition" "3.6.1" - "@aws-sdk/client-textract" "3.6.1" - "@aws-sdk/client-translate" "3.6.1" - "@aws-sdk/eventstream-marshaller" "3.6.1" - "@aws-sdk/util-utf8-node" "3.6.1" - uuid "^3.2.1" - -"@aws-amplify/pubsub@4.5.14": - version "4.5.14" - resolved "https://registry.yarnpkg.com/@aws-amplify/pubsub/-/pubsub-4.5.14.tgz#dead3329e64ad0a69ce005de70703d185d6f05cc" - integrity sha512-WGR26nOMW2+DQE1DuWE4W9Ehx1RxmNmQN6Mq27DnKicLL0nMgyKT7OGBAHmQzVtsvMzFgUo/KcMBL3GltZ0M5g== - dependencies: - "@aws-amplify/auth" "4.6.17" - "@aws-amplify/cache" "4.0.66" - "@aws-amplify/core" "4.7.15" - graphql "15.8.0" - paho-mqtt "^1.1.0" - uuid "^3.2.1" - zen-observable-ts "0.8.19" - -"@aws-amplify/storage@4.5.17": - version "4.5.17" - resolved "https://registry.yarnpkg.com/@aws-amplify/storage/-/storage-4.5.17.tgz#9fd75d2e89fce220d0d1e1dd823416a77f2d2284" - integrity sha512-GZJvTdZ8zjlSfQ32x4EY56sOTafL843s6geqd8d/ybpJYZqEyBpfbcLZnsZFStAEERBKB4hCyCs/m+E2zZg/xg== - dependencies: - "@aws-amplify/core" "4.7.15" - "@aws-sdk/client-s3" "3.6.1" - "@aws-sdk/s3-request-presigner" "3.6.1" - "@aws-sdk/util-create-request" "3.6.1" - "@aws-sdk/util-format-url" "3.6.1" - axios "0.26.0" - events "^3.1.0" - -"@aws-amplify/ui@2.0.7": - version "2.0.7" - resolved "https://registry.yarnpkg.com/@aws-amplify/ui/-/ui-2.0.7.tgz#1d0b230306ca4fcd9c9ab5475f37d33c3eb83b37" - integrity sha512-tT7onRv+OCznFhUE2mKPpbGHHV+oODZk4VDX3lYNIfJ7LXv1hVtllQbPNJF5beNBRw9r6uotlXpeJrkph6v07A== - -"@aws-amplify/xr@3.0.64": - version "3.0.64" - resolved "https://registry.yarnpkg.com/@aws-amplify/xr/-/xr-3.0.64.tgz#a852c3d857373d34415d8050780a302ed3ac269c" - integrity sha512-YZJbHVEU9uN8yKHms2uIWyikUPEj4go6qL40vcIDwCv9LNyer2lP+yZ1Djn1FFhqUgLi5lK+yh4PUCoqPUWE8w== - dependencies: - "@aws-amplify/core" "4.7.15" - -"@aws-crypto/crc32@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-2.0.0.tgz#4ad432a3c03ec3087c5540ff6e41e6565d2dc153" - integrity sha512-TvE1r2CUueyXOuHdEigYjIZVesInd9KN+K/TFFNfkkxRThiNxO6i4ZqqAVMoEjAamZZ1AA8WXJkjCz7YShHPQA== - dependencies: - "@aws-crypto/util" "^2.0.0" - "@aws-sdk/types" "^3.1.0" - tslib "^1.11.1" - -"@aws-crypto/crc32@^1.0.0": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-1.2.2.tgz#4a758a596fa8cb3ab463f037a78c2ca4992fe81f" - integrity sha512-8K0b1672qbv05chSoKpwGZ3fhvVp28Fg3AVHVkEHFl2lTLChO7wD/hTyyo8ING7uc31uZRt7bNra/hA74Td7Tw== - dependencies: - "@aws-crypto/util" "^1.2.2" - "@aws-sdk/types" "^3.1.0" - tslib "^1.11.1" - -"@aws-crypto/ie11-detection@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/ie11-detection/-/ie11-detection-1.0.0.tgz#d3a6af29ba7f15458f79c41d1cd8cac3925e726a" - integrity sha512-kCKVhCF1oDxFYgQrxXmIrS5oaWulkvRcPz+QBDMsUr2crbF4VGgGT6+uQhSwJFdUAQ2A//Vq+uT83eJrkzFgXA== - dependencies: - tslib "^1.11.1" - -"@aws-crypto/ie11-detection@^2.0.0": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz#9c39f4a5558196636031a933ec1b4792de959d6a" - integrity sha512-5XDMQY98gMAf/WRTic5G++jfmS/VLM0rwpiOpaainKi4L0nqWMSB1SzsrEG5rjFZGYN6ZAefO+/Yta2dFM0kMw== - dependencies: - tslib "^1.11.1" - -"@aws-crypto/sha256-browser@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-2.0.0.tgz#741c9024df55ec59b51e5b1f5d806a4852699fb5" - integrity sha512-rYXOQ8BFOaqMEHJrLHul/25ckWH6GTJtdLSajhlqGMx0PmSueAuvboCuZCTqEKlxR8CQOwRarxYMZZSYlhRA1A== - dependencies: - "@aws-crypto/ie11-detection" "^2.0.0" - "@aws-crypto/sha256-js" "^2.0.0" - "@aws-crypto/supports-web-crypto" "^2.0.0" - "@aws-crypto/util" "^2.0.0" - "@aws-sdk/types" "^3.1.0" - "@aws-sdk/util-locate-window" "^3.0.0" - "@aws-sdk/util-utf8-browser" "^3.0.0" - tslib "^1.11.1" - -"@aws-crypto/sha256-browser@^1.0.0": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-1.2.2.tgz#004d806e3bbae130046c259ec3279a02d4a0b576" - integrity sha512-0tNR4kBtJp+9S0kis4+JLab3eg6QWuIeuPhzaYoYwNUXGBgsWIkktA2mnilet+EGWzf3n1zknJXC4X4DVyyXbg== - dependencies: - "@aws-crypto/ie11-detection" "^1.0.0" - "@aws-crypto/sha256-js" "^1.2.2" - "@aws-crypto/supports-web-crypto" "^1.0.0" - "@aws-crypto/util" "^1.2.2" - "@aws-sdk/types" "^3.1.0" - "@aws-sdk/util-locate-window" "^3.0.0" - tslib "^1.11.1" - -"@aws-crypto/sha256-js@1.0.0-alpha.0": - version "1.0.0-alpha.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-1.0.0-alpha.0.tgz#1146f6fa823001a9065ce60db5bf1afcc7c1cc3a" - integrity sha512-GidX2lccEtHZw8mXDKJQj6tea7qh3pAnsNSp1eZNxsN4MMu2OvSraPSqiB1EihsQkZBMg0IiZPpZHoACUX/QMQ== - dependencies: - "@aws-sdk/types" "^1.0.0-alpha.0" - "@aws-sdk/util-utf8-browser" "^1.0.0-alpha.0" - tslib "^1.9.3" - -"@aws-crypto/sha256-js@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-2.0.0.tgz#f1f936039bdebd0b9e2dd834d65afdc2aac4efcb" - integrity sha512-VZY+mCY4Nmrs5WGfitmNqXzaE873fcIZDu54cbaDaaamsaTOP1DBImV9F4pICc3EHjQXujyE8jig+PFCaew9ig== - dependencies: - "@aws-crypto/util" "^2.0.0" - "@aws-sdk/types" "^3.1.0" - tslib "^1.11.1" - -"@aws-crypto/sha256-js@^1.0.0", "@aws-crypto/sha256-js@^1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-1.2.2.tgz#02acd1a1fda92896fc5a28ec7c6e164644ea32fc" - integrity sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g== - dependencies: - "@aws-crypto/util" "^1.2.2" - "@aws-sdk/types" "^3.1.0" - tslib "^1.11.1" - -"@aws-crypto/sha256-js@^2.0.0": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-2.0.2.tgz#c81e5d378b8a74ff1671b58632779986e50f4c99" - integrity sha512-iXLdKH19qPmIC73fVCrHWCSYjN/sxaAvZ3jNNyw6FclmHyjLKg0f69WlC9KTnyElxCR5MO9SKaG00VwlJwyAkQ== - dependencies: - "@aws-crypto/util" "^2.0.2" - "@aws-sdk/types" "^3.110.0" - tslib "^1.11.1" - -"@aws-crypto/supports-web-crypto@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-1.0.0.tgz#c40901bc17ac1e875e248df16a2b47ad8bfd9a93" - integrity sha512-IHLfv+WmVH89EW4n6a5eE8/hUlz6qkWGMn/v4r5ZgzcXdTC5nolii2z3k46y01hWRiC2PPhOdeSLzMUCUMco7g== - dependencies: - tslib "^1.11.1" - -"@aws-crypto/supports-web-crypto@^2.0.0": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-2.0.2.tgz#9f02aafad8789cac9c0ab5faaebb1ab8aa841338" - integrity sha512-6mbSsLHwZ99CTOOswvCRP3C+VCWnzBf+1SnbWxzzJ9lR0mA0JnY2JEAhp8rqmTE0GPFy88rrM27ffgp62oErMQ== - dependencies: - tslib "^1.11.1" - -"@aws-crypto/util@^1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-1.2.2.tgz#b28f7897730eb6538b21c18bd4de22d0ea09003c" - integrity sha512-H8PjG5WJ4wz0UXAFXeJjWCW1vkvIJ3qUUD+rGRwJ2/hj+xT58Qle2MTql/2MGzkU+1JLAFuR6aJpLAjHwhmwwg== - dependencies: - "@aws-sdk/types" "^3.1.0" - "@aws-sdk/util-utf8-browser" "^3.0.0" - tslib "^1.11.1" - -"@aws-crypto/util@^2.0.0", "@aws-crypto/util@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-2.0.2.tgz#adf5ff5dfbc7713082f897f1d01e551ce0edb9c0" - integrity sha512-Lgu5v/0e/BcrZ5m/IWqzPUf3UYFTy/PpeED+uc9SWUR1iZQL8XXbGQg10UfllwwBryO3hFF5dizK+78aoXC1eA== - dependencies: - "@aws-sdk/types" "^3.110.0" - "@aws-sdk/util-utf8-browser" "^3.0.0" - tslib "^1.11.1" - -"@aws-sdk/abort-controller@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/abort-controller/-/abort-controller-3.186.0.tgz#dfaccd296d57136930582e1a19203d6cb60debc7" - integrity sha512-JFvvvtEcbYOvVRRXasi64Dd1VcOz5kJmPvtzsJ+HzMHvPbGGs/aopOJAZQJMJttzJmJwVTay0QL6yag9Kk8nYA== - dependencies: - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/abort-controller@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/abort-controller/-/abort-controller-3.6.1.tgz#75812875bbef6ad17e0e3a6d96aab9df636376f9" - integrity sha512-X81XkxX/2Tvv9YNcEto/rcQzPIdKJHFSnl9hBl/qkSdCFV/GaQ2XNWfKm5qFXMLlZNFS0Fn5CnBJ83qnBm47vg== - dependencies: - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/chunked-blob-reader-native@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/chunked-blob-reader-native/-/chunked-blob-reader-native-3.6.1.tgz#21c2c8773c3cd8403c2a953fd0e9e4f69c120214" - integrity sha512-vP6bc2v9h442Srmo7t2QcIbPjk5IqLSf4jGnKDAes8z+7eyjCtKugRP3lOM1fJCfGlPIsJGYnexxYdEGw008vA== - dependencies: - "@aws-sdk/util-base64-browser" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/chunked-blob-reader@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/chunked-blob-reader/-/chunked-blob-reader-3.6.1.tgz#63363025dcecc2f9dd47ae5c282d79c01b327d82" - integrity sha512-QBGUBoD8D5nsM/EKoc0rjpApa5NE5pQVzw1caE8sG00QMMPkCXWSB/gTVKVY0GOAhJFoA/VpVPQchIlZcOrBFg== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/client-cloudwatch-logs@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.6.1.tgz#5e8dba495a2ba9a901b0a1a2d53edef8bd452398" - integrity sha512-QOxIDnlVTpnwJ26Gap6RGz61cDLH6TKrIp30VqwdMeT1pCGy8mn9rWln6XA+ymkofHy/08RfpGp+VN4axwd4Lw== - dependencies: - "@aws-crypto/sha256-browser" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-sdk/config-resolver" "3.6.1" - "@aws-sdk/credential-provider-node" "3.6.1" - "@aws-sdk/fetch-http-handler" "3.6.1" - "@aws-sdk/hash-node" "3.6.1" - "@aws-sdk/invalid-dependency" "3.6.1" - "@aws-sdk/middleware-content-length" "3.6.1" - "@aws-sdk/middleware-host-header" "3.6.1" - "@aws-sdk/middleware-logger" "3.6.1" - "@aws-sdk/middleware-retry" "3.6.1" - "@aws-sdk/middleware-serde" "3.6.1" - "@aws-sdk/middleware-signing" "3.6.1" - "@aws-sdk/middleware-stack" "3.6.1" - "@aws-sdk/middleware-user-agent" "3.6.1" - "@aws-sdk/node-config-provider" "3.6.1" - "@aws-sdk/node-http-handler" "3.6.1" - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/smithy-client" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/url-parser" "3.6.1" - "@aws-sdk/url-parser-native" "3.6.1" - "@aws-sdk/util-base64-browser" "3.6.1" - "@aws-sdk/util-base64-node" "3.6.1" - "@aws-sdk/util-body-length-browser" "3.6.1" - "@aws-sdk/util-body-length-node" "3.6.1" - "@aws-sdk/util-user-agent-browser" "3.6.1" - "@aws-sdk/util-user-agent-node" "3.6.1" - "@aws-sdk/util-utf8-browser" "3.6.1" - "@aws-sdk/util-utf8-node" "3.6.1" - tslib "^2.0.0" - -"@aws-sdk/client-cognito-identity@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.6.1.tgz#36992a4fef7eff1f2b1dbee30850e30ebdfc15bb" - integrity sha512-FMj2GR9R5oCKb3/NI16GIvWeHcE4uX42fBAaQKPbjg2gALFDx9CcJYsdOtDP37V89GtPyZilLv6GJxrwJKzYGg== - dependencies: - "@aws-crypto/sha256-browser" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-sdk/config-resolver" "3.6.1" - "@aws-sdk/credential-provider-node" "3.6.1" - "@aws-sdk/fetch-http-handler" "3.6.1" - "@aws-sdk/hash-node" "3.6.1" - "@aws-sdk/invalid-dependency" "3.6.1" - "@aws-sdk/middleware-content-length" "3.6.1" - "@aws-sdk/middleware-host-header" "3.6.1" - "@aws-sdk/middleware-logger" "3.6.1" - "@aws-sdk/middleware-retry" "3.6.1" - "@aws-sdk/middleware-serde" "3.6.1" - "@aws-sdk/middleware-signing" "3.6.1" - "@aws-sdk/middleware-stack" "3.6.1" - "@aws-sdk/middleware-user-agent" "3.6.1" - "@aws-sdk/node-config-provider" "3.6.1" - "@aws-sdk/node-http-handler" "3.6.1" - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/smithy-client" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/url-parser" "3.6.1" - "@aws-sdk/url-parser-native" "3.6.1" - "@aws-sdk/util-base64-browser" "3.6.1" - "@aws-sdk/util-base64-node" "3.6.1" - "@aws-sdk/util-body-length-browser" "3.6.1" - "@aws-sdk/util-body-length-node" "3.6.1" - "@aws-sdk/util-user-agent-browser" "3.6.1" - "@aws-sdk/util-user-agent-node" "3.6.1" - "@aws-sdk/util-utf8-browser" "3.6.1" - "@aws-sdk/util-utf8-node" "3.6.1" - tslib "^2.0.0" - -"@aws-sdk/client-comprehend@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-comprehend/-/client-comprehend-3.6.1.tgz#d640d510b49feafa94ac252cdd7942cbe5537249" - integrity sha512-Y2ixlSTjjAp2HJhkUArtYqC/X+zG5Qqu3Bl+Ez22u4u4YnG8HsNFD6FE1axuWSdSa5AFtWTEt+Cz2Ghj/tDySA== - dependencies: - "@aws-crypto/sha256-browser" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-sdk/config-resolver" "3.6.1" - "@aws-sdk/credential-provider-node" "3.6.1" - "@aws-sdk/fetch-http-handler" "3.6.1" - "@aws-sdk/hash-node" "3.6.1" - "@aws-sdk/invalid-dependency" "3.6.1" - "@aws-sdk/middleware-content-length" "3.6.1" - "@aws-sdk/middleware-host-header" "3.6.1" - "@aws-sdk/middleware-logger" "3.6.1" - "@aws-sdk/middleware-retry" "3.6.1" - "@aws-sdk/middleware-serde" "3.6.1" - "@aws-sdk/middleware-signing" "3.6.1" - "@aws-sdk/middleware-stack" "3.6.1" - "@aws-sdk/middleware-user-agent" "3.6.1" - "@aws-sdk/node-config-provider" "3.6.1" - "@aws-sdk/node-http-handler" "3.6.1" - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/smithy-client" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/url-parser" "3.6.1" - "@aws-sdk/url-parser-native" "3.6.1" - "@aws-sdk/util-base64-browser" "3.6.1" - "@aws-sdk/util-base64-node" "3.6.1" - "@aws-sdk/util-body-length-browser" "3.6.1" - "@aws-sdk/util-body-length-node" "3.6.1" - "@aws-sdk/util-user-agent-browser" "3.6.1" - "@aws-sdk/util-user-agent-node" "3.6.1" - "@aws-sdk/util-utf8-browser" "3.6.1" - "@aws-sdk/util-utf8-node" "3.6.1" - tslib "^2.0.0" - uuid "^3.0.0" - -"@aws-sdk/client-firehose@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-firehose/-/client-firehose-3.6.1.tgz#87a8ef0c18267907b3ce712e6d3de8f36b0a7c7b" - integrity sha512-KhiKCm+cJmnRFuAEyO3DBpFVDNix1XcVikdxk2lvYbFWkM1oUZoBpudxaJ+fPf2W3stF3CXIAOP+TnGqSZCy9g== - dependencies: - "@aws-crypto/sha256-browser" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-sdk/config-resolver" "3.6.1" - "@aws-sdk/credential-provider-node" "3.6.1" - "@aws-sdk/fetch-http-handler" "3.6.1" - "@aws-sdk/hash-node" "3.6.1" - "@aws-sdk/invalid-dependency" "3.6.1" - "@aws-sdk/middleware-content-length" "3.6.1" - "@aws-sdk/middleware-host-header" "3.6.1" - "@aws-sdk/middleware-logger" "3.6.1" - "@aws-sdk/middleware-retry" "3.6.1" - "@aws-sdk/middleware-serde" "3.6.1" - "@aws-sdk/middleware-signing" "3.6.1" - "@aws-sdk/middleware-stack" "3.6.1" - "@aws-sdk/middleware-user-agent" "3.6.1" - "@aws-sdk/node-config-provider" "3.6.1" - "@aws-sdk/node-http-handler" "3.6.1" - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/smithy-client" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/url-parser" "3.6.1" - "@aws-sdk/url-parser-native" "3.6.1" - "@aws-sdk/util-base64-browser" "3.6.1" - "@aws-sdk/util-base64-node" "3.6.1" - "@aws-sdk/util-body-length-browser" "3.6.1" - "@aws-sdk/util-body-length-node" "3.6.1" - "@aws-sdk/util-user-agent-browser" "3.6.1" - "@aws-sdk/util-user-agent-node" "3.6.1" - "@aws-sdk/util-utf8-browser" "3.6.1" - "@aws-sdk/util-utf8-node" "3.6.1" - tslib "^2.0.0" - -"@aws-sdk/client-kinesis@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-kinesis/-/client-kinesis-3.6.1.tgz#48583cc854f9108bc8ff6168005d9a05b24bae31" - integrity sha512-Ygo+92LxHeUZmiyhiHT+k7hIOhJd6S7ckCEVUsQs2rfwe9bAygUY/3cCoZSqgWy7exFRRKsjhzStcyV6i6jrVQ== - dependencies: - "@aws-crypto/sha256-browser" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-sdk/config-resolver" "3.6.1" - "@aws-sdk/credential-provider-node" "3.6.1" - "@aws-sdk/eventstream-serde-browser" "3.6.1" - "@aws-sdk/eventstream-serde-config-resolver" "3.6.1" - "@aws-sdk/eventstream-serde-node" "3.6.1" - "@aws-sdk/fetch-http-handler" "3.6.1" - "@aws-sdk/hash-node" "3.6.1" - "@aws-sdk/invalid-dependency" "3.6.1" - "@aws-sdk/middleware-content-length" "3.6.1" - "@aws-sdk/middleware-host-header" "3.6.1" - "@aws-sdk/middleware-logger" "3.6.1" - "@aws-sdk/middleware-retry" "3.6.1" - "@aws-sdk/middleware-serde" "3.6.1" - "@aws-sdk/middleware-signing" "3.6.1" - "@aws-sdk/middleware-stack" "3.6.1" - "@aws-sdk/middleware-user-agent" "3.6.1" - "@aws-sdk/node-config-provider" "3.6.1" - "@aws-sdk/node-http-handler" "3.6.1" - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/smithy-client" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/url-parser" "3.6.1" - "@aws-sdk/url-parser-native" "3.6.1" - "@aws-sdk/util-base64-browser" "3.6.1" - "@aws-sdk/util-base64-node" "3.6.1" - "@aws-sdk/util-body-length-browser" "3.6.1" - "@aws-sdk/util-body-length-node" "3.6.1" - "@aws-sdk/util-user-agent-browser" "3.6.1" - "@aws-sdk/util-user-agent-node" "3.6.1" - "@aws-sdk/util-utf8-browser" "3.6.1" - "@aws-sdk/util-utf8-node" "3.6.1" - "@aws-sdk/util-waiter" "3.6.1" - tslib "^2.0.0" - -"@aws-sdk/client-lex-runtime-service@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-lex-runtime-service/-/client-lex-runtime-service-3.186.0.tgz#81deea7402cb76e7f2dce56bc5778e51909e1374" - integrity sha512-EgjQvFxa/o1urxpnWV2A/D0k4m763NqrPLuL074LR+cOkNxVl9W27aYL/tddDBmmDzzx4KcuRL6/n+UBZIheTg== - dependencies: - "@aws-crypto/sha256-browser" "2.0.0" - "@aws-crypto/sha256-js" "2.0.0" - "@aws-sdk/client-sts" "3.186.0" - "@aws-sdk/config-resolver" "3.186.0" - "@aws-sdk/credential-provider-node" "3.186.0" - "@aws-sdk/fetch-http-handler" "3.186.0" - "@aws-sdk/hash-node" "3.186.0" - "@aws-sdk/invalid-dependency" "3.186.0" - "@aws-sdk/middleware-content-length" "3.186.0" - "@aws-sdk/middleware-host-header" "3.186.0" - "@aws-sdk/middleware-logger" "3.186.0" - "@aws-sdk/middleware-recursion-detection" "3.186.0" - "@aws-sdk/middleware-retry" "3.186.0" - "@aws-sdk/middleware-serde" "3.186.0" - "@aws-sdk/middleware-signing" "3.186.0" - "@aws-sdk/middleware-stack" "3.186.0" - "@aws-sdk/middleware-user-agent" "3.186.0" - "@aws-sdk/node-config-provider" "3.186.0" - "@aws-sdk/node-http-handler" "3.186.0" - "@aws-sdk/protocol-http" "3.186.0" - "@aws-sdk/smithy-client" "3.186.0" - "@aws-sdk/types" "3.186.0" - "@aws-sdk/url-parser" "3.186.0" - "@aws-sdk/util-base64-browser" "3.186.0" - "@aws-sdk/util-base64-node" "3.186.0" - "@aws-sdk/util-body-length-browser" "3.186.0" - "@aws-sdk/util-body-length-node" "3.186.0" - "@aws-sdk/util-defaults-mode-browser" "3.186.0" - "@aws-sdk/util-defaults-mode-node" "3.186.0" - "@aws-sdk/util-user-agent-browser" "3.186.0" - "@aws-sdk/util-user-agent-node" "3.186.0" - "@aws-sdk/util-utf8-browser" "3.186.0" - "@aws-sdk/util-utf8-node" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/client-lex-runtime-v2@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-lex-runtime-v2/-/client-lex-runtime-v2-3.186.0.tgz#36d153f80e1dbc466c541fd70002d5f9846c9afa" - integrity sha512-oDN07yCWc9gsEYL44KSjPj8wdHHcf5Kti+w31fE7JHZqvRXxLsLx7G+kEcPmSTRk3Y4wDPXJozL6sDUAOAEb7A== - dependencies: - "@aws-crypto/sha256-browser" "2.0.0" - "@aws-crypto/sha256-js" "2.0.0" - "@aws-sdk/client-sts" "3.186.0" - "@aws-sdk/config-resolver" "3.186.0" - "@aws-sdk/credential-provider-node" "3.186.0" - "@aws-sdk/eventstream-handler-node" "3.186.0" - "@aws-sdk/eventstream-serde-browser" "3.186.0" - "@aws-sdk/eventstream-serde-config-resolver" "3.186.0" - "@aws-sdk/eventstream-serde-node" "3.186.0" - "@aws-sdk/fetch-http-handler" "3.186.0" - "@aws-sdk/hash-node" "3.186.0" - "@aws-sdk/invalid-dependency" "3.186.0" - "@aws-sdk/middleware-content-length" "3.186.0" - "@aws-sdk/middleware-eventstream" "3.186.0" - "@aws-sdk/middleware-host-header" "3.186.0" - "@aws-sdk/middleware-logger" "3.186.0" - "@aws-sdk/middleware-recursion-detection" "3.186.0" - "@aws-sdk/middleware-retry" "3.186.0" - "@aws-sdk/middleware-serde" "3.186.0" - "@aws-sdk/middleware-signing" "3.186.0" - "@aws-sdk/middleware-stack" "3.186.0" - "@aws-sdk/middleware-user-agent" "3.186.0" - "@aws-sdk/node-config-provider" "3.186.0" - "@aws-sdk/node-http-handler" "3.186.0" - "@aws-sdk/protocol-http" "3.186.0" - "@aws-sdk/smithy-client" "3.186.0" - "@aws-sdk/types" "3.186.0" - "@aws-sdk/url-parser" "3.186.0" - "@aws-sdk/util-base64-browser" "3.186.0" - "@aws-sdk/util-base64-node" "3.186.0" - "@aws-sdk/util-body-length-browser" "3.186.0" - "@aws-sdk/util-body-length-node" "3.186.0" - "@aws-sdk/util-defaults-mode-browser" "3.186.0" - "@aws-sdk/util-defaults-mode-node" "3.186.0" - "@aws-sdk/util-user-agent-browser" "3.186.0" - "@aws-sdk/util-user-agent-node" "3.186.0" - "@aws-sdk/util-utf8-browser" "3.186.0" - "@aws-sdk/util-utf8-node" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/client-location@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-location/-/client-location-3.186.0.tgz#0801433a1c3fb1fe534771daf67b5d57ffd474f4" - integrity sha512-RXT1Z7jgYrPEdD1VkErH9Wm+z6y7c/ua1Pu9VQ8weu9vtD15S8Qnyd1m4HS8ZPQUUM/gTxs/fL9+s53wRWpfGQ== - dependencies: - "@aws-crypto/sha256-browser" "2.0.0" - "@aws-crypto/sha256-js" "2.0.0" - "@aws-sdk/client-sts" "3.186.0" - "@aws-sdk/config-resolver" "3.186.0" - "@aws-sdk/credential-provider-node" "3.186.0" - "@aws-sdk/fetch-http-handler" "3.186.0" - "@aws-sdk/hash-node" "3.186.0" - "@aws-sdk/invalid-dependency" "3.186.0" - "@aws-sdk/middleware-content-length" "3.186.0" - "@aws-sdk/middleware-host-header" "3.186.0" - "@aws-sdk/middleware-logger" "3.186.0" - "@aws-sdk/middleware-recursion-detection" "3.186.0" - "@aws-sdk/middleware-retry" "3.186.0" - "@aws-sdk/middleware-serde" "3.186.0" - "@aws-sdk/middleware-signing" "3.186.0" - "@aws-sdk/middleware-stack" "3.186.0" - "@aws-sdk/middleware-user-agent" "3.186.0" - "@aws-sdk/node-config-provider" "3.186.0" - "@aws-sdk/node-http-handler" "3.186.0" - "@aws-sdk/protocol-http" "3.186.0" - "@aws-sdk/smithy-client" "3.186.0" - "@aws-sdk/types" "3.186.0" - "@aws-sdk/url-parser" "3.186.0" - "@aws-sdk/util-base64-browser" "3.186.0" - "@aws-sdk/util-base64-node" "3.186.0" - "@aws-sdk/util-body-length-browser" "3.186.0" - "@aws-sdk/util-body-length-node" "3.186.0" - "@aws-sdk/util-defaults-mode-browser" "3.186.0" - "@aws-sdk/util-defaults-mode-node" "3.186.0" - "@aws-sdk/util-user-agent-browser" "3.186.0" - "@aws-sdk/util-user-agent-node" "3.186.0" - "@aws-sdk/util-utf8-browser" "3.186.0" - "@aws-sdk/util-utf8-node" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/client-personalize-events@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-personalize-events/-/client-personalize-events-3.6.1.tgz#86942bb64108cfc2f6c31a8b54aab6fa7f7be00f" - integrity sha512-x9Jl/7emSQsB6GwBvjyw5BiSO26CnH4uvjNit6n54yNMtJ26q0+oIxkplnUDyjLTfLRe373c/z5/4dQQtDffkw== - dependencies: - "@aws-crypto/sha256-browser" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-sdk/config-resolver" "3.6.1" - "@aws-sdk/credential-provider-node" "3.6.1" - "@aws-sdk/fetch-http-handler" "3.6.1" - "@aws-sdk/hash-node" "3.6.1" - "@aws-sdk/invalid-dependency" "3.6.1" - "@aws-sdk/middleware-content-length" "3.6.1" - "@aws-sdk/middleware-host-header" "3.6.1" - "@aws-sdk/middleware-logger" "3.6.1" - "@aws-sdk/middleware-retry" "3.6.1" - "@aws-sdk/middleware-serde" "3.6.1" - "@aws-sdk/middleware-signing" "3.6.1" - "@aws-sdk/middleware-stack" "3.6.1" - "@aws-sdk/middleware-user-agent" "3.6.1" - "@aws-sdk/node-config-provider" "3.6.1" - "@aws-sdk/node-http-handler" "3.6.1" - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/smithy-client" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/url-parser" "3.6.1" - "@aws-sdk/url-parser-native" "3.6.1" - "@aws-sdk/util-base64-browser" "3.6.1" - "@aws-sdk/util-base64-node" "3.6.1" - "@aws-sdk/util-body-length-browser" "3.6.1" - "@aws-sdk/util-body-length-node" "3.6.1" - "@aws-sdk/util-user-agent-browser" "3.6.1" - "@aws-sdk/util-user-agent-node" "3.6.1" - "@aws-sdk/util-utf8-browser" "3.6.1" - "@aws-sdk/util-utf8-node" "3.6.1" - tslib "^2.0.0" - -"@aws-sdk/client-pinpoint@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-pinpoint/-/client-pinpoint-3.6.1.tgz#6b93f46475ae2667d77053be51ea62f52e330155" - integrity sha512-dueBedp91EKAHxcWLR3aNx/eUEdxdF9niEQTzOO2O4iJL2yvO2Hh7ZYiO7B3g7FuuICTpWSHd//Y9mGmSVLMCg== - dependencies: - "@aws-crypto/sha256-browser" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-sdk/config-resolver" "3.6.1" - "@aws-sdk/credential-provider-node" "3.6.1" - "@aws-sdk/fetch-http-handler" "3.6.1" - "@aws-sdk/hash-node" "3.6.1" - "@aws-sdk/invalid-dependency" "3.6.1" - "@aws-sdk/middleware-content-length" "3.6.1" - "@aws-sdk/middleware-host-header" "3.6.1" - "@aws-sdk/middleware-logger" "3.6.1" - "@aws-sdk/middleware-retry" "3.6.1" - "@aws-sdk/middleware-serde" "3.6.1" - "@aws-sdk/middleware-signing" "3.6.1" - "@aws-sdk/middleware-stack" "3.6.1" - "@aws-sdk/middleware-user-agent" "3.6.1" - "@aws-sdk/node-config-provider" "3.6.1" - "@aws-sdk/node-http-handler" "3.6.1" - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/smithy-client" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/url-parser" "3.6.1" - "@aws-sdk/url-parser-native" "3.6.1" - "@aws-sdk/util-base64-browser" "3.6.1" - "@aws-sdk/util-base64-node" "3.6.1" - "@aws-sdk/util-body-length-browser" "3.6.1" - "@aws-sdk/util-body-length-node" "3.6.1" - "@aws-sdk/util-user-agent-browser" "3.6.1" - "@aws-sdk/util-user-agent-node" "3.6.1" - "@aws-sdk/util-utf8-browser" "3.6.1" - "@aws-sdk/util-utf8-node" "3.6.1" - tslib "^2.0.0" - -"@aws-sdk/client-polly@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-polly/-/client-polly-3.6.1.tgz#869deb186e57fca29737bfa7af094599d7879841" - integrity sha512-y6fxVYndGS7z2KqHViPCqagBEOsZlxBUYUJZuD6WWTiQrI0Pwe5qG02oKJVaa5OmxE20QLf6bRBWj2rQpeF4IQ== - dependencies: - "@aws-crypto/sha256-browser" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-sdk/config-resolver" "3.6.1" - "@aws-sdk/credential-provider-node" "3.6.1" - "@aws-sdk/fetch-http-handler" "3.6.1" - "@aws-sdk/hash-node" "3.6.1" - "@aws-sdk/invalid-dependency" "3.6.1" - "@aws-sdk/middleware-content-length" "3.6.1" - "@aws-sdk/middleware-host-header" "3.6.1" - "@aws-sdk/middleware-logger" "3.6.1" - "@aws-sdk/middleware-retry" "3.6.1" - "@aws-sdk/middleware-serde" "3.6.1" - "@aws-sdk/middleware-signing" "3.6.1" - "@aws-sdk/middleware-stack" "3.6.1" - "@aws-sdk/middleware-user-agent" "3.6.1" - "@aws-sdk/node-config-provider" "3.6.1" - "@aws-sdk/node-http-handler" "3.6.1" - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/smithy-client" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/url-parser" "3.6.1" - "@aws-sdk/url-parser-native" "3.6.1" - "@aws-sdk/util-base64-browser" "3.6.1" - "@aws-sdk/util-base64-node" "3.6.1" - "@aws-sdk/util-body-length-browser" "3.6.1" - "@aws-sdk/util-body-length-node" "3.6.1" - "@aws-sdk/util-user-agent-browser" "3.6.1" - "@aws-sdk/util-user-agent-node" "3.6.1" - "@aws-sdk/util-utf8-browser" "3.6.1" - "@aws-sdk/util-utf8-node" "3.6.1" - tslib "^2.0.0" - -"@aws-sdk/client-rekognition@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-rekognition/-/client-rekognition-3.6.1.tgz#710ba6d4509a2caa417cf0702ba81b5b65aa73eb" - integrity sha512-Ia4FEog9RrI0IoDRbOJO6djwhVAAaEZutxEKrWbjrVz4bgib28L+V+yAio2SUneeirj8pNYXwBKPfoYOUqGHhA== - dependencies: - "@aws-crypto/sha256-browser" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-sdk/config-resolver" "3.6.1" - "@aws-sdk/credential-provider-node" "3.6.1" - "@aws-sdk/fetch-http-handler" "3.6.1" - "@aws-sdk/hash-node" "3.6.1" - "@aws-sdk/invalid-dependency" "3.6.1" - "@aws-sdk/middleware-content-length" "3.6.1" - "@aws-sdk/middleware-host-header" "3.6.1" - "@aws-sdk/middleware-logger" "3.6.1" - "@aws-sdk/middleware-retry" "3.6.1" - "@aws-sdk/middleware-serde" "3.6.1" - "@aws-sdk/middleware-signing" "3.6.1" - "@aws-sdk/middleware-stack" "3.6.1" - "@aws-sdk/middleware-user-agent" "3.6.1" - "@aws-sdk/node-config-provider" "3.6.1" - "@aws-sdk/node-http-handler" "3.6.1" - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/smithy-client" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/url-parser" "3.6.1" - "@aws-sdk/url-parser-native" "3.6.1" - "@aws-sdk/util-base64-browser" "3.6.1" - "@aws-sdk/util-base64-node" "3.6.1" - "@aws-sdk/util-body-length-browser" "3.6.1" - "@aws-sdk/util-body-length-node" "3.6.1" - "@aws-sdk/util-user-agent-browser" "3.6.1" - "@aws-sdk/util-user-agent-node" "3.6.1" - "@aws-sdk/util-utf8-browser" "3.6.1" - "@aws-sdk/util-utf8-node" "3.6.1" - "@aws-sdk/util-waiter" "3.6.1" - tslib "^2.0.0" - -"@aws-sdk/client-s3@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.6.1.tgz#aab1e0e92b353d9d51152d9347b7e1809f3593d0" - integrity sha512-59cTmZj92iwgNoAeJirK5sZNQNXLc/oI3luqrEHRNLuOh70bjdgad70T0a5k2Ysd/v/QNamqJxnCJMPuX1bhgw== - dependencies: - "@aws-crypto/sha256-browser" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-sdk/config-resolver" "3.6.1" - "@aws-sdk/credential-provider-node" "3.6.1" - "@aws-sdk/eventstream-serde-browser" "3.6.1" - "@aws-sdk/eventstream-serde-config-resolver" "3.6.1" - "@aws-sdk/eventstream-serde-node" "3.6.1" - "@aws-sdk/fetch-http-handler" "3.6.1" - "@aws-sdk/hash-blob-browser" "3.6.1" - "@aws-sdk/hash-node" "3.6.1" - "@aws-sdk/hash-stream-node" "3.6.1" - "@aws-sdk/invalid-dependency" "3.6.1" - "@aws-sdk/md5-js" "3.6.1" - "@aws-sdk/middleware-apply-body-checksum" "3.6.1" - "@aws-sdk/middleware-bucket-endpoint" "3.6.1" - "@aws-sdk/middleware-content-length" "3.6.1" - "@aws-sdk/middleware-expect-continue" "3.6.1" - "@aws-sdk/middleware-host-header" "3.6.1" - "@aws-sdk/middleware-location-constraint" "3.6.1" - "@aws-sdk/middleware-logger" "3.6.1" - "@aws-sdk/middleware-retry" "3.6.1" - "@aws-sdk/middleware-sdk-s3" "3.6.1" - "@aws-sdk/middleware-serde" "3.6.1" - "@aws-sdk/middleware-signing" "3.6.1" - "@aws-sdk/middleware-ssec" "3.6.1" - "@aws-sdk/middleware-stack" "3.6.1" - "@aws-sdk/middleware-user-agent" "3.6.1" - "@aws-sdk/node-config-provider" "3.6.1" - "@aws-sdk/node-http-handler" "3.6.1" - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/smithy-client" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/url-parser" "3.6.1" - "@aws-sdk/url-parser-native" "3.6.1" - "@aws-sdk/util-base64-browser" "3.6.1" - "@aws-sdk/util-base64-node" "3.6.1" - "@aws-sdk/util-body-length-browser" "3.6.1" - "@aws-sdk/util-body-length-node" "3.6.1" - "@aws-sdk/util-user-agent-browser" "3.6.1" - "@aws-sdk/util-user-agent-node" "3.6.1" - "@aws-sdk/util-utf8-browser" "3.6.1" - "@aws-sdk/util-utf8-node" "3.6.1" - "@aws-sdk/util-waiter" "3.6.1" - "@aws-sdk/xml-builder" "3.6.1" - fast-xml-parser "^3.16.0" - tslib "^2.0.0" - -"@aws-sdk/client-sso@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.186.0.tgz#233bdd1312dbf88ef9452f8a62c3c3f1ac580330" - integrity sha512-qwLPomqq+fjvp42izzEpBEtGL2+dIlWH5pUCteV55hTEwHgo+m9LJPIrMWkPeoMBzqbNiu5n6+zihnwYlCIlEA== - dependencies: - "@aws-crypto/sha256-browser" "2.0.0" - "@aws-crypto/sha256-js" "2.0.0" - "@aws-sdk/config-resolver" "3.186.0" - "@aws-sdk/fetch-http-handler" "3.186.0" - "@aws-sdk/hash-node" "3.186.0" - "@aws-sdk/invalid-dependency" "3.186.0" - "@aws-sdk/middleware-content-length" "3.186.0" - "@aws-sdk/middleware-host-header" "3.186.0" - "@aws-sdk/middleware-logger" "3.186.0" - "@aws-sdk/middleware-recursion-detection" "3.186.0" - "@aws-sdk/middleware-retry" "3.186.0" - "@aws-sdk/middleware-serde" "3.186.0" - "@aws-sdk/middleware-stack" "3.186.0" - "@aws-sdk/middleware-user-agent" "3.186.0" - "@aws-sdk/node-config-provider" "3.186.0" - "@aws-sdk/node-http-handler" "3.186.0" - "@aws-sdk/protocol-http" "3.186.0" - "@aws-sdk/smithy-client" "3.186.0" - "@aws-sdk/types" "3.186.0" - "@aws-sdk/url-parser" "3.186.0" - "@aws-sdk/util-base64-browser" "3.186.0" - "@aws-sdk/util-base64-node" "3.186.0" - "@aws-sdk/util-body-length-browser" "3.186.0" - "@aws-sdk/util-body-length-node" "3.186.0" - "@aws-sdk/util-defaults-mode-browser" "3.186.0" - "@aws-sdk/util-defaults-mode-node" "3.186.0" - "@aws-sdk/util-user-agent-browser" "3.186.0" - "@aws-sdk/util-user-agent-node" "3.186.0" - "@aws-sdk/util-utf8-browser" "3.186.0" - "@aws-sdk/util-utf8-node" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/client-sts@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.186.0.tgz#12514601b0b01f892ddb11d8a2ab4bee1b03cbf1" - integrity sha512-lyAPI6YmIWWYZHQ9fBZ7QgXjGMTtktL5fk8kOcZ98ja+8Vu0STH1/u837uxqvZta8/k0wijunIL3jWUhjsNRcg== - dependencies: - "@aws-crypto/sha256-browser" "2.0.0" - "@aws-crypto/sha256-js" "2.0.0" - "@aws-sdk/config-resolver" "3.186.0" - "@aws-sdk/credential-provider-node" "3.186.0" - "@aws-sdk/fetch-http-handler" "3.186.0" - "@aws-sdk/hash-node" "3.186.0" - "@aws-sdk/invalid-dependency" "3.186.0" - "@aws-sdk/middleware-content-length" "3.186.0" - "@aws-sdk/middleware-host-header" "3.186.0" - "@aws-sdk/middleware-logger" "3.186.0" - "@aws-sdk/middleware-recursion-detection" "3.186.0" - "@aws-sdk/middleware-retry" "3.186.0" - "@aws-sdk/middleware-sdk-sts" "3.186.0" - "@aws-sdk/middleware-serde" "3.186.0" - "@aws-sdk/middleware-signing" "3.186.0" - "@aws-sdk/middleware-stack" "3.186.0" - "@aws-sdk/middleware-user-agent" "3.186.0" - "@aws-sdk/node-config-provider" "3.186.0" - "@aws-sdk/node-http-handler" "3.186.0" - "@aws-sdk/protocol-http" "3.186.0" - "@aws-sdk/smithy-client" "3.186.0" - "@aws-sdk/types" "3.186.0" - "@aws-sdk/url-parser" "3.186.0" - "@aws-sdk/util-base64-browser" "3.186.0" - "@aws-sdk/util-base64-node" "3.186.0" - "@aws-sdk/util-body-length-browser" "3.186.0" - "@aws-sdk/util-body-length-node" "3.186.0" - "@aws-sdk/util-defaults-mode-browser" "3.186.0" - "@aws-sdk/util-defaults-mode-node" "3.186.0" - "@aws-sdk/util-user-agent-browser" "3.186.0" - "@aws-sdk/util-user-agent-node" "3.186.0" - "@aws-sdk/util-utf8-browser" "3.186.0" - "@aws-sdk/util-utf8-node" "3.186.0" - entities "2.2.0" - fast-xml-parser "3.19.0" - tslib "^2.3.1" - -"@aws-sdk/client-textract@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-textract/-/client-textract-3.6.1.tgz#b8972f53f0353222b4c052adc784291e602be6aa" - integrity sha512-nLrBzWDt3ToiGVFF4lW7a/eZpI2zjdvu7lwmOWyXX8iiPzhBVVEfd5oOorRyJYBsGMslp4sqV8TBkU5Ld/a97Q== - dependencies: - "@aws-crypto/sha256-browser" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-sdk/config-resolver" "3.6.1" - "@aws-sdk/credential-provider-node" "3.6.1" - "@aws-sdk/fetch-http-handler" "3.6.1" - "@aws-sdk/hash-node" "3.6.1" - "@aws-sdk/invalid-dependency" "3.6.1" - "@aws-sdk/middleware-content-length" "3.6.1" - "@aws-sdk/middleware-host-header" "3.6.1" - "@aws-sdk/middleware-logger" "3.6.1" - "@aws-sdk/middleware-retry" "3.6.1" - "@aws-sdk/middleware-serde" "3.6.1" - "@aws-sdk/middleware-signing" "3.6.1" - "@aws-sdk/middleware-stack" "3.6.1" - "@aws-sdk/middleware-user-agent" "3.6.1" - "@aws-sdk/node-config-provider" "3.6.1" - "@aws-sdk/node-http-handler" "3.6.1" - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/smithy-client" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/url-parser" "3.6.1" - "@aws-sdk/url-parser-native" "3.6.1" - "@aws-sdk/util-base64-browser" "3.6.1" - "@aws-sdk/util-base64-node" "3.6.1" - "@aws-sdk/util-body-length-browser" "3.6.1" - "@aws-sdk/util-body-length-node" "3.6.1" - "@aws-sdk/util-user-agent-browser" "3.6.1" - "@aws-sdk/util-user-agent-node" "3.6.1" - "@aws-sdk/util-utf8-browser" "3.6.1" - "@aws-sdk/util-utf8-node" "3.6.1" - tslib "^2.0.0" - -"@aws-sdk/client-translate@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-translate/-/client-translate-3.6.1.tgz#ce855c9fe7885b930d4039c2e45c869e3c0a6656" - integrity sha512-RIHY+Og1i43B5aWlfUUk0ZFnNfM7j2vzlYUwOqhndawV49GFf96M3pmskR5sKEZI+5TXY77qR9TgZ/r3UxVCRQ== - dependencies: - "@aws-crypto/sha256-browser" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-sdk/config-resolver" "3.6.1" - "@aws-sdk/credential-provider-node" "3.6.1" - "@aws-sdk/fetch-http-handler" "3.6.1" - "@aws-sdk/hash-node" "3.6.1" - "@aws-sdk/invalid-dependency" "3.6.1" - "@aws-sdk/middleware-content-length" "3.6.1" - "@aws-sdk/middleware-host-header" "3.6.1" - "@aws-sdk/middleware-logger" "3.6.1" - "@aws-sdk/middleware-retry" "3.6.1" - "@aws-sdk/middleware-serde" "3.6.1" - "@aws-sdk/middleware-signing" "3.6.1" - "@aws-sdk/middleware-stack" "3.6.1" - "@aws-sdk/middleware-user-agent" "3.6.1" - "@aws-sdk/node-config-provider" "3.6.1" - "@aws-sdk/node-http-handler" "3.6.1" - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/smithy-client" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/url-parser" "3.6.1" - "@aws-sdk/url-parser-native" "3.6.1" - "@aws-sdk/util-base64-browser" "3.6.1" - "@aws-sdk/util-base64-node" "3.6.1" - "@aws-sdk/util-body-length-browser" "3.6.1" - "@aws-sdk/util-body-length-node" "3.6.1" - "@aws-sdk/util-user-agent-browser" "3.6.1" - "@aws-sdk/util-user-agent-node" "3.6.1" - "@aws-sdk/util-utf8-browser" "3.6.1" - "@aws-sdk/util-utf8-node" "3.6.1" - tslib "^2.0.0" - uuid "^3.0.0" - -"@aws-sdk/config-resolver@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/config-resolver/-/config-resolver-3.186.0.tgz#68bbf82b572f03ee3ec9ac84d000147e1050149b" - integrity sha512-l8DR7Q4grEn1fgo2/KvtIfIHJS33HGKPQnht8OPxkl0dMzOJ0jxjOw/tMbrIcPnr2T3Fi7LLcj3dY1Fo1poruQ== - dependencies: - "@aws-sdk/signature-v4" "3.186.0" - "@aws-sdk/types" "3.186.0" - "@aws-sdk/util-config-provider" "3.186.0" - "@aws-sdk/util-middleware" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/config-resolver@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/config-resolver/-/config-resolver-3.6.1.tgz#3bcc5e6a0ebeedf0981b0540e1f18a72b4dafebf" - integrity sha512-qjP1g3jLIm+XvOIJ4J7VmZRi87vsDmTRzIFePVeG+EFWwYQLxQjTGMdIj3yKTh1WuZ0HByf47mGcpiS4HZLm1Q== - dependencies: - "@aws-sdk/signature-v4" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/credential-provider-cognito-identity@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.6.1.tgz#df928951612a34832c2df15fb899251d828c2df3" - integrity sha512-uJ9q+yq+Dhdo32gcv0p/AT7sKSAUH0y4ts9XRK/vx0dW9Q3XJy99mOJlq/6fkh4LfWeavJJlaCo9lSHNMWXx4w== - dependencies: - "@aws-sdk/client-cognito-identity" "3.6.1" - "@aws-sdk/property-provider" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/credential-provider-env@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.186.0.tgz#55dec9c4c29ebbdff4f3bce72de9e98f7a1f92e1" - integrity sha512-N9LPAqi1lsQWgxzmU4NPvLPnCN5+IQ3Ai1IFf3wM6FFPNoSUd1kIA2c6xaf0BE7j5Kelm0raZOb4LnV3TBAv+g== - dependencies: - "@aws-sdk/property-provider" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-env@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.6.1.tgz#d8b2dd36836432a9b8ec05a5cf9fe428b04c9964" - integrity sha512-coeFf/HnhpGidcAN1i1NuFgyFB2M6DeN1zNVy4f6s4mAh96ftr9DgWM1CcE3C+cLHEdpNqleVgC/2VQpyzOBLQ== - dependencies: - "@aws-sdk/property-provider" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/credential-provider-imds@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.186.0.tgz#73e0f62832726c7734b4f6c50a02ab0d869c00e1" - integrity sha512-iJeC7KrEgPPAuXjCZ3ExYZrRQvzpSdTZopYgUm5TnNZ8S1NU/4nvv5xVy61JvMj3JQAeG8UDYYgC421Foc8wQw== - dependencies: - "@aws-sdk/node-config-provider" "3.186.0" - "@aws-sdk/property-provider" "3.186.0" - "@aws-sdk/types" "3.186.0" - "@aws-sdk/url-parser" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-imds@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.6.1.tgz#b5a8b8ef15eac26c58e469451a6c7c34ab3ca875" - integrity sha512-bf4LMI418OYcQbyLZRAW8Q5AYM2IKrNqOnIcfrFn2f17ulG7TzoWW3WN/kMOw4TC9+y+vIlCWOv87GxU1yP0Bg== - dependencies: - "@aws-sdk/property-provider" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/credential-provider-ini@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.186.0.tgz#3b3873ccae855ee3f6f15dcd8212c5ca4ec01bf3" - integrity sha512-ecrFh3MoZhAj5P2k/HXo/hMJQ3sfmvlommzXuZ/D1Bj2yMcyWuBhF1A83Fwd2gtYrWRrllsK3IOMM5Jr8UIVZA== - dependencies: - "@aws-sdk/credential-provider-env" "3.186.0" - "@aws-sdk/credential-provider-imds" "3.186.0" - "@aws-sdk/credential-provider-sso" "3.186.0" - "@aws-sdk/credential-provider-web-identity" "3.186.0" - "@aws-sdk/property-provider" "3.186.0" - "@aws-sdk/shared-ini-file-loader" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-ini@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.6.1.tgz#0da6d9341e621f8e0815814ed017b88e268fbc3d" - integrity sha512-3jguW6+ttRNddRZvbrs1yb3F1jrUbqyv0UfRoHuOGthjTt+L9sDpJaJGugYnT3bS9WBu1NydLVE2kDV++mJGVw== - dependencies: - "@aws-sdk/property-provider" "3.6.1" - "@aws-sdk/shared-ini-file-loader" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/credential-provider-node@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.186.0.tgz#0be58623660b41eed3a349a89b31a01d4cc773ea" - integrity sha512-HIt2XhSRhEvVgRxTveLCzIkd/SzEBQfkQ6xMJhkBtfJw1o3+jeCk+VysXM0idqmXytctL0O3g9cvvTHOsUgxOA== - dependencies: - "@aws-sdk/credential-provider-env" "3.186.0" - "@aws-sdk/credential-provider-imds" "3.186.0" - "@aws-sdk/credential-provider-ini" "3.186.0" - "@aws-sdk/credential-provider-process" "3.186.0" - "@aws-sdk/credential-provider-sso" "3.186.0" - "@aws-sdk/credential-provider-web-identity" "3.186.0" - "@aws-sdk/property-provider" "3.186.0" - "@aws-sdk/shared-ini-file-loader" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-node@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.6.1.tgz#0055292a4f0f49d053e8dfcc9174d8d2cf6862bb" - integrity sha512-VAHOcsqkPrF1k/fA62pv9c75lUWe5bHpcbFX83C3EUPd2FXV10Lfkv6bdWhyZPQy0k8T+9/yikHH3c7ZQeFE5A== - dependencies: - "@aws-sdk/credential-provider-env" "3.6.1" - "@aws-sdk/credential-provider-imds" "3.6.1" - "@aws-sdk/credential-provider-ini" "3.6.1" - "@aws-sdk/credential-provider-process" "3.6.1" - "@aws-sdk/property-provider" "3.6.1" - "@aws-sdk/shared-ini-file-loader" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/credential-provider-process@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.186.0.tgz#e3be60983261a58c212f5c38b6fb76305bbb8ce7" - integrity sha512-ATRU6gbXvWC1TLnjOEZugC/PBXHBoZgBADid4fDcEQY1vF5e5Ux1kmqkJxyHtV5Wl8sE2uJfwWn+FlpUHRX67g== - dependencies: - "@aws-sdk/property-provider" "3.186.0" - "@aws-sdk/shared-ini-file-loader" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-process@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.6.1.tgz#5bf851f3ee232c565b8c82608926df0ad28c1958" - integrity sha512-d0/TpMoEV4qMYkdpyyjU2Otse9X2jC1DuxWajHOWZYEw8oejMvXYTZ10hNaXZvAcNM9q214rp+k4mkt6gIcI6g== - dependencies: - "@aws-sdk/credential-provider-ini" "3.6.1" - "@aws-sdk/property-provider" "3.6.1" - "@aws-sdk/shared-ini-file-loader" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/credential-provider-sso@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.186.0.tgz#e1aa466543b3b0877d45b885a1c11b329232df22" - integrity sha512-mJ+IZljgXPx99HCmuLgBVDPLepHrwqnEEC/0wigrLCx6uz3SrAWmGZsNbxSEtb2CFSAaczlTHcU/kIl7XZIyeQ== - dependencies: - "@aws-sdk/client-sso" "3.186.0" - "@aws-sdk/property-provider" "3.186.0" - "@aws-sdk/shared-ini-file-loader" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-web-identity@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.186.0.tgz#db43f37f7827b553490dd865dbaa9a2c45f95494" - integrity sha512-KqzI5eBV72FE+8SuOQAu+r53RXGVHg4AuDJmdXyo7Gc4wS/B9FNElA8jVUjjYgVnf0FSiri+l41VzQ44dCopSA== - dependencies: - "@aws-sdk/property-provider" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/eventstream-codec@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-codec/-/eventstream-codec-3.186.0.tgz#9da9608866b38179edf72987f2bc3b865d11db13" - integrity sha512-3kLcJ0/H+zxFlhTlE1SGoFpzd/SitwXOsTSlYVwrwdISKRjooGg0BJpm1CSTkvmWnQIUlYijJvS96TAJ+fCPIA== - dependencies: - "@aws-crypto/crc32" "2.0.0" - "@aws-sdk/types" "3.186.0" - "@aws-sdk/util-hex-encoding" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/eventstream-handler-node@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.186.0.tgz#d58aec9a8617ed1a9a3800d5526333deb3efebb2" - integrity sha512-S8eAxCHyFAGSH7F6GHKU2ckpiwFPwJUQwMzewISLg3wzLQeu6lmduxBxVaV3/SoEbEMsbNmrgw9EXtw3Vt/odQ== - dependencies: - "@aws-sdk/eventstream-codec" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/eventstream-marshaller@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-marshaller/-/eventstream-marshaller-3.6.1.tgz#6abfbdf3639249d1a77686cbcae5d8e47bcba989" - integrity sha512-ZvN3Nvxn2Gul08L9MOSN123LwSO0E1gF/CqmOGZtEWzPnoSX/PWM9mhPPeXubyw2KdlXylOodYYw3EAATk3OmA== - dependencies: - "@aws-crypto/crc32" "^1.0.0" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/util-hex-encoding" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/eventstream-serde-browser@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.186.0.tgz#2a0bd942f977b3e2f1a77822ac091ddebe069475" - integrity sha512-0r2c+yugBdkP5bglGhGOgztjeHdHTKqu2u6bvTByM0nJShNO9YyqWygqPqDUOE5axcYQE1D0aFDGzDtP3mGJhw== - dependencies: - "@aws-sdk/eventstream-serde-universal" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/eventstream-serde-browser@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.6.1.tgz#1253bd5215745f79d534fc9bc6bd006ee7a0f239" - integrity sha512-J8B30d+YUfkBtgWRr7+9AfYiPnbG28zjMlFGsJf8Wxr/hDCfff+Z8NzlBYFEbS7McXXhRiIN8DHUvCtolJtWJQ== - dependencies: - "@aws-sdk/eventstream-marshaller" "3.6.1" - "@aws-sdk/eventstream-serde-universal" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/eventstream-serde-config-resolver@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.186.0.tgz#6c277058bb0fa14752f0b6d7043576e0b5f13da4" - integrity sha512-xhwCqYrAX5c7fg9COXVw6r7Sa3BO5cCfQMSR5S1QisE7do8K1GDKEHvUCheOx+RLon+P3glLjuNBMdD0HfCVNA== - dependencies: - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/eventstream-serde-config-resolver@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.6.1.tgz#ebb5c1614f55d0ebb225defac1f76c420e188086" - integrity sha512-72pCzcT/KeD4gPgRVBSQzEzz4JBim8bNwPwZCGaIYdYAsAI8YMlvp0JNdis3Ov9DFURc87YilWKQlAfw7CDJxA== - dependencies: - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/eventstream-serde-node@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-node/-/eventstream-serde-node-3.186.0.tgz#dabeab714f447790c5dd31d401c5a3822b795109" - integrity sha512-9p/gdukJYfmA+OEYd6MfIuufxrrfdt15lBDM3FODuc9j09LSYSRHSxthkIhiM5XYYaaUM+4R0ZlSMdaC3vFDFQ== - dependencies: - "@aws-sdk/eventstream-serde-universal" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/eventstream-serde-node@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-node/-/eventstream-serde-node-3.6.1.tgz#705e12bea185905a198d7812af10e3a679dfc841" - integrity sha512-rjBbJFjCrEcm2NxZctp+eJmyPxKYayG3tQZo8PEAQSViIlK5QexQI3fgqNAeCtK7l/SFAAvnOMRZF6Z3NdUY6A== - dependencies: - "@aws-sdk/eventstream-marshaller" "3.6.1" - "@aws-sdk/eventstream-serde-universal" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/eventstream-serde-universal@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-universal/-/eventstream-serde-universal-3.186.0.tgz#85a88a2cd5c336b1271976fa8db70654ec90fbf4" - integrity sha512-rIgPmwUxn2tzainBoh+cxAF+b7o01CcW+17yloXmawsi0kiR7QK7v9m/JTGQPWKtHSsPOrtRzuiWQNX57SlcsQ== - dependencies: - "@aws-sdk/eventstream-codec" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/eventstream-serde-universal@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-universal/-/eventstream-serde-universal-3.6.1.tgz#5be6865adb55436cbc90557df3a3c49b53553470" - integrity sha512-rpRu97yAGHr9GQLWMzcGICR2PxNu1dHU/MYc9Kb6UgGeZd4fod4o1zjhAJuj98cXn2xwHNFM4wMKua6B4zKrZg== - dependencies: - "@aws-sdk/eventstream-marshaller" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/fetch-http-handler@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.186.0.tgz#c1adc5f741e1ba9ad9d3fb13c9c2afdc88530a85" - integrity sha512-k2v4AAHRD76WnLg7arH94EvIclClo/YfuqO7NoQ6/KwOxjRhs4G6TgIsAZ9E0xmqoJoV81Xqy8H8ldfy9F8LEw== - dependencies: - "@aws-sdk/protocol-http" "3.186.0" - "@aws-sdk/querystring-builder" "3.186.0" - "@aws-sdk/types" "3.186.0" - "@aws-sdk/util-base64-browser" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/fetch-http-handler@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.6.1.tgz#c5fb4a4ee158161fca52b220d2c11dddcda9b092" - integrity sha512-N8l6ZbwhINuWG5hsl625lmIQmVjzsqRPmlgh061jm5D90IhsM5/3A3wUxpB/k0av1dmuMRw/m0YtBU5w4LOwvw== - dependencies: - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/querystring-builder" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/util-base64-browser" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/hash-blob-browser@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/hash-blob-browser/-/hash-blob-browser-3.6.1.tgz#f44a1857b75769e21cd6091211171135e03531e6" - integrity sha512-9jPaZ/e3F8gf9JZd44DD6MvbYV6bKnn99rkG3GFIINOy9etoxPrLehp2bH2DK/j0ow60RNuwgUjj5qHV/zF67g== - dependencies: - "@aws-sdk/chunked-blob-reader" "3.6.1" - "@aws-sdk/chunked-blob-reader-native" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/hash-node@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/hash-node/-/hash-node-3.186.0.tgz#8cb13aae8f46eb360fed76baf5062f66f27dfb70" - integrity sha512-G3zuK8/3KExDTxqrGqko+opOMLRF0BwcwekV/wm3GKIM/NnLhHblBs2zd/yi7VsEoWmuzibfp6uzxgFpEoJ87w== - dependencies: - "@aws-sdk/types" "3.186.0" - "@aws-sdk/util-buffer-from" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/hash-node@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/hash-node/-/hash-node-3.6.1.tgz#72d75ec3b9c7e7f9b0c498805364f1f897165ce9" - integrity sha512-iKEpzpyaG9PYCnaOGwTIf0lffsF/TpsXrzAfnBlfeOU/3FbgniW2z/yq5xBbtMDtLobtOYC09kUFwDnDvuveSA== - dependencies: - "@aws-sdk/types" "3.6.1" - "@aws-sdk/util-buffer-from" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/hash-stream-node@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/hash-stream-node/-/hash-stream-node-3.6.1.tgz#91c77e382ef3d0472160a49b1109395a4a70c801" - integrity sha512-ePaWjCItIWxuSxA/UnUM/keQ3IAOsQz3FYSxu0KK8K0e1bKTEUgDIG9oMLBq7jIl9TzJG0HBXuPfMe73QHUNug== - dependencies: - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/invalid-dependency@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/invalid-dependency/-/invalid-dependency-3.186.0.tgz#aa6331ccf404cb659ec38483116080e4b82b0663" - integrity sha512-hjeZKqORhG2DPWYZ776lQ9YO3gjw166vZHZCZU/43kEYaCZHsF4mexHwHzreAY6RfS25cH60Um7dUh1aeVIpkw== - dependencies: - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/invalid-dependency@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/invalid-dependency/-/invalid-dependency-3.6.1.tgz#fd2519f5482c6d6113d38a73b7143fd8d5b5b670" - integrity sha512-d0RLqK7yeDCZJKopnGmGXo2rYkQNE7sGKVmBHQD1j1kKZ9lWwRoJeWqo834JNPZzY5XRvZG5SuIjJ1kFy8LpyQ== - dependencies: - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/is-array-buffer@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/is-array-buffer/-/is-array-buffer-3.186.0.tgz#7700e36f29d416c2677f4bf8816120f96d87f1b7" - integrity sha512-fObm+P6mjWYzxoFY4y2STHBmSdgKbIAXez0xope563mox62I8I4hhVPUCaDVydXvDpJv8tbedJMk0meJl22+xA== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/is-array-buffer@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/is-array-buffer/-/is-array-buffer-3.6.1.tgz#96df5d64b2d599947f81b164d5d92623f85c659c" - integrity sha512-qm2iDJmCrxlQE2dsFG+TujPe7jw4DF+4RTrsFMhk/e3lOl3MAzQ6Fc2kXtgeUcVrZVFTL8fQvXE1ByYyI6WbCw== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/md5-js@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/md5-js/-/md5-js-3.6.1.tgz#bffe21106fba0174d73ccc2c29ca1c5364d2af2d" - integrity sha512-lzCqkZF1sbzGFDyq1dI+lR3AmlE33rbC/JhZ5fzw3hJZvfZ6Beq3Su7YwDo65IWEu0zOKYaNywTeOloXP/CkxQ== - dependencies: - "@aws-sdk/types" "3.6.1" - "@aws-sdk/util-utf8-browser" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/middleware-apply-body-checksum@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-apply-body-checksum/-/middleware-apply-body-checksum-3.6.1.tgz#dece86e489531981b8aa2786dafbbef69edce1d6" - integrity sha512-IncmXR1MPk6aYvmD37It8dP6wVMzaxxzgrkIU2ACkN5UVwA+/0Sr3ZNd9dNwjpyoH1AwpL9BetnlJaWtT6K5ew== - dependencies: - "@aws-sdk/is-array-buffer" "3.6.1" - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/middleware-bucket-endpoint@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.6.1.tgz#7ebdd79fac0f78d8af549f4fd799d4f7d02e78de" - integrity sha512-Frcqn2RQDNHy+e2Q9hv3ejT3mQWtGlfZESbXEF6toR4M0R8MmEVqIB/ohI6VKBj11lRmGwvpPsR6zz+PJ8HS7A== - dependencies: - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/util-arn-parser" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/middleware-content-length@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-content-length/-/middleware-content-length-3.186.0.tgz#8cc7aeec527738c46fdaf4a48b17c5cbfdc7ce58" - integrity sha512-Ol3c1ks3IK1s+Okc/rHIX7w2WpXofuQdoAEme37gHeml+8FtUlWH/881h62xfMdf+0YZpRuYv/eM7lBmJBPNJw== - dependencies: - "@aws-sdk/protocol-http" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-content-length@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-content-length/-/middleware-content-length-3.6.1.tgz#f9c00a4045b2b56c1ff8bcbb3dec9c3d42332992" - integrity sha512-QRcocG9f5YjYzbjs2HjKla6ZIjvx8Y8tm1ZSFOPey81m18CLif1O7M3AtJXvxn+0zeSck9StFdhz5gfjVNYtDg== - dependencies: - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/middleware-eventstream@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.186.0.tgz#64a66102ed2e182182473948f131f23dda84e729" - integrity sha512-7yjFiitTGgfKL6cHK3u3HYFnld26IW5aUAFuEd6ocR/FjliysfBd8g0g1bw3bRfIMgCDD8OIOkXK8iCk2iYGWQ== - dependencies: - "@aws-sdk/protocol-http" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-expect-continue@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.6.1.tgz#56e56db572f81dd4fa8803e85bd1f36005f9fffa" - integrity sha512-vvMOqVYU3uvdJzg/X6NHewZUEBZhSqND1IEcdahLb6RmvDhsS39iS97VZmEFsjj/UFGoePtYjrrdEgRG9Rm1kQ== - dependencies: - "@aws-sdk/middleware-header-default" "3.6.1" - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/middleware-header-default@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-header-default/-/middleware-header-default-3.6.1.tgz#a3a108d22cbdd1e1754910625fafb2f2a67fbcfc" - integrity sha512-YD137iIctXVH8Eut0WOBalvvA+uL0jM0UXZ9N2oKrC8kPQPpqjK9lYGFKZQFsl/XlQHAjJi+gCAFrYsBntRWJQ== - dependencies: - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/middleware-host-header@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.186.0.tgz#fce4f1219ce1835e2348c787d8341080b0024e34" - integrity sha512-5bTzrRzP2IGwyF3QCyMGtSXpOOud537x32htZf344IvVjrqZF/P8CDfGTkHkeBCIH+wnJxjK+l/QBb3ypAMIqQ== - dependencies: - "@aws-sdk/protocol-http" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-host-header@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.6.1.tgz#6e1b4b95c5bfea5a4416fa32f11d8fa2e6edaeff" - integrity sha512-nwq8R2fGBRZQE0Fr/jiOgqfppfiTQCUoD8hyX3qSS7Qc2uqpsDOt2TnnoZl56mpQYkF/344IvMAkp+ew6wR73w== - dependencies: - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/middleware-location-constraint@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.6.1.tgz#6fc2dd6a42968f011eb060ca564e9f749649eb01" - integrity sha512-nFisTc0O5D+4I+sRxiiLPasC/I4NDc3s+hgbPPt/b3uAdrujJjhwFBOSaTx8qQvz/xJPAA8pUA/bfWIyeZKi/w== - dependencies: - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/middleware-logger@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.186.0.tgz#8a027fbbb1b8098ccc888bce51f34b000c0a0550" - integrity sha512-/1gGBImQT8xYh80pB7QtyzA799TqXtLZYQUohWAsFReYB7fdh5o+mu2rX0FNzZnrLIh2zBUNs4yaWGsnab4uXg== - dependencies: - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-logger@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.6.1.tgz#78b3732cf188d5e4df13488db6418f7f98a77d6d" - integrity sha512-zxaSLpwKlja7JvK20UsDTxPqBZUo3rbDA1uv3VWwpxzOrEWSlVZYx/KLuyGWGkx9V71ZEkf6oOWWJIstS0wyQQ== - dependencies: - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/middleware-recursion-detection@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.186.0.tgz#9d9d3212e9a954b557840bb80415987f4484487e" - integrity sha512-Za7k26Kovb4LuV5tmC6wcVILDCt0kwztwSlB991xk4vwNTja8kKxSt53WsYG8Q2wSaW6UOIbSoguZVyxbIY07Q== - dependencies: - "@aws-sdk/protocol-http" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-retry@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-retry/-/middleware-retry-3.186.0.tgz#0ff9af58d73855863683991a809b40b93c753ad1" - integrity sha512-/VI9emEKhhDzlNv9lQMmkyxx3GjJ8yPfXH3HuAeOgM1wx1BjCTLRYEWnTbQwq7BDzVENdneleCsGAp7yaj80Aw== - dependencies: - "@aws-sdk/protocol-http" "3.186.0" - "@aws-sdk/service-error-classification" "3.186.0" - "@aws-sdk/types" "3.186.0" - "@aws-sdk/util-middleware" "3.186.0" - tslib "^2.3.1" - uuid "^8.3.2" - -"@aws-sdk/middleware-retry@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-retry/-/middleware-retry-3.6.1.tgz#202aadb1a3bf0e1ceabcd8319a5fa308b32db247" - integrity sha512-WHeo4d2jsXxBP+cec2SeLb0btYXwYXuE56WLmNt0RvJYmiBzytUeGJeRa9HuwV574kgigAuHGCeHlPO36G4Y0Q== - dependencies: - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/service-error-classification" "3.6.1" - "@aws-sdk/types" "3.6.1" - react-native-get-random-values "^1.4.0" - tslib "^1.8.0" - uuid "^3.0.0" - -"@aws-sdk/middleware-sdk-s3@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.6.1.tgz#371f8991ac82432982153c035ab9450d8df14546" - integrity sha512-HEA9kynNTsOSIIz8p5GEEAH03pnn+SSohwPl80sGqkmI1yl1tzjqgYZRii0e6acJTh4j9655XFzSx36hYPeB2w== - dependencies: - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/util-arn-parser" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/middleware-sdk-sts@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.186.0.tgz#18f3d6b7b42c1345b5733ac3e3119d370a403e94" - integrity sha512-GDcK0O8rjtnd+XRGnxzheq1V2jk4Sj4HtjrxW/ROyhzLOAOyyxutBt+/zOpDD6Gba3qxc69wE+Cf/qngOkEkDw== - dependencies: - "@aws-sdk/middleware-signing" "3.186.0" - "@aws-sdk/property-provider" "3.186.0" - "@aws-sdk/protocol-http" "3.186.0" - "@aws-sdk/signature-v4" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-serde@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.186.0.tgz#f7944241ad5fb31cb15cd250c9e92147942b9ec6" - integrity sha512-6FEAz70RNf18fKL5O7CepPSwTKJEIoyG9zU6p17GzKMgPeFsxS5xO94Hcq5tV2/CqeHliebjqhKY7yi+Pgok7g== - dependencies: - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-serde@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.6.1.tgz#734c7d16c2aa9ccc01f6cca5e2f6aa2993b6739d" - integrity sha512-EdQCFZRERfP3uDuWcPNuaa2WUR3qL1WFDXafhcx+7ywQxagdYqBUWKFJlLYi6njbkOKXFM+eHBzoXGF0OV3MJA== - dependencies: - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/middleware-signing@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.186.0.tgz#37633bf855667b4841464e0044492d0aec5778b9" - integrity sha512-riCJYG/LlF/rkgVbHkr4xJscc0/sECzDivzTaUmfb9kJhAwGxCyNqnTvg0q6UO00kxSdEB9zNZI2/iJYVBijBQ== - dependencies: - "@aws-sdk/property-provider" "3.186.0" - "@aws-sdk/protocol-http" "3.186.0" - "@aws-sdk/signature-v4" "3.186.0" - "@aws-sdk/types" "3.186.0" - "@aws-sdk/util-middleware" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-signing@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.6.1.tgz#e70a2f35d85d70e33c9fddfb54b9520f6382db16" - integrity sha512-1woKq+1sU3eausdl8BNdAMRZMkSYuy4mxhLsF0/qAUuLwo1eJLLUCOQp477tICawgu4O4q2OAyUHk7wMqYnQCg== - dependencies: - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/signature-v4" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/middleware-ssec@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.6.1.tgz#c7dd80e4c1e06be9050c742af7879619b400f0d1" - integrity sha512-svuH6s91uKUTORt51msiL/ZBjtYSW32c3uVoWxludd/PEf6zO5wCmUEsKoyVwa88L7rrCq+81UBv5A8S5kc3Cw== - dependencies: - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/middleware-stack@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-stack/-/middleware-stack-3.186.0.tgz#da3445fe74b867ee6d7eec4f0dde28aaca1125d6" - integrity sha512-fENMoo0pW7UBrbuycPf+3WZ+fcUgP9PnQ0jcOK3WWZlZ9d2ewh4HNxLh4EE3NkNYj4VIUFXtTUuVNHlG8trXjQ== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/middleware-stack@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-stack/-/middleware-stack-3.6.1.tgz#d7483201706bb5935a62884e9b60f425f1c6434f" - integrity sha512-EPsIxMi8LtCt7YwTFpWGlVGYJc0q4kwFbOssY02qfqdCnyqi2y5wo089dH7OdxUooQ0D7CPsXM1zTTuzvm+9Fw== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/middleware-user-agent@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.186.0.tgz#6d881e9cea5fe7517e220f3a47c2f3557c7f27fc" - integrity sha512-fb+F2PF9DLKOVMgmhkr+ltN8ZhNJavTla9aqmbd01846OLEaN1n5xEnV7p8q5+EznVBWDF38Oz9Ae5BMt3Hs7w== - dependencies: - "@aws-sdk/protocol-http" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-user-agent@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.6.1.tgz#6845dfb3bc6187897f348c2c87dec833e6a65c99" - integrity sha512-YvXvwllNDVvxQ30vIqLsx+P6jjnfFEQUmhlv64n98gOme6h2BqoyQDcC3yHRGctuxRZEsR7W/H1ASTKC+iabbQ== - dependencies: - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/node-config-provider@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/node-config-provider/-/node-config-provider-3.186.0.tgz#64259429d39f2ef5a76663162bf2e8db6032a322" - integrity sha512-De93mgmtuUUeoiKXU8pVHXWKPBfJQlS/lh1k2H9T2Pd9Tzi0l7p5ttddx4BsEx4gk+Pc5flNz+DeptiSjZpa4A== - dependencies: - "@aws-sdk/property-provider" "3.186.0" - "@aws-sdk/shared-ini-file-loader" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/node-config-provider@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/node-config-provider/-/node-config-provider-3.6.1.tgz#cb85d06329347fde566f08426f8714b1f65d2fb7" - integrity sha512-x2Z7lm0ZhHYqMybvkaI5hDKfBkaLaXhTDfgrLl9TmBZ3QHO4fIHgeL82VZ90Paol+OS+jdq2AheLmzbSxv3HrA== - dependencies: - "@aws-sdk/property-provider" "3.6.1" - "@aws-sdk/shared-ini-file-loader" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/node-http-handler@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/node-http-handler/-/node-http-handler-3.186.0.tgz#8be1598a9187637a767dc337bf22fe01461e86eb" - integrity sha512-CbkbDuPZT9UNJ4dAZJWB3BV+Z65wFy7OduqGkzNNrKq6ZYMUfehthhUOTk8vU6RMe/0FkN+J0fFXlBx/bs/cHw== - dependencies: - "@aws-sdk/abort-controller" "3.186.0" - "@aws-sdk/protocol-http" "3.186.0" - "@aws-sdk/querystring-builder" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/node-http-handler@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/node-http-handler/-/node-http-handler-3.6.1.tgz#4b65c4dcc0cf46ba44cb6c3bf29c5f817bb8d9a7" - integrity sha512-6XSaoqbm9ZF6T4UdBCcs/Gn2XclwBotkdjj46AxO+9vRAgZDP+lH/8WwZsvfqJhhRhS0qxWrks98WGJwmaTG8g== - dependencies: - "@aws-sdk/abort-controller" "3.6.1" - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/querystring-builder" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/property-provider@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/property-provider/-/property-provider-3.186.0.tgz#af41e615662a2749d3ff7da78c41f79f4be95b3b" - integrity sha512-nWKqt36UW3xV23RlHUmat+yevw9up+T+953nfjcmCBKtgWlCWu/aUzewTRhKj3VRscbN+Wer95SBw9Lr/MMOlQ== - dependencies: - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/property-provider@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/property-provider/-/property-provider-3.6.1.tgz#d973fc87d199d32c44d947e17f2ee2dd140a9593" - integrity sha512-2gR2DzDySXKFoj9iXLm1TZBVSvFIikEPJsbRmAZx5RBY+tp1IXWqZM6PESjaLdLg/ZtR0QhW2ZcRn0fyq2JfnQ== - dependencies: - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/protocol-http@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/protocol-http/-/protocol-http-3.186.0.tgz#99115870846312dd4202b5e2cc68fe39324b9bfa" - integrity sha512-l/KYr/UBDUU5ginqTgHtFfHR3X6ljf/1J1ThIiUg3C3kVC/Zwztm7BEOw8hHRWnWQGU/jYasGYcrcPLdQqFZyQ== - dependencies: - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/protocol-http@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/protocol-http/-/protocol-http-3.6.1.tgz#d3d276846bec19ddb339d06bbc48116d17bbc656" - integrity sha512-WkQz7ncVYTLvCidDfXWouDzqxgSNPZDz3Bql+7VhZeITnzAEcr4hNMyEqMAVYBVugGmkG2W6YiUqNNs1goOcDA== - dependencies: - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/querystring-builder@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.186.0.tgz#a380db0e1c71004932d9e2f3e6dc6761d1165c47" - integrity sha512-mweCpuLufImxfq/rRBTEpjGuB4xhQvbokA+otjnUxlPdIobytLqEs7pCGQfLzQ7+1ZMo8LBXt70RH4A2nSX/JQ== - dependencies: - "@aws-sdk/types" "3.186.0" - "@aws-sdk/util-uri-escape" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/querystring-builder@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.6.1.tgz#4c769829a3760ef065d0d3801f297a7f0cd324d4" - integrity sha512-ESe255Yl6vB1AMNqaGSQow3TBYYnpw0AFjE40q2VyiNrkbaqKmW2EzjeCy3wEmB1IfJDHy3O12ZOMUMOnjFT8g== - dependencies: - "@aws-sdk/types" "3.6.1" - "@aws-sdk/util-uri-escape" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/querystring-parser@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.186.0.tgz#4db6d31ad4df0d45baa2a35e371fbaa23e45ddd2" - integrity sha512-0iYfEloghzPVXJjmnzHamNx1F1jIiTW9Svy5ZF9LVqyr/uHZcQuiWYsuhWloBMLs8mfWarkZM02WfxZ8buAuhg== - dependencies: - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/querystring-parser@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.6.1.tgz#e3fa5a710429c7dd411e802a0b82beb48012cce2" - integrity sha512-hh6dhqamKrWWaDSuO2YULci0RGwJWygoy8hpCRxs/FpzzHIcbm6Cl6Jhrn5eKBzOBv+PhCcYwbfad0kIZZovcQ== - dependencies: - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/s3-request-presigner@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.6.1.tgz#ec83c70171692862a7f7ebbd151242a5af443695" - integrity sha512-OI7UHCKBwuiO/RmHHewBKnL2NYqdilXRmpX67TJ4tTszIrWP2+vpm3lIfrx/BM8nf8nKTzgkO98uFhoJsEhmTg== - dependencies: - "@aws-sdk/protocol-http" "3.6.1" - "@aws-sdk/signature-v4" "3.6.1" - "@aws-sdk/smithy-client" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/util-create-request" "3.6.1" - "@aws-sdk/util-format-url" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/service-error-classification@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/service-error-classification/-/service-error-classification-3.186.0.tgz#6e4e1d4b53d68bd28c28d9cf0b3b4cb6a6a59dbb" - integrity sha512-DRl3ORk4tF+jmH5uvftlfaq0IeKKpt0UPAOAFQ/JFWe+TjOcQd/K+VC0iiIG97YFp3aeFmH1JbEgsNxd+8fdxw== - -"@aws-sdk/service-error-classification@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/service-error-classification/-/service-error-classification-3.6.1.tgz#296fe62ac61338341e8a009c9a2dab013a791903" - integrity sha512-kZ7ZhbrN1f+vrSRkTJvXsu7BlOyZgym058nPA745+1RZ1Rtv4Ax8oknf2RvJyj/1qRUi8LBaAREjzQ3C8tmLBA== - -"@aws-sdk/shared-ini-file-loader@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.186.0.tgz#a2d285bb3c4f8d69f7bfbde7a5868740cd3f7795" - integrity sha512-2FZqxmICtwN9CYid4dwfJSz/gGFHyStFQ3HCOQ8DsJUf2yREMSBsVmKqsyWgOrYcQ98gPcD5GIa7QO5yl3XF6A== - dependencies: - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/shared-ini-file-loader@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.6.1.tgz#2b7182cbb0d632ad7c9712bebffdeee24a6f7eb6" - integrity sha512-BnLHtsNLOoow6rPV+QVi6jnovU5g1m0YzoUG0BQYZ1ALyVlWVr0VvlUX30gMDfdYoPMp+DHvF8GXdMuGINq6kQ== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/signature-v4@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.186.0.tgz#bbd56e71af95548abaeec6307ea1dfe7bd26b4e4" - integrity sha512-18i96P5c4suMqwSNhnEOqhq4doqqyjH4fn0YV3F8TkekHPIWP4mtIJ0PWAN4eievqdtcKgD/GqVO6FaJG9texw== - dependencies: - "@aws-sdk/is-array-buffer" "3.186.0" - "@aws-sdk/types" "3.186.0" - "@aws-sdk/util-hex-encoding" "3.186.0" - "@aws-sdk/util-middleware" "3.186.0" - "@aws-sdk/util-uri-escape" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/signature-v4@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.6.1.tgz#b20a3cf3e891131f83b012651f7d4af2bf240611" - integrity sha512-EAR0qGVL4AgzodZv4t+BSuBfyOXhTNxDxom50IFI1MqidR9vI6avNZKcPHhgXbm7XVcsDGThZKbzQ2q7MZ2NTA== - dependencies: - "@aws-sdk/is-array-buffer" "3.6.1" - "@aws-sdk/types" "3.6.1" - "@aws-sdk/util-hex-encoding" "3.6.1" - "@aws-sdk/util-uri-escape" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/smithy-client@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.186.0.tgz#67514544fb55d7eff46300e1e73311625cf6f916" - integrity sha512-rdAxSFGSnrSprVJ6i1BXi65r4X14cuya6fYe8dSdgmFSa+U2ZevT97lb3tSINCUxBGeMXhENIzbVGkRZuMh+DQ== - dependencies: - "@aws-sdk/middleware-stack" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/smithy-client@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.6.1.tgz#683fef89802e318922f8529a5433592d71a7ce9d" - integrity sha512-AVpRK4/iUxNeDdAm8UqP0ZgtgJMQeWcagTylijwelhWXyXzHUReY1sgILsWcdWnoy6gq845W7K2VBhBleni8+w== - dependencies: - "@aws-sdk/middleware-stack" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/types@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.186.0.tgz#f6fb6997b6a364f399288bfd5cd494bc680ac922" - integrity sha512-NatmSU37U+XauMFJCdFI6nougC20JUFZar+ump5wVv0i54H+2Refg1YbFDxSs0FY28TSB9jfhWIpfFBmXgL5MQ== - -"@aws-sdk/types@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.6.1.tgz#00686db69e998b521fcd4a5f81ef0960980f80c4" - integrity sha512-4Dx3eRTrUHLxhFdLJL8zdNGzVsJfAxtxPYYGmIddUkO2Gj3WA1TGjdfG4XN/ClI6e1XonCHafQX3UYO/mgnH3g== - -"@aws-sdk/types@^1.0.0-alpha.0": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-1.0.0-rc.10.tgz#729127fbfac5da1a3368ffe6ec2e90acc9ad69c3" - integrity sha512-9gwhYnkTNuYZ+etCtM4T8gjpZ0SWSXbzQxY34UjSS+dt3C/UnbX0J22tMahp/9Z1yCa9pihtXrkD+nO2xn7nVQ== - -"@aws-sdk/types@^3.1.0", "@aws-sdk/types@^3.110.0": - version "3.329.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.329.0.tgz#bc20659abfcd666954196c3a24ad47785db80dd3" - integrity sha512-wFBW4yciDfzQBSFmWNaEvHShnSGLMxSu9Lls6EUf6xDMavxSB36bsrVRX6CyAo/W0NeIIyEOW1LclGPgJV1okg== - dependencies: - tslib "^2.5.0" - -"@aws-sdk/url-parser-native@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser-native/-/url-parser-native-3.6.1.tgz#a5e787f98aafa777e73007f9490df334ef3389a2" - integrity sha512-3O+ktsrJoE8YQCho9L41YXO8EWILXrSeES7amUaV3mgIV5w4S3SB/r4RkmylpqRpQF7Ry8LFiAnMqH1wa4WBPA== - dependencies: - "@aws-sdk/querystring-parser" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - url "^0.11.0" - -"@aws-sdk/url-parser@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.186.0.tgz#e42f845cd405c1920fdbdcc796a350d4ace16ae9" - integrity sha512-jfdJkKqJZp8qjjwEjIGDqbqTuajBsddw02f86WiL8bPqD8W13/hdqbG4Fpwc+Bm6GwR6/4MY6xWXFnk8jDUKeA== - dependencies: - "@aws-sdk/querystring-parser" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/url-parser@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.6.1.tgz#f5d89fb21680469a61cb9fe08a7da3ef887884dd" - integrity sha512-pWFIePDx0PMCleQRsQDWoDl17YiijOLj0ZobN39rQt+wv5PhLSZDz9PgJsqS48nZ6hqsKgipRcjiBMhn5NtFcQ== - dependencies: - "@aws-sdk/querystring-parser" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/util-arn-parser@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.6.1.tgz#aa60b1bfa752ad3fa331f22fea4f703b741d1d6d" - integrity sha512-NFdYeuhaSrgnBG6Pt3zHNU7QwvhHq6sKUTWZShUayLMJYYbQr6IjmYVlPST4c84b+lyDoK68y/Zga621VfIdBg== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-base64-browser@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-base64-browser/-/util-base64-browser-3.186.0.tgz#0310482752163fa819718ce9ea9250836b20346d" - integrity sha512-TpQL8opoFfzTwUDxKeon/vuc83kGXpYqjl6hR8WzmHoQgmFfdFlV+0KXZOohra1001OP3FhqvMqaYbO8p9vXVQ== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-base64-browser@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-base64-browser/-/util-base64-browser-3.6.1.tgz#eddea1311b41037fc3fddd889d3e0a9882363215" - integrity sha512-+DHAIgt0AFARDVC7J0Z9FkSmJhBMlkYdOPeAAgO0WaQoKj7rtsLQJ7P3v3aS1paKN5/sk5xNY7ziVB6uHtOvHA== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-base64-node@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-base64-node/-/util-base64-node-3.186.0.tgz#500bd04b1ef7a6a5c0a2d11c0957a415922e05c7" - integrity sha512-wH5Y/EQNBfGS4VkkmiMyZXU+Ak6VCoFM1GKWopV+sj03zR2D4FHexi4SxWwEBMpZCd6foMtihhbNBuPA5fnh6w== - dependencies: - "@aws-sdk/util-buffer-from" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/util-base64-node@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-base64-node/-/util-base64-node-3.6.1.tgz#a79c233861e50d3a30728c72b736afdee07d4009" - integrity sha512-oiqzpsvtTSS92+cL3ykhGd7t3qBJKeHvrgOwUyEf1wFWHQ2DPJR+dIMy5rMFRXWLKCl3w7IddY2rJCkLYMjaqQ== - dependencies: - "@aws-sdk/util-buffer-from" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/util-body-length-browser@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.186.0.tgz#a898eda9f874f6974a9c5c60fcc76bcb6beac820" - integrity sha512-zKtjkI/dkj9oGkjo+7fIz+I9KuHrVt1ROAeL4OmDESS8UZi3/O8uMDFMuCp8jft6H+WFuYH6qRVWAVwXMiasXw== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-body-length-browser@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.6.1.tgz#2e8088f2d9a5a8258b4f56079a8890f538c2797e" - integrity sha512-IdWwE3rm/CFDk2F+IwTZOFTnnNW5SB8y1lWiQ54cfc7y03hO6jmXNnpZGZ5goHhT+vf1oheNQt1J47m0pM/Irw== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-body-length-node@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-node/-/util-body-length-node-3.186.0.tgz#95efbacbd13cb739b942c126c5d16ecf6712d4db" - integrity sha512-U7Ii8u8Wvu9EnBWKKeuwkdrWto3c0j7LG677Spe6vtwWkvY70n9WGfiKHTgBpVeLNv8jvfcx5+H0UOPQK1o9SQ== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-body-length-node@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-node/-/util-body-length-node-3.6.1.tgz#6e4f2eae46c5a7b0417a12ca7f4b54c390d4cacd" - integrity sha512-CUG3gc18bSOsqViQhB3M4AlLpAWV47RE6yWJ6rLD0J6/rSuzbwbjzxM39q0YTAVuSo/ivdbij+G9c3QCirC+QQ== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-buffer-from@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-buffer-from/-/util-buffer-from-3.186.0.tgz#01f7edb683d2f40374d0ca8ef2d16346dc8040a1" - integrity sha512-be2GCk2lsLWg/2V5Y+S4/9pOMXhOQo4DR4dIqBdR2R+jrMMHN9Xsr5QrkT6chcqLaJ/SBlwiAEEi3StMRmCOXA== - dependencies: - "@aws-sdk/is-array-buffer" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/util-buffer-from@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-buffer-from/-/util-buffer-from-3.6.1.tgz#24184ce74512f764d84002201b7f5101565e26f9" - integrity sha512-OGUh2B5NY4h7iRabqeZ+EgsrzE1LUmNFzMyhoZv0tO4NExyfQjxIYXLQQvydeOq9DJUbCw+yrRZrj8vXNDQG+g== - dependencies: - "@aws-sdk/is-array-buffer" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/util-config-provider@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-config-provider/-/util-config-provider-3.186.0.tgz#52ce3711edceadfac1b75fccc7c615e90c33fb2f" - integrity sha512-71Qwu/PN02XsRLApyxG0EUy/NxWh/CXxtl2C7qY14t+KTiRapwbDkdJ1cMsqYqghYP4BwJoj1M+EFMQSSlkZQQ== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-create-request@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-create-request/-/util-create-request-3.6.1.tgz#ecc4364551c7b3d0d9834ca3f56528fb8b083838" - integrity sha512-jR1U8WpwXl+xZ9ThS42Jr5MXuegQ7QioHsZjQn3V5pbm8CXTkBF0B2BcULQu/2G1XtHOJb8qUZQlk/REoaORfQ== - dependencies: - "@aws-sdk/middleware-stack" "3.6.1" - "@aws-sdk/smithy-client" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/util-defaults-mode-browser@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.186.0.tgz#d30b2f572e273d7d98287274c37c9ee00b493507" - integrity sha512-U8GOfIdQ0dZ7RRVpPynGteAHx4URtEh+JfWHHVfS6xLPthPHWTbyRhkQX++K/F8Jk+T5U8Anrrqlea4TlcO2DA== - dependencies: - "@aws-sdk/property-provider" "3.186.0" - "@aws-sdk/types" "3.186.0" - bowser "^2.11.0" - tslib "^2.3.1" - -"@aws-sdk/util-defaults-mode-node@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.186.0.tgz#8572453ba910fd2ab08d2cfee130ce5a0db83ba7" - integrity sha512-N6O5bpwCiE4z8y7SPHd7KYlszmNOYREa+mMgtOIXRU3VXSEHVKVWTZsHKvNTTHpW0qMqtgIvjvXCo3vsch5l3A== - dependencies: - "@aws-sdk/config-resolver" "3.186.0" - "@aws-sdk/credential-provider-imds" "3.186.0" - "@aws-sdk/node-config-provider" "3.186.0" - "@aws-sdk/property-provider" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/util-format-url@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-format-url/-/util-format-url-3.6.1.tgz#a011444aed0c47698d65095bcce95d7b4716324b" - integrity sha512-FvhcXcqLyJ0j0WdlmGs7PtjCCv8NaY4zBuXYO2iwAmqoy2SIZXQL63uAvmilqWj25q47ASAsUwSFLReCCfMklQ== - dependencies: - "@aws-sdk/querystring-builder" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/util-hex-encoding@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.186.0.tgz#7ed58b923997c6265f4dce60c8704237edb98895" - integrity sha512-UL9rdgIZz1E/jpAfaKH8QgUxNK9VP5JPgoR0bSiaefMjnsoBh0x/VVMsfUyziOoJCMLebhJzFowtwrSKEGsxNg== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-hex-encoding@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.6.1.tgz#84954fcc47b74ffbd2911ba5113e93bd9b1c6510" - integrity sha512-pzsGOHtU2eGca4NJgFg94lLaeXDOg8pcS9sVt4f9LmtUGbrqRveeyBv0XlkHeZW2n0IZBssPHipVYQFlk7iaRA== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-locate-window@^3.0.0": - version "3.310.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.310.0.tgz#b071baf050301adee89051032bd4139bba32cc40" - integrity sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w== - dependencies: - tslib "^2.5.0" - -"@aws-sdk/util-middleware@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-middleware/-/util-middleware-3.186.0.tgz#ba2e286b206cbead306b6d2564f9d0495f384b40" - integrity sha512-fddwDgXtnHyL9mEZ4s1tBBsKnVQHqTUmFbZKUUKPrg9CxOh0Y/zZxEa5Olg/8dS/LzM1tvg0ATkcyd4/kEHIhg== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-uri-escape@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-uri-escape/-/util-uri-escape-3.186.0.tgz#1752a93dfe58ec88196edb6929806807fd8986da" - integrity sha512-imtOrJFpIZAipAg8VmRqYwv1G/x4xzyoxOJ48ZSn1/ZGnKEEnB6n6E9gwYRebi4mlRuMSVeZwCPLq0ey5hReeQ== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-uri-escape@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-uri-escape/-/util-uri-escape-3.6.1.tgz#433e87458bb510d0e457a86c0acf12b046a5068c" - integrity sha512-tgABiT71r0ScRJZ1pMX0xO0QPMMiISCtumph50IU5VDyZWYgeIxqkMhIcrL1lX0QbNCMgX0n6rZxGrrbjDNavA== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-user-agent-browser@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.186.0.tgz#02e214887d30a69176c6a6c2d6903ce774b013b4" - integrity sha512-fbRcTTutMk4YXY3A2LePI4jWSIeHOT8DaYavpc/9Xshz/WH9RTGMmokeVOcClRNBeDSi5cELPJJ7gx6SFD3ZlQ== - dependencies: - "@aws-sdk/types" "3.186.0" - bowser "^2.11.0" - tslib "^2.3.1" - -"@aws-sdk/util-user-agent-browser@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.6.1.tgz#11b9cc8743392761adb304460f4b54ec8acc2ee6" - integrity sha512-KhJ4VED4QpuBVPXoTjb5LqspX1xHWJTuL8hbPrKfxj+cAaRRW2CNEe7PPy2CfuHtPzP3dU3urtGTachbwNb0jg== - dependencies: - "@aws-sdk/types" "3.6.1" - bowser "^2.11.0" - tslib "^1.8.0" - -"@aws-sdk/util-user-agent-node@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.186.0.tgz#1ef74973442c8650c7b64ff2fd15cf3c09d8c004" - integrity sha512-oWZR7hN6NtOgnT6fUvHaafgbipQc2xJCRB93XHiF9aZGptGNLJzznIOP7uURdn0bTnF73ejbUXWLQIm8/6ue6w== - dependencies: - "@aws-sdk/node-config-provider" "3.186.0" - "@aws-sdk/types" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/util-user-agent-node@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.6.1.tgz#98384095fa67d098ae7dd26f3ccaad028e8aebb6" - integrity sha512-PWwL5EDRwhkXX40m5jjgttlBmLA7vDhHBen1Jcle0RPIDFRVPSE7GgvLF3y4r3SNH0WD6hxqadT50bHQynXW6w== - dependencies: - "@aws-sdk/node-config-provider" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/util-utf8-browser@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.186.0.tgz#5fee6385cfc3effa2be704edc2998abfd6633082" - integrity sha512-n+IdFYF/4qT2WxhMOCeig8LndDggaYHw3BJJtfIBZRiS16lgwcGYvOUmhCkn0aSlG1f/eyg9YZHQG0iz9eLdHQ== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-utf8-browser@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.6.1.tgz#97a8770cae9d29218adc0f32c7798350261377c7" - integrity sha512-gZPySY6JU5gswnw3nGOEHl3tYE7vPKvtXGYoS2NRabfDKRejFvu+4/nNW6SSpoOxk6LSXsrWB39NO51k+G4PVA== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-utf8-browser@^1.0.0-alpha.0": - version "1.0.0-rc.8" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-1.0.0-rc.8.tgz#bf1f1cfed8c024f43a7c43b643fdf2b4523b5973" - integrity sha512-clncPMJ23rxCIkZ9LoUC8SowwZGxWyN2TwRb0XvW/Cv9EavkRgRCOrCpneGyC326lqtMKx36onnpaSRHxErUYw== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-utf8-browser@^3.0.0": - version "3.259.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz#3275a6f5eb334f96ca76635b961d3c50259fd9ff" - integrity sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-utf8-node@3.186.0": - version "3.186.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-node/-/util-utf8-node-3.186.0.tgz#722d9b0f5675ae2e9d79cf67322126d9c9d8d3d8" - integrity sha512-7qlE0dOVdjuRbZTb7HFywnHHCrsN7AeQiTnsWT63mjXGDbPeUWQQw3TrdI20um3cxZXnKoeudGq8K6zbXyQ4iA== - dependencies: - "@aws-sdk/util-buffer-from" "3.186.0" - tslib "^2.3.1" - -"@aws-sdk/util-utf8-node@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-node/-/util-utf8-node-3.6.1.tgz#18534c2069b61f5739ee4cdc70060c9f4b4c4c4f" - integrity sha512-4s0vYfMUn74XLn13rUUhNsmuPMh0j1d4rF58wXtjlVUU78THxonnN8mbCLC48fI3fKDHTmDDkeEqy7+IWP9VyA== - dependencies: - "@aws-sdk/util-buffer-from" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/util-waiter@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-waiter/-/util-waiter-3.6.1.tgz#5c66c2da33ff98468726fefddc2ca7ac3352c17d" - integrity sha512-CQMRteoxW1XZSzPBVrTsOTnfzsEGs8N/xZ8BuBnXLBjoIQmRKVxIH9lgphm1ohCtVHoSWf28XH/KoOPFULQ4Tg== - dependencies: - "@aws-sdk/abort-controller" "3.6.1" - "@aws-sdk/types" "3.6.1" - tslib "^1.8.0" - -"@aws-sdk/xml-builder@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.6.1.tgz#d85d7db5e8e30ba74de93ddf0cf6197e6e4b15ea" - integrity sha512-+HOCH4a0XO+I09okd0xdVP5Q5c9ZsEsDvnogiOcBQxoMivWhPUCo9pjXP3buCvVKP2oDHXQplBKSjGHvGaKFdg== - dependencies: - tslib "^1.8.0" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.21.4", "@babel/code-frame@^7.8.3": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.21.4.tgz#d0fa9e4413aca81f2b23b9442797bda1826edb39" @@ -4226,26 +2123,6 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== -"@turf/boolean-clockwise@6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/boolean-clockwise/-/boolean-clockwise-6.5.0.tgz#34573ecc18f900080f00e4ff364631a8b1135794" - integrity sha512-45+C7LC5RMbRWrxh3Z0Eihsc8db1VGBO5d9BLTOAwU4jR6SgsunTfRWR16X7JUwIDYlCVEmnjcXJNi/kIU3VIw== - dependencies: - "@turf/helpers" "^6.5.0" - "@turf/invariant" "^6.5.0" - -"@turf/helpers@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.5.0.tgz#f79af094bd6b8ce7ed2bd3e089a8493ee6cae82e" - integrity sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw== - -"@turf/invariant@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/invariant/-/invariant-6.5.0.tgz#970afc988023e39c7ccab2341bd06979ddc7463f" - integrity sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg== - dependencies: - "@turf/helpers" "^6.5.0" - "@types/aria-query@^5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" @@ -4314,11 +2191,6 @@ dependencies: "@types/node" "*" -"@types/cookie@^0.3.3": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" - integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== - "@types/eslint-scope@^3.7.3": version "3.7.4" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" @@ -5001,33 +2873,6 @@ ajv@^8.0.0, ajv@^8.6.0, ajv@^8.9.0: require-from-string "^2.0.2" uri-js "^4.2.2" -amazon-cognito-auth-js@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/amazon-cognito-auth-js/-/amazon-cognito-auth-js-1.3.3.tgz#8278a76091300094615a4552b02453e9b970bf9c" - integrity sha512-Auyv8chr5Vw5p1FVAnczWHaXtiDX9jhlmPzB5viSqjLRDDs2UZlTA9aQAC7gaSvwx1b1INJaXvIOgm5Ctm91Nw== - dependencies: - js-cookie "^2.1.4" - -amazon-cognito-identity-js@5.2.14: - version "5.2.14" - resolved "https://registry.yarnpkg.com/amazon-cognito-identity-js/-/amazon-cognito-identity-js-5.2.14.tgz#780d633e2912971e77b00d60d5b959f26d66e6a5" - integrity sha512-9LMgLZfbypbbGTpARQ+QqglE09b1MWti11NXhcD/wPom0uhU/L90dfmUOpTwknz//eE6/dGYf004mJucWzrfxQ== - dependencies: - buffer "4.9.2" - crypto-js "^4.1.1" - fast-base64-decode "^1.0.0" - isomorphic-unfetch "^3.0.0" - js-cookie "^2.2.1" - -amazon-cognito-identity-js@^3.2.7: - version "3.3.3" - resolved "https://registry.yarnpkg.com/amazon-cognito-identity-js/-/amazon-cognito-identity-js-3.3.3.tgz#3c10a91def29b998d0974bf7ce63a262a6d4dd89" - integrity sha512-uB1Bk2ezxVUz0vELZ4tI40ZJEYEZZcWdz8TVyNOPjQCKS+SszNUORTkOkL0KgawZMak7KhDfLTEXbInBeTsiow== - dependencies: - buffer "4.9.1" - crypto-js "^3.1.9-1" - js-cookie "^2.1.4" - ansi-escapes@^4.2.1, ansi-escapes@^4.3.0, ansi-escapes@^4.3.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -5300,33 +3145,6 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -aws-amplify-react@^5.1.9: - version "5.1.43" - resolved "https://registry.yarnpkg.com/aws-amplify-react/-/aws-amplify-react-5.1.43.tgz#aa07b30da2e061fe55959252fc95a5a6726c1a01" - integrity sha512-dhetRbT1qLXrH6p30J8VwVz0t/zzMyMopb/ciMSxhuukfgGo8/8dsiIYgABdrvoZojwhlY24M7RHPAMIPva+Vg== - dependencies: - qrcode.react "^0.8.0" - regenerator-runtime "^0.11.1" - -aws-amplify@^4.3.16: - version "4.3.46" - resolved "https://registry.yarnpkg.com/aws-amplify/-/aws-amplify-4.3.46.tgz#e8897d97796fa5475ed75f37bb590fc9cc4cf013" - integrity sha512-LygkBq+mrV+hFf3DCrVcyYNxFsiYwL0HLN89X1Eg+s3f7df6T2xpjh4JuaDJFbmodEdAlZNfdtRGLMk6ApnPcA== - dependencies: - "@aws-amplify/analytics" "5.2.31" - "@aws-amplify/api" "4.0.64" - "@aws-amplify/auth" "4.6.17" - "@aws-amplify/cache" "4.0.66" - "@aws-amplify/core" "4.7.15" - "@aws-amplify/datastore" "3.14.7" - "@aws-amplify/geo" "1.3.27" - "@aws-amplify/interactions" "4.1.12" - "@aws-amplify/predictions" "4.0.64" - "@aws-amplify/pubsub" "4.5.14" - "@aws-amplify/storage" "4.5.17" - "@aws-amplify/ui" "2.0.7" - "@aws-amplify/xr" "3.0.64" - aws-sdk@^2.649.0: version "2.1379.0" resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1379.0.tgz#2d5609a37287d25edda4395fbb89306939f98890" @@ -5358,13 +3176,6 @@ axe-core@^4.6.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.1.tgz#04392c9ccb3d7d7c5d2f8684f148d56d3442f33d" integrity sha512-sCXXUhA+cljomZ3ZAwb8i1p3oOlkABzPy08ZDAoGcYuvtBPlQ1Ytde129ArXyHWDhfeewq7rlx9F+cUx2SSlkg== -axios@0.26.0: - version "0.26.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.0.tgz#9a318f1c69ec108f8cd5f3c3d390366635e13928" - integrity sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og== - dependencies: - follow-redirects "^1.14.8" - axios@^0.21.1: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" @@ -5550,11 +3361,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base-64@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/base-64/-/base-64-1.0.0.tgz#09d0f2084e32a3fd08c2475b973788eee6ae8f4a" - integrity sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg== - base64-js@^1.0.2: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -5635,11 +3441,6 @@ bootstrap@^5.1.3: resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.2.3.tgz#54739f4414de121b9785c5da3c87b37ff008322b" integrity sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ== -bowser@^2.11.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" - integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -5689,15 +3490,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@4.9.1: - version "4.9.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" - integrity sha512-DNK4ruAqtyHaN8Zne7PkBTO+dD1Lr0YfTduMqlIyjvQIoztBkUxrvL+hKeLW8NXFKHOq/2upkxuoS9znQ9bW9A== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - buffer@4.9.2: version "4.9.2" resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" @@ -5772,7 +3564,7 @@ camelcase-css@^2.0.1: resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== -camelcase-keys@6.2.2, camelcase-keys@^6.2.2: +camelcase-keys@^6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== @@ -6136,11 +3928,6 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -cookie@^0.4.0: - version "0.4.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" - integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== - copy-anything@^2.0.1: version "2.0.6" resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480" @@ -6216,11 +4003,6 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-js@^3.1.9-1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.3.0.tgz#846dd1cce2f68aacfa156c8578f926a609b7976b" - integrity sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q== - crypto-js@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" @@ -6865,7 +4647,7 @@ enhanced-resolve@^5.14.0: graceful-fs "^4.2.4" tapable "^2.2.0" -entities@2.2.0, entities@^2.0.0: +entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== @@ -7301,7 +5083,7 @@ events@1.1.1: resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw== -events@^3.1.0, events@^3.2.0: +events@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -7403,11 +5185,6 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== -fast-base64-decode@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418" - integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -7434,18 +5211,6 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-xml-parser@3.19.0: - version "3.19.0" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz#cb637ec3f3999f51406dd8ff0e6fc4d83e520d01" - integrity sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg== - -fast-xml-parser@^3.16.0: - version "3.21.1" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz#152a1d51d445380f7046b304672dd55d15c9e736" - integrity sha512-FTFVjYoBOZTJekiUsawGsSYV9QL0A+zDYCRj7y34IO6Jg+2IMYEtQa+bbictpdpV8dHxXywqU7C0gRDEOFtBFg== - dependencies: - strnum "^1.0.4" - fastq@^1.6.0: version "1.15.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" @@ -7467,11 +5232,6 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fflate@0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.7.3.tgz#288b034ff0e9c380eaa2feff48c787b8371b7fa5" - integrity sha512-0Zz1jOzJWERhyhsimS54VTqOteCNwRtIlh8isdL0AXLo0g7xNTfTL7oWrkmCnPhZGocKIkWHBistBrrpoNH3aw== - file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -7581,7 +5341,7 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.8: +follow-redirects@^1.0.0, follow-redirects@^1.14.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -7971,11 +5731,6 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== -graphql@15.8.0: - version "15.8.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" - integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== - gzip-size@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" @@ -8303,11 +6058,6 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -idb@5.0.6: - version "5.0.6" - resolved "https://registry.yarnpkg.com/idb/-/idb-5.0.6.tgz#8c94624f5a8a026abe3bef3c7166a5febd1cadc1" - integrity sha512-/PFvOWPzRcEPmlDt5jEvzVZVs0wyd/EvGvkDIcbBpGuMMLQKrTPG0TxvE2UJtgZtCQCmOtM2QD7yQJBVEjKGOw== - idb@^7.0.1: version "7.1.1" resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" @@ -8340,11 +6090,6 @@ image-size@~0.5.0: resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== -immer@9.0.6: - version "9.0.6" - resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73" - integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ== - immer@^9.0.21, immer@^9.0.7: version "9.0.21" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" @@ -8716,14 +6461,6 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isomorphic-unfetch@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f" - integrity sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q== - dependencies: - node-fetch "^2.6.1" - unfetch "^4.2.0" - isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -9308,11 +7045,6 @@ js-base64@^2.4.9: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== -js-cookie@^2.1.4, js-cookie@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" - integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== - js-sdsl@^4.1.4: version "4.4.0" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430" @@ -10080,13 +7812,6 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -node-fetch@^2.6.1: - version "2.6.11" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" - integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== - dependencies: - whatwg-url "^5.0.0" - node-forge@^1: version "1.3.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" @@ -10462,16 +8187,6 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -paho-mqtt@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/paho-mqtt/-/paho-mqtt-1.1.0.tgz#8c10e29eb162e966fb15188d965c3dce505de9d9" - integrity sha512-KPbL9KAB0ASvhSDbOrZBaccXS+/s7/LIofbPyERww8hM5Ko71GUJQ6Nmg0BWqj8phAIT8zdf/Sd/RftHU9i2HA== - -pako@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.4.tgz#6cebc4bbb0b6c73b0d5b8d7e8476e2b2fbea576d" - integrity sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg== - param-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" @@ -11295,7 +9010,7 @@ prop-types-extra@^1.1.0: react-is "^16.3.2" warning "^4.0.0" -prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -11342,19 +9057,6 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== -qr.js@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f" - integrity sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ== - -qrcode.react@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-0.8.0.tgz#413b31cc3b62910e39513f7bead945e01c4c34fb" - integrity sha512-16wKpuFvLwciIq2YAsfmPUCnSR8GrYPsXRK5KVdcIuX0+W/MKZbBkFhl44ttRx4TWZHqRjfztoWOxdPF0Hb9JA== - dependencies: - prop-types "^15.6.0" - qr.js "0.0.0" - qs@6.11.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" @@ -11622,13 +9324,6 @@ react-moment@^1.1.1: resolved "https://registry.yarnpkg.com/react-moment/-/react-moment-1.1.3.tgz#829b21dfb279aa6db47ce4f1ac2555af17a1bcdc" integrity sha512-8EPvlUL8u6EknPp1ISF5MQ3wx2OHJVXIP/iZc4wRh3iV3XozftZERDv9ANZeAtMlhNNQHdFoqcZHFUkBSTONfA== -react-native-get-random-values@^1.4.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.8.0.tgz#1cb4bd4bd3966a356e59697b8f372999fe97cb16" - integrity sha512-H/zghhun0T+UIJLmig3+ZuBCvF66rdbiWUfRSNS6kv5oDSpa1ZiVyvRWtuPesQpT8dXj+Bv7WJRQOUP+5TB1sA== - dependencies: - fast-base64-decode "^1.0.0" - react-oidc-context@^2.2.0: version "2.2.2" resolved "https://registry.yarnpkg.com/react-oidc-context/-/react-oidc-context-2.2.2.tgz#aa9b66596b63f1373d312007099c32b112646396" @@ -11687,7 +9382,7 @@ react-router@5.3.4: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-scripts@^5.0.1: +react-scripts@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003" integrity sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ== @@ -11883,11 +9578,6 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.11.1: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.9: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" @@ -12835,11 +10525,6 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strnum@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" - integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== - style-loader@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.2.tgz#eaebca714d9e462c19aa1e3599057bc363924899" @@ -13173,11 +10858,6 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - trim-newlines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" @@ -13210,12 +10890,12 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.10.0, tslib@^1.11.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.3: +tslib@^1.10.0, tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.5.0: +tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== @@ -13317,11 +10997,6 @@ typescript@^3.8.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== -ulid@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/ulid/-/ulid-2.3.0.tgz#93063522771a9774121a84d126ecd3eb9804071f" - integrity sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw== - unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -13342,11 +11017,6 @@ uncontrollable@^7.2.1: invariant "^2.2.4" react-lifecycles-compat "^3.0.4" -unfetch@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" - integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA== - unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -13391,14 +11061,6 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" -universal-cookie@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-4.0.4.tgz#06e8b3625bf9af049569ef97109b4bb226ad798d" - integrity sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw== - dependencies: - "@types/cookie" "^0.3.3" - cookie "^0.4.0" - universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" @@ -13460,14 +11122,6 @@ url@0.10.3: punycode "1.3.2" querystring "0.2.0" -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ== - dependencies: - punycode "1.3.2" - querystring "0.2.0" - util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -13504,17 +11158,12 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== -uuid@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== - uuid@8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== -uuid@^3.0.0, uuid@^3.2.1, uuid@^3.3.2: +uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -13608,11 +11257,6 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" @@ -13765,14 +11409,6 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - whatwg-url@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" @@ -14163,28 +11799,3 @@ yup@^0.32.11: nanoclone "^0.2.1" property-expr "^2.0.4" toposort "^2.0.2" - -zen-observable-ts@0.8.19: - version "0.8.19" - resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.19.tgz#c094cd20e83ddb02a11144a6e2a89706946b5694" - integrity sha512-u1a2rpE13G+jSzrg3aiCqXU5tN2kw41b+cBZGmnc+30YimdkKiDj9bTowcB41eL77/17RF/h+393AuVgShyheQ== - dependencies: - tslib "^1.9.3" - zen-observable "^0.8.0" - -zen-observable@^0.7.0: - version "0.7.1" - resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.7.1.tgz#f84075c0ee085594d3566e1d6454207f126411b3" - integrity sha512-OI6VMSe0yeqaouIXtedC+F55Sr6r9ppS7+wTbSexkYdHbdt4ctTuPNXP/rwm7GTVI63YBc+EBT0b0tl7YnJLRg== - -zen-observable@^0.8.0: - version "0.8.15" - resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" - integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== - -zen-push@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/zen-push/-/zen-push-0.2.1.tgz#ddc33b90f66f9a84237d5f1893970f6be60c3c28" - integrity sha512-Qv4qvc8ZIue51B/0zmeIMxpIGDVhz4GhJALBvnKs/FRa2T7jy4Ori9wFwaHVt0zWV7MIFglKAHbgnVxVTw7U1w== - dependencies: - zen-observable "^0.7.0" diff --git a/functions/authorizer/pom.xml b/functions/authorizer/pom.xml index b3069d1b..7219b9c3 100644 --- a/functions/authorizer/pom.xml +++ b/functions/authorizer/pom.xml @@ -33,6 +33,7 @@ limitations under the License. + ${project.basedir}/../.. 0 @@ -53,25 +54,38 @@ limitations under the License. + + org.jacoco + jacoco-maven-plugin + org.apache.maven.plugins maven-assembly-plugin - pl.project13.maven - git-commit-id-plugin + io.github.git-commit-id + git-commit-id-maven-plugin + + + com.github.spotbugs + spotbugs-maven-plugin + + Max + medium + + + software.amazon.lambda.snapstart + aws-lambda-snapstart-java-rules + 0.1.0 + + + ${project.basedir}/src/main/resources/spotbugs-exclude.xml + - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - com.fasterxml.jackson.core jackson-annotations diff --git a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayAuthorizer.java b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayAuthorizer.java index ce203a99..998ee3ef 100644 --- a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayAuthorizer.java +++ b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayAuthorizer.java @@ -18,6 +18,7 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import com.auth0.jwt.interfaces.DecodedJWT; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.regions.Region; @@ -27,13 +28,20 @@ import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.StandardCharsets; -import java.util.HashMap; +import java.util.*; public class ApiGatewayAuthorizer implements RequestStreamHandler { private static final Logger LOGGER = LoggerFactory.getLogger(ApiGatewayAuthorizer.class); private static final String AWS_REGION = System.getenv("AWS_REGION"); + private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); private static final String IDENTITY_PROVIDER = System.getenv("IDENTITY_PROVIDER"); + static final String ADMIN_WEB_APP_CLIENT_ID = System.getenv("ADMIN_WEB_APP_CLIENT_ID"); + static final String API_APP_CLIENT_ID = System.getenv("API_APP_CLIENT_ID"); + static final String PRIVATE_API_APP_CLIENT_ID = System.getenv("PRIVATE_API_APP_CLIENT_ID"); + static final String READ_SCOPE = "saas-boost/" + SAAS_BOOST_ENV + "/read"; + static final String WRITE_SCOPE = "saas-boost/" + SAAS_BOOST_ENV + "/write"; + static final String PRIVATE_SCOPE = "saas-boost/" + SAAS_BOOST_ENV + "/private"; private final Authorizer authorizer; public ApiGatewayAuthorizer() { @@ -63,7 +71,8 @@ public void handleRequest(InputStream input, OutputStream output, Context contex LOGGER.info(Utils.toJson(event)); AuthorizerResponse response; - if (!authorizer.verifyToken(event)) { + DecodedJWT token = authorizer.verifyToken(event); + if (token == null) { LOGGER.error("JWT not verified. Returning Not Authorized"); response = AuthorizerResponse.builder() .principalId(event.getAccountId()) @@ -79,12 +88,39 @@ public void handleRequest(InputStream input, OutputStream output, Context contex .build(); } else { LOGGER.info("JWT verified. Returning Authorized."); + LOGGER.debug(Utils.toJson(token)); + + List resources = new ArrayList<>(); + String scopes = token.getClaim("scope").asString(); + if (ADMIN_WEB_APP_CLIENT_ID.equals(authorizer.getClientId(token))) { + List groups = authorizer.getGroups(token); + if (groups != null && groups.contains("admin")) { + LOGGER.debug("Token includes admin group, adding read/write scopes"); + scopes = scopes + " " + READ_SCOPE + " " + WRITE_SCOPE; + } else { + LOGGER.error("Admin web app client does not contain RBAC groups!"); + } + } + //LOGGER.info("Access token scopes {}", scopes); + if (scopes.contains(READ_SCOPE)) { + LOGGER.info("Adding READ scope resources"); + resources.addAll(readApiResources(event)); + } + if (scopes.contains(WRITE_SCOPE)) { + LOGGER.info("Adding WRITE scope resources"); + resources.addAll(writeApiResources(event)); + } + if (scopes.contains(PRIVATE_SCOPE)) { + LOGGER.info("Adding PRIVATE scope resources"); + resources.addAll(privateApiResources(event)); + } + response = AuthorizerResponse.builder() .principalId(event.getAccountId()) .policyDocument(PolicyDocument.builder() .statement(Statement.builder() .effect("Allow") - .resource(ApiGatewayAuthorizer.apiGatewayResource(event)) + .resource(resources) .build() ) .build() @@ -108,9 +144,8 @@ public static String apiGatewayResource(TokenAuthorizerRequest event) { } public static String apiGatewayResource(TokenAuthorizerRequest event, String method, String resource) { - String partition = Region.of(AWS_REGION).metadata().partition().id(); String arn = String.format("arn:%s:execute-api:%s:%s:%s/%s/%s/%s", - partition, + Region.of(event.getRegion()).metadata().partition().id(), event.getRegion(), event.getAccountId(), event.getApiId(), @@ -120,4 +155,76 @@ public static String apiGatewayResource(TokenAuthorizerRequest event, String met ); return arn; } + + public static List readApiResources(TokenAuthorizerRequest event) { + Set> readResources = new LinkedHashSet<>(); + readResources.add(new AbstractMap.SimpleEntry<>("api", "GET")); + readResources.add(new AbstractMap.SimpleEntry<>("api/*", "GET")); + readResources.add(new AbstractMap.SimpleEntry<>("billing/plans", "GET")); + //readResources.add(new AbstractMap.SimpleEntry<>("metrics/alb/*", "GET")); + //readResources.add(new AbstractMap.SimpleEntry<>("metrics/datasets", "GET")); + //readResources.add(new AbstractMap.SimpleEntry<>("metrics/query", "POST")); // Yes, this is a "read" resource + readResources.add(new AbstractMap.SimpleEntry<>("onboarding", "GET")); + readResources.add(new AbstractMap.SimpleEntry<>("onboarding/*", "GET")); + readResources.add(new AbstractMap.SimpleEntry<>("settings", "GET")); + readResources.add(new AbstractMap.SimpleEntry<>("settings/*", "GET")); + readResources.add(new AbstractMap.SimpleEntry<>("appconfig", "GET")); + readResources.add(new AbstractMap.SimpleEntry<>("appconfig/*", "GET")); + readResources.add(new AbstractMap.SimpleEntry<>("appconfig/options", "GET")); + readResources.add(new AbstractMap.SimpleEntry<>("sysusers", "GET")); + readResources.add(new AbstractMap.SimpleEntry<>("sysusers/*", "GET")); + readResources.add(new AbstractMap.SimpleEntry<>("tenants", "GET")); + readResources.add(new AbstractMap.SimpleEntry<>("tenants/*", "GET")); + readResources.add(new AbstractMap.SimpleEntry<>("tiers", "GET")); + readResources.add(new AbstractMap.SimpleEntry<>("tiers/*", "GET")); + readResources.add(new AbstractMap.SimpleEntry<>("identity", "GET")); + readResources.add(new AbstractMap.SimpleEntry<>("identity/providers", "GET")); + + List resources = new ArrayList<>(); + for (Map.Entry resource : readResources) { + resources.add(apiGatewayResource(event, resource.getValue(), resource.getKey())); + } + return resources; + } + + public static List writeApiResources(TokenAuthorizerRequest event) { + Set> writeResources = new LinkedHashSet<>(); + writeResources.add(new AbstractMap.SimpleEntry<>("onboarding*", "POST")); + writeResources.add(new AbstractMap.SimpleEntry<>("appconfig*", "PUT")); + writeResources.add(new AbstractMap.SimpleEntry<>("sysusers*", "POST")); + writeResources.add(new AbstractMap.SimpleEntry<>("sysusers/*", "DELETE")); + writeResources.add(new AbstractMap.SimpleEntry<>("sysusers/*", "PUT")); + writeResources.add(new AbstractMap.SimpleEntry<>("sysusers/*/disable", "PATCH")); + writeResources.add(new AbstractMap.SimpleEntry<>("sysusers/*/enable", "PATCH")); + writeResources.add(new AbstractMap.SimpleEntry<>("tenants/*", "DELETE")); + writeResources.add(new AbstractMap.SimpleEntry<>("tenants/*", "PUT")); + writeResources.add(new AbstractMap.SimpleEntry<>("tenants/*/disable", "PATCH")); + writeResources.add(new AbstractMap.SimpleEntry<>("tenants/*/enable", "PATCH")); + writeResources.add(new AbstractMap.SimpleEntry<>("tiers*", "POST")); + writeResources.add(new AbstractMap.SimpleEntry<>("tiers/*", "PUT")); + writeResources.add(new AbstractMap.SimpleEntry<>("tiers/*", "DELETE")); + writeResources.add(new AbstractMap.SimpleEntry<>("identity*", "POST")); + writeResources.add(new AbstractMap.SimpleEntry<>("metrics*", "POST")); + writeResources.add(new AbstractMap.SimpleEntry<>("settings/*", "PUT")); + + List resources = new ArrayList<>(); + for (Map.Entry resource : writeResources) { + resources.add(apiGatewayResource(event, resource.getValue(), resource.getKey())); + } + return resources; + } + + public static List privateApiResources(TokenAuthorizerRequest event) { + Set> privateResources = new LinkedHashSet<>(); + privateResources.add(new AbstractMap.SimpleEntry<>("quotas/check", "GET")); + privateResources.add(new AbstractMap.SimpleEntry<>("appconfig", "DELETE")); + privateResources.add(new AbstractMap.SimpleEntry<>("settings/*/secret", "GET")); + privateResources.add(new AbstractMap.SimpleEntry<>("tenants*", "POST")); + + List resources = new ArrayList<>(); + for (Map.Entry resource : privateResources) { + resources.add(apiGatewayResource(event, resource.getValue(), resource.getKey())); + } + return resources; + } } \ No newline at end of file diff --git a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Authorizer.java b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Authorizer.java index 40749957..59413f5a 100644 --- a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Authorizer.java +++ b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Authorizer.java @@ -16,7 +16,15 @@ package com.amazon.aws.partners.saasfactory.saasboost; +import com.auth0.jwt.interfaces.DecodedJWT; + +import java.util.List; + public interface Authorizer { - boolean verifyToken(TokenAuthorizerRequest request); + DecodedJWT verifyToken(TokenAuthorizerRequest request); + + String getClientId(DecodedJWT token); + + List getGroups(DecodedJWT token); } diff --git a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoAuthorizer.java b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoAuthorizer.java index 9f1b3470..c745b29c 100644 --- a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoAuthorizer.java +++ b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoAuthorizer.java @@ -19,33 +19,46 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.JWTVerifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; + public class CognitoAuthorizer implements Authorizer { private static final Logger LOGGER = LoggerFactory.getLogger(CognitoAuthorizer.class); @Override - public boolean verifyToken(TokenAuthorizerRequest request) { - // TODO add aud claim and pass client id in to the Lambda as an env variable + public DecodedJWT verifyToken(TokenAuthorizerRequest request) { JWTVerifier verifier = JWT .require(Algorithm.RSA256(new CognitoKeyProvider())) .acceptLeeway(5L) // Allowed seconds of clock skew between token issuer and verifier - .withClaim("token_use", (claim, token) -> ( - // Per Cognito documentation, make sure we got an Access or Identity token - // (not a refresh token) - "access".equals(claim.asString()) || "id".equals(claim.asString())) + .withClaim("token_use", "access") + .withClaim("client_id", (claim, token) -> ( + List.of(ApiGatewayAuthorizer.ADMIN_WEB_APP_CLIENT_ID, + ApiGatewayAuthorizer.API_APP_CLIENT_ID, + ApiGatewayAuthorizer.PRIVATE_API_APP_CLIENT_ID).contains(claim.asString()) + ) ) .build(); - boolean valid = false; + DecodedJWT token = null; try { - verifier.verify(request.tokenPayload()); - valid = true; + token = verifier.verify(request.tokenPayload()); } catch (JWTVerificationException e) { LOGGER.error(Utils.getFullStackTrace(e)); } - return valid; + return token; + } + + @Override + public String getClientId(DecodedJWT token) { + return token.getClaim("client_id").asString(); + } + + @Override + public List getGroups(DecodedJWT token) { + return token.getClaim("cognito:groups").asList(String.class); } } diff --git a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/KeycloakAuthorizer.java b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/KeycloakAuthorizer.java index 36154cac..27c54097 100644 --- a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/KeycloakAuthorizer.java +++ b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/KeycloakAuthorizer.java @@ -19,27 +19,47 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.JWTVerifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; + public class KeycloakAuthorizer implements Authorizer { private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAuthorizer.class); @Override - public boolean verifyToken(TokenAuthorizerRequest request) { + public DecodedJWT verifyToken(TokenAuthorizerRequest request) { JWTVerifier verifier = JWT .require(Algorithm.RSA256(new KeycloakKeyProvider())) .acceptLeeway(5L) // Allowed seconds of clock skew between token issuer and verifier + .withClaim("typ", "Bearer") + .withClaim("azp", (claim, token) -> ( + List.of(ApiGatewayAuthorizer.ADMIN_WEB_APP_CLIENT_ID, + ApiGatewayAuthorizer.API_APP_CLIENT_ID, + ApiGatewayAuthorizer.PRIVATE_API_APP_CLIENT_ID).contains(claim.asString()) + ) + ) .build(); - boolean valid = false; + DecodedJWT token = null; try { - verifier.verify(request.tokenPayload()); - valid = true; + token = verifier.verify(request.tokenPayload()); } catch (JWTVerificationException e) { LOGGER.error(Utils.getFullStackTrace(e)); } - return valid; + return token; + } + + @Override + public String getClientId(DecodedJWT token) { + // Also available as "claimId" when using a confidential client + return token.getClaim("azp").asString(); + } + + @Override + public List getGroups(DecodedJWT token) { + return token.getClaim("groups").asList(String.class); } } diff --git a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Statement.java b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Statement.java index ea362b06..20c73264 100644 --- a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Statement.java +++ b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Statement.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; @@ -76,8 +77,14 @@ public Builder resource(String... resource) { if (resource != null && resource.length > 0) { resources.clear(); Collections.addAll(this.resources, resource); - } else { - this.resources = new ArrayList<>(List.of("*")); + } + return this; + } + + public Builder resource(Collection resource) { + if (resource != null && !resource.isEmpty()) { + resources.clear(); + resources.addAll(resource); } return this; } diff --git a/functions/authorizer/src/main/resources/spotbugs-exclude.xml b/functions/authorizer/src/main/resources/spotbugs-exclude.xml new file mode 100644 index 00000000..710974a4 --- /dev/null +++ b/functions/authorizer/src/main/resources/spotbugs-exclude.xml @@ -0,0 +1,7 @@ + + + + diff --git a/functions/codepipeline-wait-handler/pom.xml b/functions/codepipeline-wait-handler/pom.xml index 782b6f39..5580c738 100644 --- a/functions/codepipeline-wait-handler/pom.xml +++ b/functions/codepipeline-wait-handler/pom.xml @@ -33,6 +33,7 @@ limitations under the License. + ${project.basedir}/../.. 0 @@ -59,13 +60,6 @@ limitations under the License. - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - com.amazon.aws.partners.saasfactory.saasboost CloudFormationUtils diff --git a/functions/codepipeline-wait-handler/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CodePipelineWaitHandler.java b/functions/codepipeline-wait-handler/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CodePipelineWaitHandler.java index 01b4e7d9..2f095c3c 100644 --- a/functions/codepipeline-wait-handler/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CodePipelineWaitHandler.java +++ b/functions/codepipeline-wait-handler/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CodePipelineWaitHandler.java @@ -34,7 +34,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -public class CodePipelineWaitHandler implements RequestHandler, Object> { +public class CodePipelineWaitHandler implements RequestHandler, Void> { private static final Logger LOGGER = LoggerFactory.getLogger(CodePipelineWaitHandler.class); private final CodePipelineClient codepipeline; @@ -45,7 +45,7 @@ public CodePipelineWaitHandler() { } @Override - public Object handleRequest(Map event, Context context) { + public Void handleRequest(Map event, Context context) { //logRequestEvent(event); Map job = (Map) event.get("CodePipeline.job"); @@ -65,7 +65,7 @@ public Object handleRequest(Map event, Context context) { // Pre signed S3 URL String waitHandle = (String) params.get("waitHandle"); - // Since there's not built-in way to have dynamic CodePipeline stage actions, + // Since there's no built-in way to have dynamic CodePipeline stage actions, // and because this pipeline could be triggered outside of CloudFormation, the // wait condition handle mey be irrelevant. Skip it if it's out of date. if (signalCloudFormation(waitHandle)) { @@ -90,7 +90,6 @@ public Object handleRequest(Map event, Context context) { failJob(jobId, "Error ", context); throw e; } - return null; } diff --git a/functions/core-stack-listener/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CoreStackListener.java b/functions/core-stack-listener/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CoreStackListener.java deleted file mode 100644 index 8b9df42a..00000000 --- a/functions/core-stack-listener/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CoreStackListener.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SNSEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.core.exception.SdkServiceException; -import software.amazon.awssdk.services.cloudformation.CloudFormationClient; -import software.amazon.awssdk.services.cloudformation.model.ListStackResourcesResponse; -import software.amazon.awssdk.services.cloudformation.model.ResourceStatus; -import software.amazon.awssdk.services.cloudformation.model.StackResourceSummary; -import software.amazon.awssdk.services.ecr.EcrClient; -import software.amazon.awssdk.services.ecr.model.ListTagsForResourceResponse; -import software.amazon.awssdk.services.ecr.model.Tag; -import software.amazon.awssdk.services.eventbridge.EventBridgeClient; - -import java.util.*; - -public class CoreStackListener implements RequestHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(CoreStackListener.class); - private static final String AWS_REGION = System.getenv("AWS_REGION"); - private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); - private static final String SAAS_BOOST_EVENT_BUS = System.getenv("SAAS_BOOST_EVENT_BUS"); - private static final String EVENT_SOURCE = "saas-boost"; - private static final Collection EVENTS_OF_INTEREST = Collections.unmodifiableCollection( - Arrays.asList("CREATE_COMPLETE", "UPDATE_COMPLETE")); - private final CloudFormationClient cfn; - private final EventBridgeClient eventBridge; - private final EcrClient ecr; - - public CoreStackListener() { - final long startTimeMillis = System.currentTimeMillis(); - if (Utils.isBlank(AWS_REGION)) { - throw new IllegalStateException("Missing required environment variable AWS_REGION"); - } - if (Utils.isBlank(SAAS_BOOST_EVENT_BUS)) { - throw new IllegalStateException("Missing required environment variable SAAS_BOOST_EVENT_BUS"); - } - this.cfn = Utils.sdkClient(CloudFormationClient.builder(), CloudFormationClient.SERVICE_NAME); - this.eventBridge = Utils.sdkClient(EventBridgeClient.builder(), EventBridgeClient.SERVICE_NAME); - this.ecr = Utils.sdkClient(EcrClient.builder(), EcrClient.SERVICE_NAME); - LOGGER.info("Constructor init: {}", System.currentTimeMillis() - startTimeMillis); - } - - @Override - public Object handleRequest(SNSEvent event, Context context) { - LOGGER.info(Utils.toJson(event)); - - // ARN: arn:::::/ - final String[] thisLambdaArn = context.getInvokedFunctionArn().split(":"); - final String partition = thisLambdaArn[1]; - final String region = thisLambdaArn[3]; - final String accountId = thisLambdaArn[4]; - - List records = event.getRecords(); - SNSEvent.SNS sns = records.get(0).getSNS(); - String message = sns.getMessage(); - - CloudFormationEvent cloudFormationEvent = CloudFormationEventDeserializer.deserialize(message); - - // CloudFormation sends SNS notifications for every resource in a stack going through each status change. - // We want to process the resources of the saas-boost-core.yaml CloudFormation stack only after the stack - // has finished being created or updated so we don't trigger anything downstream prematurely. - if (filter(cloudFormationEvent)) { - String stackName = cloudFormationEvent.getStackName(); - String stackStatus = cloudFormationEvent.getResourceStatus(); - LOGGER.info("Stack " + stackName + " is in status " + stackStatus); - - // We're looking for ECR repository resources in a CREATE_COMPLETE state. There could be multiple - // ECR repos provisioned depending on how the application services are configured. - try { - ListStackResourcesResponse resources = cfn.listStackResources(req -> req - .stackName(cloudFormationEvent.getStackId()) - ); - Map appConfig = new HashMap<>(); - Map services = new HashMap<>(); - String tenantStorageBucketName = null; - for (StackResourceSummary resource : resources.stackResourceSummaries()) { - if (ResourceStatus.CREATE_COMPLETE.equals(resource.resourceStatus()) - || ResourceStatus.UPDATE_COMPLETE.equals(resource.resourceStatus())) { - if (AwsResource.ECR_REPO.getResourceType().equals(resource.resourceType())) { - String ecrRepo = resource.physicalResourceId(); - String ecrResourceArn = AwsResource.ECR_REPO.formatArn( - partition, region, accountId, ecrRepo); - LOGGER.info("Listing tags for ECR repo {}", ecrRepo); - ListTagsForResourceResponse response = ecr.listTagsForResource(request -> request - .resourceArn(ecrResourceArn)); - String serviceName = resource.logicalResourceId(); - String serviceNameContext = "Read from Template"; - if (response.hasTags()) { - for (Tag tag : response.tags()) { - if ("Name".equalsIgnoreCase(tag.key())) { - serviceName = tag.value(); - serviceNameContext = "Read from Tag"; - } - } - } - LOGGER.info("Publishing appConfig update event for ECR repository {}({}) {}", - serviceName, - serviceNameContext, - ecrRepo); - // TODO if in the future we support alternate compute options, ECS may not be - // TODO the right one to specify here however, this coreStackListener has no - // TODO extra information about the container type without the macro providing - // TODO more information when adding the ECR repo to the stack - services.put(serviceName, Map.of("compute", Map.of( - "type", "ECS", - "containerRepo", ecrRepo))); - } - // The object storage extension creates a single S3 bucket for the entire application - // with a separate "folder" for each service. - if ("AWS::S3::Bucket".equals(resource.resourceType()) - && "TenantStorage".equals(resource.logicalResourceId())) { - tenantStorageBucketName = resource.physicalResourceId(); - LOGGER.info("Updating appConfig for TenantStorageBucket {}", - tenantStorageBucketName); - } - } - } - // add the tenantStorageBucketName to each ServiceConfig - if (tenantStorageBucketName != null) { - for (String serviceName : services.keySet()) { - Map service = new HashMap((Map) services.get(serviceName)); - service.put("s3", Map.of("bucketName", tenantStorageBucketName)); - services.put(serviceName, service); - } - } - // Only fire one event for all the app config resources changes by this stack - if (!services.isEmpty()) { - appConfig.put("services", services); - } - if (!appConfig.isEmpty()) { - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Application Configuration Resource Changed", - appConfig); - } - } catch (SdkServiceException cfnError) { - LOGGER.error("cfn:ListStackResources error", cfnError); - LOGGER.error(Utils.getFullStackTrace(cfnError)); - throw cfnError; - } - } - return null; - } - - protected static boolean filter(CloudFormationEvent cloudFormationEvent) { - return ("AWS::CloudFormation::Stack".equals(cloudFormationEvent.getResourceType()) - && cloudFormationEvent.getStackName().startsWith("sb-" + SAAS_BOOST_ENV + "-core-") - && EVENTS_OF_INTEREST.contains(cloudFormationEvent.getResourceStatus())); - } - -} diff --git a/functions/core-stack-listener/update.sh b/functions/core-stack-listener/update.sh deleted file mode 100755 index 82c2ae43..00000000 --- a/functions/core-stack-listener/update.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -if [ -z $1 ]; then - echo "Usage: $0 [Lambda Folder]" - exit 2 -fi - -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -echo "AWS Region = $MY_AWS_REGION" - -ENVIRONMENT=$1 -LAMBDA_STAGE_FOLDER=$2 -if [ -z $LAMBDA_STAGE_FOLDER ]; then - LAMBDA_STAGE_FOLDER="lambdas" -fi -LAMBDA_CODE=CoreStackListener-lambda.zip - -#set this for V2 AWS CLI to disable paging -export AWS_PAGER="" - -SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) -echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" -if [ -z $SAAS_BOOST_BUCKET ]; then - echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" - exit 1 -fi - -# Do a fresh build of the project -mvn -if [ $? -ne 0 ]; then - echo "Error building project" - exit 1 -fi - -# And copy it up to S3 -aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ - -eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-core-stack-listener\`)] | [].FunctionName' --output text"\) - -for FUNCTION in ${FUNCTIONS[@]}; do - #echo $FUNCTION - aws lambda --region $MY_AWS_REGION update-function-code --function-name $FUNCTION --s3-bucket $SAAS_BOOST_BUCKET --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE -done diff --git a/functions/ecs-service-update/pom.xml b/functions/ecs-service-update/pom.xml index b7e641f4..78ea618a 100644 --- a/functions/ecs-service-update/pom.xml +++ b/functions/ecs-service-update/pom.xml @@ -33,6 +33,7 @@ limitations under the License. + ${project.basedir}/../.. 5 @@ -59,13 +60,6 @@ limitations under the License. - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - software.amazon.awssdk ecs diff --git a/functions/ecs-service-update/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/EcsServiceUpdateTest.java b/functions/ecs-service-update/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/EcsServiceUpdateTest.java index 24ff28ab..0c71f939 100644 --- a/functions/ecs-service-update/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/EcsServiceUpdateTest.java +++ b/functions/ecs-service-update/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/EcsServiceUpdateTest.java @@ -15,14 +15,12 @@ */ package com.amazon.aws.partners.saasfactory.saasboost; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.junit.Assert.*; - public class EcsServiceUpdateTest { @Test diff --git a/functions/ecs-shutdown-services/README.md b/functions/ecs-shutdown-services/README.md deleted file mode 100644 index 51fd156f..00000000 --- a/functions/ecs-shutdown-services/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# ECS Shutdown Services - -## Overview -This function can be used to gracefully shutdown all application workloads running for your provisioned tenants. It does this by setting the `desiredCount` attribute of each of your ECS services for each tenant to zero (0). [Watch a detailed walkthru](https://www.twitch.tv/videos/1065389231) of building this solution during our [Office Hours](https://github.com/awslabs/aws-saas-boost/discussions/106). - -## Why would you turn off your SaaS application? -Excellent question! You should **not** use this function for your production environments. Your SaaS customers expect your service to be available at all times. However, for development and other non-production environments, it may be useful to temporarily shutdown your application tasks as a way to save operational costs. ECS tasks that run on Fargate are only billed when they are running. If you are running your tasks on EC2, the ECS capacity provider will shutdown the EC2 instances in your ECS cluster. - -## How do I use it? -Because you should not use this feature in production, SaaS Boost will not automatically turn it on during install. The function and supporting resources like log groups and IAM policies will be installed and ready-to-use. - -The common use case will be to trigger this function on a schedule. For example, you may choose to shutdown your application services overnight in a development environment. Amazon EventBridge supports [triggering Lambda functions on a schedule](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html). See the sample [enable.sh](enable.sh) script for one way to configure EventBridge with the EcsShutdownServices function. - -You can also simply invoke the Lambda function manually with the AWS CLI `aws lambda invoke --function-name sb-${SAAS_BOOST_ENV}-ecs-shutdown-services response.json`. - -Once you've shutdown your application tasks to save money, you'll probably want to turn them back on. See [ECS Startup Services](../ecs-startup-services/README.md). \ No newline at end of file diff --git a/functions/ecs-shutdown-services/disable.sh b/functions/ecs-shutdown-services/disable.sh deleted file mode 100755 index 928072d9..00000000 --- a/functions/ecs-shutdown-services/disable.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# What SaaS Boost installation are we working on? -SAAS_BOOST_ENV=$1 -if [ -z "$SAAS_BOOST_ENV" ]; then - read -p "Enter your AWS SaaS Boost Environment label: " SAAS_BOOST_ENV - if [ -z "$SAAS_BOOST_ENV" ]; then - echo "You must enter a AWS SaaS Boost Environment label to continue. Exiting." - exit 1 - fi -fi -# Can we confirm that this AWS CLI is connected to that SaaS Boost environment? -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -SB_ENV=$(aws --region ${MY_AWS_REGION} ssm get-parameter --name /saas-boost/${SAAS_BOOST_ENV}/SAAS_BOOST_ENVIRONMENT --query "Parameter.Value" --output text) -if [ "${SB_ENV}" != "${SAAS_BOOST_ENV}" ]; then - echo "Can't find SaaS Boost environment $SAAS_BOOST_ENV in region $MY_AWS_REGION. Double-check the current AWS CLI profile and region." - exit 1 -fi - -RULE="sb-${SAAS_BOOST_ENV}-ecs-shutdown-services" -EXISTING_RULE=$(aws events describe-rule --name "${RULE}") -if [ $? != 0 ]; then - echo "Can't find the EventBridge rule ${RULE}" - exit 1 -else - aws events disable-rule --name "${RULE}" -fi diff --git a/functions/ecs-shutdown-services/enable.sh b/functions/ecs-shutdown-services/enable.sh deleted file mode 100755 index f86f31eb..00000000 --- a/functions/ecs-shutdown-services/enable.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/bin/bash -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# What SaaS Boost installation are we working on? -SAAS_BOOST_ENV=$1 -if [ -z "$SAAS_BOOST_ENV" ]; then - read -p "Enter your AWS SaaS Boost Environment label: " SAAS_BOOST_ENV - if [ -z "$SAAS_BOOST_ENV" ]; then - echo "You must enter a AWS SaaS Boost Environment label to continue. Exiting." - exit 1 - fi -fi -# Can we confirm that this AWS CLI is connected to that SaaS Boost environment? -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -SB_ENV=$(aws --region ${MY_AWS_REGION} ssm get-parameter --name /saas-boost/${SAAS_BOOST_ENV}/SAAS_BOOST_ENVIRONMENT --query "Parameter.Value" --output text) -if [ "${SB_ENV}" != "${SAAS_BOOST_ENV}" ]; then - echo "Can't find SaaS Boost environment $SAAS_BOOST_ENV in region $MY_AWS_REGION. Double-check the current AWS CLI profile and region." - exit 1 -fi - -# Has the EcsShutdownServices Lambda function been setup? -LAMBDA_FX="sb-${SAAS_BOOST_ENV}-ecs-shutdown-services" -LAMBDA_ARN=$(aws lambda get-function --function-name "${LAMBDA_FX}" --query "Configuration.FunctionArn" --output text) -if [ $? != 0 ]; then - echo "Can't find the EcsShutdownServices Lambda function in this SaaS Boost environment." - exit 1 -fi - -RULE="sb-${SAAS_BOOST_ENV}-ecs-shutdown-services" -EXISTING_SCHEDULE=$(aws events describe-rule --name "${RULE}" --query "ScheduleExpression" --output text) -if [ $? == 0 ] && [ ! -z "${EXISTING_SCHEDULE}" ]; then - read -p "Reuse the existing schedule expression ${EXISTING_SCHEDULE}? [Y/N] " REUSE_SCHEDULE -fi -if ! [[ $REUSE_SCHEDULE =~ ^[Yy] ]]; then - # Get a cron schedule to invoke our Lambda on - echo "Enter an EventBridge cron schedule expression (https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html#eb-cron-expressions)." - read -p "[Press enter for default schedule of 12 midnight daily, cron(0 0 * * ? *)]: " SCHEDULE - if [ -z "$SCHEDULE" ]; then - SCHEDULE="cron(0 0 * * ? *)" - fi -else - SCHEDULE="${EXISTING_SCHEDULE}" -fi - -# Simple pattern check -- doesn't test for EventBridge restriction on -# day-of-month and day-of-week not both being asterisks -CRON_REGEX="^cron\(.+ .+ .+ .+ .+ .+\)$" -if ! [[ $SCHEDULE =~ $CRON_REGEX ]]; then - echo "Schedule cron expression ${SCHEDULE} is not valid." - exit 1 -fi -#echo "Schedule = $SCHEDULE" - -# Now we can make or update an EventBridge rule for this schedule -RULE_ARN=$(aws events put-rule --name "${RULE}" --schedule-expression "${SCHEDULE}" --state ENABLED --description "Shuts down all tenant tasks in ECS" --query "RuleArn" --output text) -if [ $? != 0 ]; then - echo "Error putting scheduled event rule" - exit 1 -fi -#echo $RULE_ARN -echo "Set EventBridge scheduled event rule to ${SCHEDULE}" - -# Adding a Lambda permission with the same statement id is an error. -# Unfortunately, the get-policy call returns the policy JSON as an -# escaped string rather than a proper JSON structure, so we can't use -# --query like we normally would. We could always call remove-permission -# before add-permission... but, this seems more correct. -STATEMENT_ID="sb-${SAAS_BOOST_ENV}-ecs-shutdown-services-permission" -GREP_PATTERN="\"Sid\":\"${STATEMENT_ID}\"" -EXISTING_PERMISSION=$(aws lambda get-policy --function-name ${LAMBDA_FX} --query "Policy" --output text | grep $GREP_PATTERN) -if [ $? == 0 ]; then - echo "Lambda permission for EventBridge rule already exists" -else - LAMBDA_PERMISSION=$(aws lambda add-permission --function-name ${LAMBDA_FX} --action 'lambda:InvokeFunction' --principal events.amazonaws.com --source-arn ${RULE_ARN} --statement-id ${STATEMENT_ID}) - if [ $? != 0 ]; then - echo "Error adding Lambda permission for EventBridge rule" - exit 1 - fi - #echo $LAMBDA_PERMISSION - echo "Added Lambda function permission for EventBridge rule" -fi - -# Finally, wire together the EventBridge scheduled rule with the Lambda function -EVENT_TARGET=$(aws events put-targets --rule "${RULE}" --targets "Id"="EcsShutdownServicesLambda","Arn"="${LAMBDA_ARN}") -if [ $? != 0 ]; then - echo "Error putting EventBridge target for rule ${RULE} to function ${LAMBDA_FX}" - exit 1 -fi -#echo $EVENT_TARGET -echo "Set EventBridge target for rule ${RULE} to function ${LAMBDA_FX}" - diff --git a/functions/ecs-shutdown-services/pom.xml b/functions/ecs-shutdown-services/pom.xml deleted file mode 100644 index 74ed8c4d..00000000 --- a/functions/ecs-shutdown-services/pom.xml +++ /dev/null @@ -1,93 +0,0 @@ - - - - 4.0.0 - - com.amazon.aws.partners.saasfactory.saasboost - saasboost-functions - 1.0.0 - - EcsShutdownServices - 1.0.0 - jar - - - Apache-2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - - - 1 - - - - ${project.artifactId} - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - org.apache.maven.plugins - maven-assembly-plugin - - - io.github.git-commit-id - git-commit-id-maven-plugin - - - - - - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - - - com.amazon.aws.partners.saasfactory.saasboost - ApiGatewayHelper - 1.0.0 - - provided - - - software.amazon.awssdk - ecs - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - - diff --git a/functions/ecs-shutdown-services/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/EcsShutdownServices.java b/functions/ecs-shutdown-services/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/EcsShutdownServices.java deleted file mode 100644 index 44c3585a..00000000 --- a/functions/ecs-shutdown-services/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/EcsShutdownServices.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.core.exception.SdkServiceException; -import software.amazon.awssdk.services.ecs.EcsClient; -import software.amazon.awssdk.services.ecs.model.DescribeServicesResponse; -import software.amazon.awssdk.services.ecs.model.Service; - -import java.util.*; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; - -public class EcsShutdownServices implements RequestHandler, Object> { - - private static final Logger LOGGER = LoggerFactory.getLogger(EcsShutdownServices.class); - private static final String AWS_REGION = System.getenv("AWS_REGION"); - private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); - private static final String API_GATEWAY_HOST = System.getenv("API_GATEWAY_HOST"); - private static final String API_GATEWAY_STAGE = System.getenv("API_GATEWAY_STAGE"); - private static final String API_TRUST_ROLE = System.getenv("API_TRUST_ROLE"); - private final EcsClient ecs; - - public EcsShutdownServices() { - final long startTimeMillis = System.currentTimeMillis(); - if (Utils.isBlank(AWS_REGION)) { - throw new IllegalStateException("Missing required environment variable AWS_REGION"); - } - if (Utils.isBlank(SAAS_BOOST_ENV)) { - throw new IllegalStateException("Missing required environment variable SAAS_BOOST_ENV"); - } - if (Utils.isBlank(API_GATEWAY_HOST)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_HOST"); - } - if (Utils.isBlank(API_GATEWAY_STAGE)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_STAGE"); - } - if (Utils.isBlank(API_TRUST_ROLE)) { - throw new IllegalStateException("Missing required environment variable API_TRUST_ROLE"); - } - LOGGER.info("Version Info: {}", Utils.version(this.getClass())); - - this.ecs = Utils.sdkClient(EcsClient.builder(), EcsClient.SERVICE_NAME); - - LOGGER.info("Constructor init: {}", System.currentTimeMillis() - startTimeMillis); - } - - @Override - public Object handleRequest(Map event, Context context) { - Utils.logRequestEvent(event); - - List> provisionedTenants = getProvisionedTenants(context); - if (provisionedTenants != null) { - LOGGER.info("{} provisioned tenants to process", provisionedTenants.size()); - - // Fetch the app config and make a list of all the configured services - Map appConfig = getAppConfig(context); - Map services = (Map) appConfig.get("services"); - List serviceNames = new ArrayList<>(services.keySet()); - - // Batch the list of services into slices of 10 to deal with the limitations of the - // describeServices SDK call - final int maxDescribeServices = 10; - final AtomicInteger batch = new AtomicInteger(0); - Collection> describeServiceBatches = serviceNames - .stream() - .collect( - Collectors.groupingBy(slice -> (batch.getAndIncrement() / maxDescribeServices)) - ) - .values(); - - for (Map tenant : provisionedTenants) { - Map> tenantResources = (Map>) tenant.get("resources"); - String cluster = tenantResources.get("ECS_CLUSTER").get("name"); - LOGGER.info("Shutting down services in cluster {}", cluster); - - // For each batch of services (will only be 1 batch unless there are more than 10 services - // in the app config), update each service's desired count to zero. Setting the service's - // desired count to 0 will gracefully remove all running tasks - final Integer count = 0; - for (List describeServiceBatch : describeServiceBatches) { - try { - DescribeServicesResponse existingServiceSettings = ecs.describeServices(request -> request - .cluster(cluster) - .services(describeServiceBatch) - ); - for (Service ecsService : existingServiceSettings.services()) { - if (ecsService.desiredCount() > count) { - LOGGER.info("Updating desired count for service {} to {}", ecsService.serviceName(), - count); - try { - ecs.updateService(request -> request - .cluster(cluster) - .service(ecsService.serviceName()) - .desiredCount(count) - ); - } catch (SdkServiceException ecsError) { - LOGGER.error("ecs::UpdateService", ecsError); - LOGGER.error(Utils.getFullStackTrace(ecsError)); - throw ecsError; - } - } else { - LOGGER.info("Skipping desired count for service {} already at {}", - ecsService.serviceName(), ecsService.desiredCount()); - } - } - } catch (SdkServiceException ecsError) { - LOGGER.error("ecs::DescribeServices", ecsError); - LOGGER.error(Utils.getFullStackTrace(ecsError)); - throw ecsError; - } - } - } - } - - return null; - } - - protected Map getAppConfig(Context context) { - // Fetch all of the services configured for this application - LOGGER.info("Calling settings service get app config API"); - String getAppConfigResponseBody = ApiGatewayHelper.signAndExecuteApiRequest( - ApiGatewayHelper.getApiRequest( - API_GATEWAY_HOST, - API_GATEWAY_STAGE, - ApiRequest.builder() - .resource("settings/config") - .method("GET") - .build() - ), - API_TRUST_ROLE, - context.getAwsRequestId() - ); - Map appConfig = Utils.fromJson(getAppConfigResponseBody, LinkedHashMap.class); - return appConfig; - } - - protected List> getProvisionedTenants(Context context) { - LOGGER.info("Calling tenants service get tenants API"); - String resource = "tenants?status=provisioned"; - String getTenantsResponseBody = ApiGatewayHelper.signAndExecuteApiRequest( - ApiGatewayHelper.getApiRequest( - API_GATEWAY_HOST, - API_GATEWAY_STAGE, - ApiRequest.builder() - .resource(resource) - .method("GET") - .build() - ), - API_TRUST_ROLE, - context.getAwsRequestId() - ); - List> tenants = Utils.fromJson(getTenantsResponseBody, ArrayList.class); - return tenants; - } -} \ No newline at end of file diff --git a/functions/ecs-shutdown-services/src/main/resources/log4j2.xml b/functions/ecs-shutdown-services/src/main/resources/log4j2.xml deleted file mode 100644 index cebb57ce..00000000 --- a/functions/ecs-shutdown-services/src/main/resources/log4j2.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} %X{AWSRequestId} %-5p %C{1} - %m%n - - - - - - - - - - - - \ No newline at end of file diff --git a/functions/ecs-shutdown-services/update.sh b/functions/ecs-shutdown-services/update.sh deleted file mode 100755 index cb3651ec..00000000 --- a/functions/ecs-shutdown-services/update.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -if [ -z $1 ]; then - echo "Usage: $0 [Lambda Folder]" - exit 2 -fi - -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -echo "AWS Region = $MY_AWS_REGION" - -ENVIRONMENT=$1 -LAMBDA_STAGE_FOLDER=$2 -if [ -z $LAMBDA_STAGE_FOLDER ]; then - LAMBDA_STAGE_FOLDER="lambdas" -fi -LAMBDA_CODE=EcsShutdownServices-lambda.zip - -#set this for V2 AWS CLI to disable paging -export AWS_PAGER="" - -SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) -echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" -if [ -z $SAAS_BOOST_BUCKET ]; then - echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" - exit 1 -fi - -# Do a fresh build of the project -mvn -if [ $? -ne 0 ]; then - echo "Error building project" - exit 1 -fi - -# And copy it up to S3 -aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ - -eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-ecs-shutdown-services\`)] | [].FunctionName' --output text"\) - -for FUNCTION in ${FUNCTIONS[@]}; do - #echo $FUNCTION - aws lambda --region $MY_AWS_REGION update-function-code --function-name $FUNCTION --s3-bucket $SAAS_BOOST_BUCKET --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE -done diff --git a/functions/ecs-startup-services/README.md b/functions/ecs-startup-services/README.md deleted file mode 100644 index fc448df2..00000000 --- a/functions/ecs-startup-services/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# ECS Startup Services - -## Overview -This function can be used to startup all application workloads running for your provisioned tenants. It does this by setting the `desiredCount` attribute of each of your ECS services for each tenant to the minium task count set for the tier that tenant is onboarded into. [Watch a detailed walkthru](https://www.twitch.tv/videos/1065389231) of building this solution during our [Office Hours](https://github.com/awslabs/aws-saas-boost/discussions/106). - -## Why would you need this? -This function gives you a way to undo the [EcsShutdownServices](../ecs-shutdown-services/README.md) function. If you're using that to save costs in your non-production environments, pair it with this function to bring your services back up when you're ready to use them again. - -## How do I use it? -Because you should not use this feature in production, SaaS Boost will not automatically turn it on during install. The function and supporting resources like log groups and IAM policies will be installed and ready-to-use. - -The common use case will be to trigger this function on a schedule. For example, you may choose to startup your application services in the morning in a development environment after having shut them down overnight. Amazon EventBridge supports [triggering Lambda functions on a schedule](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html). See the sample [enable.sh](enable.sh) script for one way to configure EventBridge with the EcsStartupServices function. - -You can also simply invoke the Lambda function manually with the AWS CLI `aws lambda invoke --function-name sb-${SAAS_BOOST_ENV}-ecs-startup-services response.json`. - -See [ECS Shutdown Services](../ecs-shutdown-services/README.md). \ No newline at end of file diff --git a/functions/ecs-startup-services/disable.sh b/functions/ecs-startup-services/disable.sh deleted file mode 100755 index f0a88152..00000000 --- a/functions/ecs-startup-services/disable.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# What SaaS Boost installation are we working on? -SAAS_BOOST_ENV=$1 -if [ -z "$SAAS_BOOST_ENV" ]; then - read -p "Enter your AWS SaaS Boost Environment label: " SAAS_BOOST_ENV - if [ -z "$SAAS_BOOST_ENV" ]; then - echo "You must enter a AWS SaaS Boost Environment label to continue. Exiting." - exit 1 - fi -fi -# Can we confirm that this AWS CLI is connected to that SaaS Boost environment? -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -SB_ENV=$(aws --region ${MY_AWS_REGION} ssm get-parameter --name /saas-boost/${SAAS_BOOST_ENV}/SAAS_BOOST_ENVIRONMENT --query "Parameter.Value" --output text) -if [ "${SB_ENV}" != "${SAAS_BOOST_ENV}" ]; then - echo "Can't find SaaS Boost environment $SAAS_BOOST_ENV in region $MY_AWS_REGION. Double-check the current AWS CLI profile and region." - exit 1 -fi - -RULE="sb-${SAAS_BOOST_ENV}-ecs-startup-services" -EXISTING_RULE=$(aws events describe-rule --name "${RULE}") -if [ $? != 0 ]; then - echo "Can't find the EventBridge rule ${RULE}" - exit 1 -else - aws events disable-rule --name "${RULE}" -fi diff --git a/functions/ecs-startup-services/enable.sh b/functions/ecs-startup-services/enable.sh deleted file mode 100755 index 0d9c490e..00000000 --- a/functions/ecs-startup-services/enable.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/bin/bash -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# What SaaS Boost installation are we working on? -SAAS_BOOST_ENV=$1 -if [ -z "$SAAS_BOOST_ENV" ]; then - read -p "Enter your AWS SaaS Boost Environment label: " SAAS_BOOST_ENV - if [ -z "$SAAS_BOOST_ENV" ]; then - echo "You must enter a AWS SaaS Boost Environment label to continue. Exiting." - exit 1 - fi -fi -# Can we confirm that this AWS CLI is connected to that SaaS Boost environment? -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -SB_ENV=$(aws --region ${MY_AWS_REGION} ssm get-parameter --name /saas-boost/${SAAS_BOOST_ENV}/SAAS_BOOST_ENVIRONMENT --query "Parameter.Value" --output text) -if [ "${SB_ENV}" != "${SAAS_BOOST_ENV}" ]; then - echo "Can't find SaaS Boost environment $SAAS_BOOST_ENV in region $MY_AWS_REGION. Double-check the current AWS CLI profile and region." - exit 1 -fi - -# Has the EcsStartupServices Lambda function been setup? -LAMBDA_FX="sb-${SAAS_BOOST_ENV}-ecs-startup-services" -LAMBDA_ARN=$(aws lambda get-function --function-name "${LAMBDA_FX}" --query "Configuration.FunctionArn" --output text) -if [ $? != 0 ]; then - echo "Can't find the EcsStartupServices Lambda function in this SaaS Boost environment." - exit 1 -fi - -RULE="sb-${SAAS_BOOST_ENV}-ecs-startup-services" -EXISTING_SCHEDULE=$(aws events describe-rule --name "${RULE}" --query "ScheduleExpression" --output text) -if [ $? == 0 ] && [ ! -z "${EXISTING_SCHEDULE}" ]; then - read -p "Reuse the existing schedule expression ${EXISTING_SCHEDULE}? [Y/N] " REUSE_SCHEDULE -fi -if ! [[ $REUSE_SCHEDULE =~ ^[Yy] ]]; then - # Get a cron schedule to invoke our Lambda on - echo "Enter an EventBridge cron schedule expression (https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html#eb-cron-expressions)." - read -p "[Press enter for default schedule of 6 AM daily, cron(0 6 * * ? *)]: " SCHEDULE - if [ -z "$SCHEDULE" ]; then - SCHEDULE="cron(0 6 * * ? *)" - fi -else - SCHEDULE="${EXISTING_SCHEDULE}" -fi - -# Simple pattern check -- doesn't test for EventBridge restriction on -# day-of-month and day-of-week not both being asterisks -CRON_REGEX="^cron\(.+ .+ .+ .+ .+ .+\)$" -if ! [[ $SCHEDULE =~ $CRON_REGEX ]]; then - echo "Schedule cron expression ${SCHEDULE} is not valid." - exit 1 -fi -#echo "Schedule = $SCHEDULE" - -# Now we can make or update an EventBridge rule for this schedule -RULE_ARN=$(aws events put-rule --name "${RULE}" --schedule-expression "${SCHEDULE}" --state ENABLED --description "Starts up all tenant tasks in ECS" --query "RuleArn" --output text) -if [ $? != 0 ]; then - echo "Error putting scheduled event rule" - exit 1 -fi -#echo $RULE_ARN -echo "Set EventBridge scheduled event rule to ${SCHEDULE}" - -# Adding a Lambda permission with the same statement id is an error. -# Unfortunately, the get-policy call returns the policy JSON as an -# escaped string rather than a proper JSON structure, so we can't use -# --query like we normally would. We could always call remove-permission -# before add-permission... but, this seems more correct. -STATEMENT_ID="sb-${SAAS_BOOST_ENV}-ecs-startup-services-permission" -GREP_PATTERN="\"Sid\":\"${STATEMENT_ID}\"" -EXISTING_PERMISSION=$(aws lambda get-policy --function-name ${LAMBDA_FX} --query "Policy" --output text | grep $GREP_PATTERN) -if [ $? == 0 ]; then - echo "Lambda permission for EventBridge rule already exists" -else - LAMBDA_PERMISSION=$(aws lambda add-permission --function-name ${LAMBDA_FX} --action 'lambda:InvokeFunction' --principal events.amazonaws.com --source-arn ${RULE_ARN} --statement-id ${STATEMENT_ID}) - if [ $? != 0 ]; then - echo "Error adding Lambda permission for EventBridge rule" - exit 1 - fi - #echo $LAMBDA_PERMISSION - echo "Added Lambda function permission for EventBridge rule" -fi - -# Finally, wire together the EventBridge scheduled rule with the Lambda function -EVENT_TARGET=$(aws events put-targets --rule "${RULE}" --targets "Id"="EcsStartupServicesLambda","Arn"="${LAMBDA_ARN}") -if [ $? != 0 ]; then - echo "Error putting EventBridge target for rule ${RULE} to function ${LAMBDA_FX}" - exit 1 -fi -#echo $EVENT_TARGET -echo "Set EventBridge target for rule ${RULE} to function ${LAMBDA_FX}" - diff --git a/functions/ecs-startup-services/pom.xml b/functions/ecs-startup-services/pom.xml deleted file mode 100644 index 5a6b8172..00000000 --- a/functions/ecs-startup-services/pom.xml +++ /dev/null @@ -1,93 +0,0 @@ - - - - 4.0.0 - - com.amazon.aws.partners.saasfactory.saasboost - saasboost-functions - 1.0.0 - - EcsStartupServices - 1.0.0 - jar - - - Apache-2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - - - 1 - - - - ${project.artifactId} - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - org.apache.maven.plugins - maven-assembly-plugin - - - io.github.git-commit-id - git-commit-id-maven-plugin - - - - - - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - - - com.amazon.aws.partners.saasfactory.saasboost - ApiGatewayHelper - 1.0.0 - - provided - - - software.amazon.awssdk - ecs - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - - diff --git a/functions/ecs-startup-services/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/EcsStartupServices.java b/functions/ecs-startup-services/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/EcsStartupServices.java deleted file mode 100644 index bf050fde..00000000 --- a/functions/ecs-startup-services/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/EcsStartupServices.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.core.exception.SdkServiceException; -import software.amazon.awssdk.services.ecs.EcsClient; -import software.amazon.awssdk.services.ecs.model.DescribeServicesResponse; -import software.amazon.awssdk.services.ecs.model.Service; -import software.amazon.awssdk.services.ecs.model.UpdateServiceRequest; - -import java.util.*; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; - -public class EcsStartupServices implements RequestHandler, Object> { - - private static final Logger LOGGER = LoggerFactory.getLogger(EcsStartupServices.class); - private static final String AWS_REGION = System.getenv("AWS_REGION"); - private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); - private static final String API_GATEWAY_HOST = System.getenv("API_GATEWAY_HOST"); - private static final String API_GATEWAY_STAGE = System.getenv("API_GATEWAY_STAGE"); - private static final String API_TRUST_ROLE = System.getenv("API_TRUST_ROLE"); - private final EcsClient ecs; - - public EcsStartupServices() { - final long startTimeMillis = System.currentTimeMillis(); - if (Utils.isBlank(AWS_REGION)) { - throw new IllegalStateException("Missing required environment variable AWS_REGION"); - } - if (Utils.isBlank(SAAS_BOOST_ENV)) { - throw new IllegalStateException("Missing required environment variable SAAS_BOOST_ENV"); - } - if (Utils.isBlank(API_GATEWAY_HOST)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_HOST"); - } - if (Utils.isBlank(API_GATEWAY_STAGE)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_STAGE"); - } - if (Utils.isBlank(API_TRUST_ROLE)) { - throw new IllegalStateException("Missing required environment variable API_TRUST_ROLE"); - } - LOGGER.info("Version Info: {}", Utils.version(this.getClass())); - - this.ecs = Utils.sdkClient(EcsClient.builder(), EcsClient.SERVICE_NAME); - - LOGGER.info("Constructor init: {}", System.currentTimeMillis() - startTimeMillis); - } - - @Override - public Object handleRequest(Map event, Context context) { - Utils.logRequestEvent(event); - - List> provisionedTenants = getProvisionedTenants(context); - if (provisionedTenants != null) { - LOGGER.info("{} provisioned tenants to process", provisionedTenants.size()); - - // Fetch the app config and make a list of all the configured services - Map appConfig = getAppConfig(context); - Map services = (Map) appConfig.get("services"); - List serviceNames = new ArrayList<>(services.keySet()); - - // Batch the list of services into slices of 10 to deal with the limitations of the - // describeServices SDK call - final int maxDescribeServices = 10; - final AtomicInteger batch = new AtomicInteger(0); - Collection> describeServiceBatches = serviceNames - .stream() - .collect( - Collectors.groupingBy(slice -> (batch.getAndIncrement() / maxDescribeServices)) - ) - .values(); - - for (Map tenant : provisionedTenants) { - Map> tenantResources = (Map>) tenant.get("resources"); - String cluster = tenantResources.get("ECS_CLUSTER").get("name"); - LOGGER.info("Starting up services in cluster {}", cluster); - String tier = (String) tenant.get("tier"); - - // For each batch of services (will only be 1 batch unless there are more than 10 services - // in the app config), update each service's desired count to the minimum for the tier that - // the tenant is in. - for (List describeServiceBatch : describeServiceBatches) { - try { - DescribeServicesResponse existingServiceSettings = ecs.describeServices(request -> request - .cluster(cluster) - .services(describeServiceBatch) - ); - for (Service ecsService : existingServiceSettings.services()) { - Map service = (Map) services.get(ecsService.serviceName()); - Map tiers = (Map) service.get("tiers"); - Map tierConfig = (Map) tiers.get(tier); - Integer count = (Integer) tierConfig.get("min"); - if (ecsService.desiredCount() < count) { - LOGGER.info("Updating desired count for service {} from {} to {}", - ecsService.serviceName(), - ecsService.desiredCount(), - count); - try { - ecs.updateService(UpdateServiceRequest.builder() - .cluster(cluster) - .service(ecsService.serviceName()) - .desiredCount(count) - .build() - ); - } catch (SdkServiceException ecsError) { - LOGGER.error("ecs::UpdateService", ecsError); - LOGGER.error(Utils.getFullStackTrace(ecsError)); - throw ecsError; - } - } - } - } catch (SdkServiceException ecsError) { - LOGGER.error("ecs::DescribeServices", ecsError); - LOGGER.error(Utils.getFullStackTrace(ecsError)); - throw ecsError; - } - } - } - } - - return null; - } - - protected Map getAppConfig(Context context) { - // Fetch all of the services configured for this application - LOGGER.info("Calling settings service get app config API"); - String getAppConfigResponseBody = ApiGatewayHelper.signAndExecuteApiRequest( - ApiGatewayHelper.getApiRequest( - API_GATEWAY_HOST, - API_GATEWAY_STAGE, - ApiRequest.builder() - .resource("settings/config") - .method("GET") - .build() - ), - API_TRUST_ROLE, - context.getAwsRequestId() - ); - Map appConfig = Utils.fromJson(getAppConfigResponseBody, LinkedHashMap.class); - return appConfig; - } - - protected List> getProvisionedTenants(Context context) { - LOGGER.info("Calling tenants service get tenants API"); - String resource = "tenants?status=provisioned"; - String getTenantsResponseBody = ApiGatewayHelper.signAndExecuteApiRequest( - ApiGatewayHelper.getApiRequest( - API_GATEWAY_HOST, - API_GATEWAY_STAGE, - ApiRequest.builder() - .resource(resource) - .method("GET") - .build() - ), - API_TRUST_ROLE, - context.getAwsRequestId() - ); - List> tenants = Utils.fromJson(getTenantsResponseBody, ArrayList.class); - return tenants; - } -} \ No newline at end of file diff --git a/functions/ecs-startup-services/src/main/resources/log4j2.xml b/functions/ecs-startup-services/src/main/resources/log4j2.xml deleted file mode 100644 index cebb57ce..00000000 --- a/functions/ecs-startup-services/src/main/resources/log4j2.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} %X{AWSRequestId} %-5p %C{1} - %m%n - - - - - - - - - - - - \ No newline at end of file diff --git a/functions/onboarding-app-stack-listener/pom.xml b/functions/onboarding-app-stack-listener/pom.xml deleted file mode 100644 index 03857dcd..00000000 --- a/functions/onboarding-app-stack-listener/pom.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - 4.0.0 - - com.amazon.aws.partners.saasfactory.saasboost - saasboost-functions - 1.0.0 - - OnboardingAppStackListener - 1.0.0 - jar - - - Apache-2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - - - 50 - - - - ${project.artifactId} - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - us-east-1 - test - - - - - org.apache.maven.plugins - maven-assembly-plugin - - - io.github.git-commit-id - git-commit-id-maven-plugin - - - - - - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - - - com.amazon.aws.partners.saasfactory.saasboost - CloudFormationUtils - 1.0.0 - - provided - - - software.amazon.awssdk - cloudformation - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - software.amazon.awssdk - eventbridge - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - - diff --git a/functions/onboarding-app-stack-listener/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingAppStackListener.java b/functions/onboarding-app-stack-listener/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingAppStackListener.java deleted file mode 100644 index 69c0ba03..00000000 --- a/functions/onboarding-app-stack-listener/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingAppStackListener.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SNSEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.core.exception.SdkServiceException; -import software.amazon.awssdk.services.cloudformation.CloudFormationClient; -import software.amazon.awssdk.services.cloudformation.model.*; -import software.amazon.awssdk.services.cloudformation.model.Stack; -import software.amazon.awssdk.services.eventbridge.EventBridgeClient; - -import java.util.*; -import java.util.regex.Pattern; - -public class OnboardingAppStackListener implements RequestHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(OnboardingAppStackListener.class); - private static final String AWS_REGION = System.getenv("AWS_REGION"); - private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); - private static final String SAAS_BOOST_EVENT_BUS = System.getenv("SAAS_BOOST_EVENT_BUS"); - private static final String EVENT_SOURCE = "saas-boost"; - private static final Pattern STACK_NAME_PATTERN = Pattern - .compile("^sb-" + SAAS_BOOST_ENV + "-tenant-[a-z0-9]{8}-app-.+-.+$"); - private static final Collection EVENTS_OF_INTEREST = Collections.unmodifiableCollection( - Arrays.asList("CREATE_COMPLETE", "CREATE_FAILED", "UPDATE_COMPLETE", "DELETE_COMPLETE", "DELETE_FAILED")); - private final CloudFormationClient cfn; - private final EventBridgeClient eventBridge; - - public OnboardingAppStackListener() { - final long startTimeMillis = System.currentTimeMillis(); - if (Utils.isBlank(AWS_REGION)) { - throw new IllegalStateException("Missing required environment variable AWS_REGION"); - } - if (Utils.isBlank(SAAS_BOOST_EVENT_BUS)) { - throw new IllegalStateException("Missing required environment variable SAAS_BOOST_EVENT_BUS"); - } - this.cfn = Utils.sdkClient(CloudFormationClient.builder(), CloudFormationClient.SERVICE_NAME); - this.eventBridge = Utils.sdkClient(EventBridgeClient.builder(), EventBridgeClient.SERVICE_NAME); - LOGGER.info("Constructor init: {}", System.currentTimeMillis() - startTimeMillis); - } - - @Override - public Object handleRequest(SNSEvent event, Context context) { - //LOGGER.info(Utils.toJson(event)); - - List records = event.getRecords(); - for (SNSEvent.SNSRecord record : records) { - SNSEvent.SNS sns = record.getSNS(); - String message = sns.getMessage(); - CloudFormationEvent cloudFormationEvent = CloudFormationEventDeserializer.deserialize(message); - - // CloudFormation sends SNS notifications for every resource in a stack going through each status change. - // We want to process the resources of the tenant-onboarding-app.yaml CloudFormation stack only after the - // stack has finished being created or updated so we don't trigger anything downstream prematurely. - if (filter(cloudFormationEvent)) { - LOGGER.info(Utils.toJson(event)); - String stackId = cloudFormationEvent.getStackId(); - String stackName = cloudFormationEvent.getStackName(); - String stackStatus = cloudFormationEvent.getResourceStatus(); - LOGGER.info("Stack " + stackName + " is in status " + stackStatus); - - // We need to get the tenant and the application service this stack was run for - String tenantId = null; - String serviceName = null; - try { - DescribeStacksResponse stacks = cfn.describeStacks(req -> req - .stackName(cloudFormationEvent.getStackId()) - ); - Stack stack = stacks.stacks().get(0); - for (Parameter parameter : stack.parameters()) { - if ("TenantId".equals(parameter.parameterKey())) { - tenantId = parameter.parameterValue(); - } - if ("ServiceName".equals(parameter.parameterKey())) { - serviceName = parameter.parameterValue(); - } - } - } catch (SdkServiceException cfnError) { - LOGGER.error("cfn:DescribeStacks error", cfnError); - LOGGER.error(Utils.getFullStackTrace(cfnError)); - throw cfnError; - } - - if ("CREATE_COMPLETE".equals(stackStatus) || "UPDATE_COMPLETE".equals(stackStatus)) { - // We use these to build the ARN of the resources we're interested in if we don't - // get the ARN straight from the CloudFormation physical resource id - final String[] lambdaArn = context.getInvokedFunctionArn().split(":"); - final String partition = lambdaArn[1]; - final String accountId = lambdaArn[4]; - - Map tenantResources = new HashMap<>(); - // We're looking for CodePipeline repository resources in a CREATE_COMPLETE state. There could be - // multiple pipelines provisioned depending on how the application services are configured. - try { - ListStackResourcesResponse resources = cfn.listStackResources(req -> req - .stackName(cloudFormationEvent.getStackId()) - ); - for (StackResourceSummary resource : resources.stackResourceSummaries()) { - LOGGER.info("Processing {} {}", resource.resourceStatusAsString(), resource.resourceType()); - if ("CREATE_COMPLETE".equals(resource.resourceStatusAsString())) { - if ("AWS::CodePipeline::Pipeline".equals(resource.resourceType())) { - String codePipeline = resource.physicalResourceId(); - // The resources collection on the tenant object is Map - // so we need a unique key per service code pipeline. We'll prefix the - // key with SERVICE_ and suffix it with _CODE_PIPELINE so we can find - // all of the tenant's code pipelines later on by looking for that pattern. - String key = serviceNameResourceKey(serviceName, AwsResource.CODE_PIPELINE.name()); - LOGGER.info("Publishing update tenant resources event for tenant {} {} {}", tenantId, - key, codePipeline); - - tenantResources.put(key, Map.of( - "name", codePipeline, - "arn", AwsResource.CODE_PIPELINE.formatArn(partition, AWS_REGION, accountId, - codePipeline), - "consoleUrl", AwsResource.CODE_PIPELINE.formatUrl(AWS_REGION, codePipeline) - )); - - // Link this pipeline to the stack that created it in Onboarding so we can keep - // track of when all pipeline executions for an onboarding request - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Onboarding Deployment Pipeline Created", - Map.of("tenantId", tenantId, - "stackId", stackId, - "stackName", stackName, - "pipeline", codePipeline) - ); - } else if ("AWS::CloudFormation::Stack".equals(resource.resourceType()) - && "rds".equals(resource.logicalResourceId())) { - // RDS nested stack - ListStackResourcesResponse rdsResources = cfn.listStackResources(req -> req - .stackName(resource.physicalResourceId()) - ); - for (StackResourceSummary rdsResource : rdsResources.stackResourceSummaries()) { - if ("CREATE_COMPLETE".equals(rdsResource.resourceStatusAsString())) { - if ("AWS::RDS::DBInstance".equals(rdsResource.resourceType())) { - String dbInstanceKey = serviceNameResourceKey(serviceName, "DB_HOST"); - String dbInstance = rdsResource.physicalResourceId(); - tenantResources.put(dbInstanceKey, Map.of( - "name", dbInstance, - "arn", AwsResource.RDS_INSTANCE.formatArn(partition, AWS_REGION, accountId, dbInstance), - "consoleUrl", AwsResource.RDS_INSTANCE.formatUrl(AWS_REGION, dbInstance) - )); - LOGGER.info("Publishing update tenant resources event for tenant {} {} {}", tenantId, - dbInstanceKey, dbInstance); - } else if ("AWS::RDS::DBCluster".equals(rdsResource.resourceType())) { - String dbClusterKey = serviceNameResourceKey(serviceName, "DB_HOST"); - String dbCluster = rdsResource.physicalResourceId(); - tenantResources.put(dbClusterKey, Map.of( - "name", dbCluster, - "arn", AwsResource.RDS_CLUSTER.formatArn(partition, AWS_REGION, accountId, dbCluster), - "consoleUrl", AwsResource.RDS_CLUSTER.formatUrl(AWS_REGION, dbCluster) - )); - LOGGER.info("Publishing update tenant resources event for tenant {} {} {}", tenantId, - dbClusterKey, dbCluster); - } - } - } - } - } - } - - if (!tenantResources.isEmpty()) { - // The update tenant resources API call is additive, so we don't need to pull the - // current tenant object ourselves. - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Tenant Resources Changed", - Map.of("tenantId", tenantId, "resources", Utils.toJson(tenantResources)) - ); - } - } catch (SdkServiceException cfnError) { - LOGGER.error("cfn:ListStackResources error", cfnError); - LOGGER.error(Utils.getFullStackTrace(cfnError)); - throw cfnError; - } - } - - // Fire a stack status change event - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Onboarding Stack Status Changed", - Map.of("tenantId", tenantId, "stackId", stackId, "stackStatus", stackStatus)); - } - } - return null; - } - - protected static String serviceNameResourceKey(String serviceName, String resourceType) { - if (Utils.isBlank(serviceName)) { - throw new IllegalArgumentException("Service name must not be blank"); - } - if (Utils.isBlank(resourceType)) { - throw new IllegalArgumentException("Resource type must not be blank"); - } - return "SERVICE_" - + Utils.toUpperSnakeCase(serviceName) - + "_" - + resourceType; - } - - protected static boolean filter(CloudFormationEvent cloudFormationEvent) { - return ("AWS::CloudFormation::Stack".equals(cloudFormationEvent.getResourceType()) - && STACK_NAME_PATTERN.matcher(cloudFormationEvent.getStackName()).matches() - && EVENTS_OF_INTEREST.contains(cloudFormationEvent.getResourceStatus())); - } - -} diff --git a/functions/onboarding-app-stack-listener/src/main/resources/lambda-assembly.xml b/functions/onboarding-app-stack-listener/src/main/resources/lambda-assembly.xml deleted file mode 100644 index 26364854..00000000 --- a/functions/onboarding-app-stack-listener/src/main/resources/lambda-assembly.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - lambda - - zip - - false - - - - ${project.build.outputDirectory} - - com/amazon/aws/partners/saasfactory/** - log4j2.xml - git.properties - - - - - - false - true - lib - - - \ No newline at end of file diff --git a/functions/onboarding-app-stack-listener/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingAppStackListenerTest.java b/functions/onboarding-app-stack-listener/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingAppStackListenerTest.java deleted file mode 100644 index 9440f2bf..00000000 --- a/functions/onboarding-app-stack-listener/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingAppStackListenerTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import org.junit.Test; - -import java.util.UUID; - -import static org.junit.Assert.*; - -public class OnboardingAppStackListenerTest { - - @Test - public void testServiceNameResourceKey() { - String serviceName = "foo bar"; - String resourceType = "CODE_PIPELINE"; - - assertThrows(IllegalArgumentException.class, () -> { - OnboardingAppStackListener.serviceNameResourceKey(null, resourceType); - }); - assertThrows(IllegalArgumentException.class, () -> { - OnboardingAppStackListener.serviceNameResourceKey("", resourceType); - }); - assertThrows(IllegalArgumentException.class, () -> { - OnboardingAppStackListener.serviceNameResourceKey(" ", resourceType); - }); - assertThrows(IllegalArgumentException.class, () -> { - OnboardingAppStackListener.serviceNameResourceKey(serviceName, null); - }); - assertThrows(IllegalArgumentException.class, () -> { - OnboardingAppStackListener.serviceNameResourceKey(serviceName, ""); - }); - assertThrows(IllegalArgumentException.class, () -> { - OnboardingAppStackListener.serviceNameResourceKey(serviceName, " "); - }); - - String expected = "SERVICE_FOO_BAR_CODE_PIPELINE"; - assertEquals(expected, OnboardingAppStackListener.serviceNameResourceKey(serviceName, resourceType)); - } - - @Test - public void testFilter() { - UUID id = UUID.randomUUID(); - String tenantId = id.toString().split("-")[0]; - CloudFormationEvent event = CloudFormationEvent.builder() - .stackName("sb-" + System.getenv("SAAS_BOOST_ENV") + "-tenant-" + tenantId + "-app-foobar-" - + Utils.randomString(12)) - .resourceType("AWS::CloudFormation::Stack") - .resourceStatus("CREATE_COMPLETE") - .build(); - assertTrue(OnboardingAppStackListener.filter(event)); - } -} diff --git a/functions/onboarding-app-stack-listener/update.sh b/functions/onboarding-app-stack-listener/update.sh deleted file mode 100755 index bbb8f76c..00000000 --- a/functions/onboarding-app-stack-listener/update.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -echo "AWS Region = $MY_AWS_REGION" - -if [ "X$1" = "X" ]; then - echo "usage: $0 " - exit 2 -fi -ENVIRONMENT=$1 -LAMBDA_STAGE_FOLDER=$2 -if [ "X$LAMBDA_STAGE_FOLDER" = "X" ]; then - LAMBDA_STAGE_FOLDER="lambdas" -fi - -LAMBDA_CODE=OnboardingAppStackListener-lambda.zip - -#set this for V2 AWS CLI to disable paging -export AWS_PAGER="" - -SAAS_BOOST_BUCKET=`aws ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query "Parameter.Value" --output text` -echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" -if [ "X$SAAS_BOOST_BUCKET" = "X" ]; then - echo "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET SSM parameter not read from AWS env" - exit 1 -fi - - - -mvn -if [ $? -ne 0 ]; then - echo "Error building project" - exit 1 -fi - -aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ - -eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-onboarding-app-listener\`)] | [].FunctionName' --output text"\) - -for FUNCTION in ${FUNCTIONS[@]}; do - #echo $FUNCTION - aws lambda --region $MY_AWS_REGION update-function-code --function-name $FUNCTION --s3-bucket $SAAS_BOOST_BUCKET --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE -done diff --git a/functions/onboarding-stack-listener/event.json b/functions/onboarding-stack-listener/event.json deleted file mode 100644 index 2a2a25e5..00000000 --- a/functions/onboarding-stack-listener/event.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "records": [ - { - "sns": { - "message": "StackId='arn:aws:cloudformation:us-west-2:914245659875:stack/sb-multi-tenant-e2f85934/3b77ee40-5a03-11ec-8817-02418856b67f'\nLogicalResourceId='sb-multi-tenant-e2f85934'\nPhysicalResourceId='arn:aws:cloudformation:us-west-2:914245659875:stack/sb-multi-tenant-e2f85934/3b77ee40-5a03-11ec-8817-02418856b67f'\nResourceStatus='CREATE_COMPLETE'\nResourceType='AWS::CloudFormation::Stack'\nStackName='sb-multi-tenant-e2f85934'" - } - } - ] -} \ No newline at end of file diff --git a/functions/onboarding-stack-listener/pom.xml b/functions/onboarding-stack-listener/pom.xml deleted file mode 100644 index b023aace..00000000 --- a/functions/onboarding-stack-listener/pom.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - - 4.0.0 - - com.amazon.aws.partners.saasfactory.saasboost - saasboost-functions - 1.0.0 - - OnboardingStackListener - 1.0.0 - jar - - - Apache-2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - - - 28 - - - - ${project.artifactId} - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - org.apache.maven.plugins - maven-assembly-plugin - - - io.github.git-commit-id - git-commit-id-maven-plugin - - - - - - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - - - com.amazon.aws.partners.saasfactory.saasboost - CloudFormationUtils - 1.0.0 - - provided - - - software.amazon.awssdk - cloudformation - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - software.amazon.awssdk - eventbridge - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - - diff --git a/functions/onboarding-stack-listener/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingStackListener.java b/functions/onboarding-stack-listener/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingStackListener.java deleted file mode 100644 index e7d12a37..00000000 --- a/functions/onboarding-stack-listener/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingStackListener.java +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SNSEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.core.exception.SdkServiceException; -import software.amazon.awssdk.services.cloudformation.CloudFormationClient; -import software.amazon.awssdk.services.cloudformation.model.*; -import software.amazon.awssdk.services.cloudformation.model.Stack; -import software.amazon.awssdk.services.eventbridge.EventBridgeClient; -import software.amazon.awssdk.services.eventbridge.model.PutEventsRequestEntry; -import software.amazon.awssdk.services.eventbridge.model.PutEventsResponse; -import software.amazon.awssdk.services.eventbridge.model.PutEventsResultEntry; - -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class OnboardingStackListener implements RequestHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(OnboardingStackListener.class); - private static final String AWS_REGION = System.getenv("AWS_REGION"); - private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); - private static final String SAAS_BOOST_EVENT_BUS = System.getenv("SAAS_BOOST_EVENT_BUS"); - private static final String SYSTEM_API_CALL = "System API Call"; - private static final String UPDATE_TENANT_RESOURCES = "Tenant Update Resources"; - private static final String BILLING_SETUP = "Billing Tenant Setup"; - private static final String BILLING_DISABLE = "Billing Tenant Disable"; - private static final String EVENT_SOURCE = "saas-boost"; - private static final Pattern STACK_NAME_PATTERN = Pattern - .compile("^sb-" + SAAS_BOOST_ENV + "-tenant-[a-z0-9]{8}$"); - private static final Collection EVENTS_OF_INTEREST = Collections.unmodifiableCollection( - Arrays.asList("CREATE_COMPLETE", "CREATE_FAILED", "UPDATE_COMPLETE", "DELETE_COMPLETE", "DELETE_FAILED")); - private final CloudFormationClient cfn; - private final EventBridgeClient eventBridge; - - public OnboardingStackListener() { - final long startTimeMillis = System.currentTimeMillis(); - if (Utils.isBlank(AWS_REGION)) { - throw new IllegalStateException("Missing required environment variable AWS_REGION"); - } - if (Utils.isBlank(SAAS_BOOST_EVENT_BUS)) { - throw new IllegalStateException("Missing required environment variable SAAS_BOOST_EVENT_BUS"); - } - this.cfn = Utils.sdkClient(CloudFormationClient.builder(), CloudFormationClient.SERVICE_NAME); - this.eventBridge = Utils.sdkClient(EventBridgeClient.builder(), EventBridgeClient.SERVICE_NAME); - LOGGER.info("Constructor init: {}", System.currentTimeMillis() - startTimeMillis); - } - - @Override - public Object handleRequest(SNSEvent event, Context context) { - //LOGGER.info(Utils.toJson(event)); - - List records = event.getRecords(); - SNSEvent.SNS sns = records.get(0).getSNS(); - String message = sns.getMessage(); - - CloudFormationEvent cloudFormationEvent = CloudFormationEventDeserializer.deserialize(message); - - // CloudFormation sends SNS notifications for every resource in a stack going through each status change. - // We want to process the resources of the tenant-onboarding.yaml CloudFormation stack only after the - // stack has finished being created or updated so we don't trigger anything downstream prematurely. - if (filter(cloudFormationEvent)) { - LOGGER.info(Utils.toJson(event)); - String stackName = cloudFormationEvent.getStackName(); - String stackStatus = cloudFormationEvent.getResourceStatus(); - String stackId = cloudFormationEvent.getStackId(); - LOGGER.info("Stack " + stackName + " is in status " + stackStatus); - - String tenantId = null; - String domainName = null; - String hostedZone = null; - String subdomain = null; - try { - DescribeStacksResponse stacks = cfn.describeStacks(req -> req - .stackName(stackId) - ); - Stack stack = stacks.stacks().get(0); - for (Parameter parameter : stack.parameters()) { - if ("TenantId".equals(parameter.parameterKey())) { - tenantId = parameter.parameterValue(); - } - if ("DomainName".equals(parameter.parameterKey())) { - domainName = parameter.parameterValue(); - } - if ("HostedZoneId".equals(parameter.parameterKey())) { - hostedZone = parameter.parameterValue(); - } - if ("TenantSubDomain".equals(parameter.parameterKey())) { - subdomain = parameter.parameterValue(); - } - } - - // The public URL to access this tenant's environment is either a custom DNS entry we made a - // Route53 record set for and pointed at the load balancer, or we can fall back to the ALB's DNS. - String hostname = null; - if (Utils.isNotBlank(domainName) && Utils.isNotBlank(hostedZone) && Utils.isNotBlank(subdomain)) { - hostname = subdomain + "." + domainName; - } else { - if (stack.hasOutputs()) { - for (Output output : stack.outputs()) { - if ("DNSName".equals(output.outputKey())) { - hostname = output.outputValue(); - } - if ("ADPasswordSecret".equals(output.outputKey())) { - - } - } - } - } - // Fire a tenant hostname changed event - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, "Tenant Hostname Changed", - Map.of("tenantId", tenantId, "hostname", hostname)); - } catch (SdkServiceException cfnError) { - LOGGER.error("cfn:DescribeStacks error", cfnError); - LOGGER.error(Utils.getFullStackTrace(cfnError)); - throw cfnError; - } - - if ("CREATE_COMPLETE".equals(stackStatus) || "UPDATE_COMPLETE".equals(stackStatus)) { - // We'll use these to build the ARN string for resources that CloudFormation doesn't return the ARN - // as either the physical or logical resource id - final String[] lambdaArn = context.getInvokedFunctionArn().split(":"); - final String partition = lambdaArn[1]; - final String accountId = lambdaArn[4]; - final String stackIdName = stackId; - - // Now, collect up all of the provisioned resources for this tenant that we want to save with the - // tenant record. Start with the "parent" CloudFormation stack. - Map> tenantResources = new HashMap<>(); - tenantResources.put(AwsResource.CLOUDFORMATION.name(), Map.of( - "name", stackName, - "arn", stackId, - "consoleUrl", AwsResource.CLOUDFORMATION.formatUrl(AWS_REGION, stackId)) - ); - - // Loop through all of the resources and grab the ones we need to save to the tenant record. - ListStackResourcesResponse resources = cfn.listStackResources(req -> req.stackName(stackIdName)); - for (StackResourceSummary resource : resources.stackResourceSummaries()) { - String resourceType = resource.resourceType(); - String physicalResourceId = resource.physicalResourceId(); - String resourceStatus = resource.resourceStatusAsString(); - String logicalId = resource.logicalResourceId(); - LOGGER.info("Processing resource {} {} {} {}", resourceType, resourceStatus, logicalId, physicalResourceId); - if ("CREATE_COMPLETE".equals(resourceStatus)) { - if ("AWS::EC2::SecurityGroup".equals(resourceType) && "ECSSecurityGroup".equals(logicalId)) { - LOGGER.info("Saving ECS Security Group {} {}", logicalId, physicalResourceId); - tenantResources.put(AwsResource.ECS_SECURITY_GROUP.name(), Map.of( - "name", physicalResourceId, - "arn", AwsResource.ECS_SECURITY_GROUP.formatArn(partition, AWS_REGION, accountId, physicalResourceId), - "consoleUrl", AwsResource.ECS_SECURITY_GROUP.formatUrl(AWS_REGION, physicalResourceId)) - ); - } else if ("AWS::EC2::Subnet".equals(resourceType)) { - // Process all the subnet resources together because we only want the 2 private subnets and - // there are other subnets in the stack which would end up overwriting the values in the - // resources map with whatever subnet happens to be last in the stack summary. - if ("SubnetPrivateA".equals(logicalId)) { - LOGGER.info("Saving Private Subnet {} {}", logicalId, physicalResourceId); - tenantResources.put(AwsResource.PRIVATE_SUBNET_A.name(), Map.of( - "name", physicalResourceId, - "arn", AwsResource.PRIVATE_SUBNET_A.formatArn(partition, AWS_REGION, accountId, physicalResourceId), - "consoleUrl", AwsResource.PRIVATE_SUBNET_A.formatUrl(AWS_REGION, physicalResourceId)) - ); - } else if ("SubnetPrivateB".equals(logicalId)) { - LOGGER.info("Saving Private Subnet {} {}", logicalId, physicalResourceId); - tenantResources.put(AwsResource.PRIVATE_SUBNET_B.name(), Map.of( - "name", physicalResourceId, - "arn", AwsResource.PRIVATE_SUBNET_B.formatArn(partition, AWS_REGION, accountId, physicalResourceId), - "consoleUrl", AwsResource.PRIVATE_SUBNET_B.formatUrl(AWS_REGION, physicalResourceId)) - ); - } - } else if ("AWS::EC2::RouteTable".equals(resourceType)) { - // Process all of the route table resources together because we only want the route table - // for the private subnets and there are other route tables in the stack which may end up - // overwriting the values in the resources map depending on which order they are listed in - // from the stack summary - if ("RouteTablePrivate".equals(logicalId)) { - LOGGER.info("Saving Private Route Table {} {}", logicalId, physicalResourceId); - tenantResources.put(AwsResource.PRIVATE_ROUTE_TABLE.name(), Map.of( - "name", physicalResourceId, - "arn", AwsResource.PRIVATE_ROUTE_TABLE.formatArn(partition, AWS_REGION, accountId, physicalResourceId), - "consoleUrl", AwsResource.PRIVATE_ROUTE_TABLE.formatUrl(AWS_REGION, physicalResourceId)) - ); - } - } else if ("AWS::ServiceDiscovery::PrivateDnsNamespace".equals(resourceType)) { - if ("ServiceDiscoveryNamespace".equals(logicalId)) { - LOGGER.info("Saving Private DNS Namespace {} {}", logicalId, physicalResourceId); - AwsResource namespace = AwsResource.PRIVATE_SERVICE_DISCOVERY_NAMESPACE; - tenantResources.put(namespace.name(), Map.of( - "name", physicalResourceId, - "arn", namespace.formatArn(partition, AWS_REGION, accountId, physicalResourceId), - "consoleUrl", namespace.formatUrl(AWS_REGION, physicalResourceId) - )); - } - } else if ("AWS::CloudFormation::Stack".equals(resource.resourceType()) - && "ad".equals(resource.logicalResourceId())) { - // Active Directory nested stack - DescribeStacksResponse adStackResponse = cfn.describeStacks(req -> req - .stackName(resource.physicalResourceId()) - ); - Stack adStack = adStackResponse.stacks().get(0); - for (Output adStackOutput : adStack.outputs()) { - if ("ActiveDirectoryCredentials".equals(adStackOutput.outputKey())) { - // CloudFormation returns the ARN for a Secrets Manager secret which includes the - // random -XXXXXX characters on the end. We need just the name of the secret to - // build the console url for the secret. - String secretArn = adStackOutput.outputValue(); - String secretName = secretArn.substring(secretArn.indexOf(":secret:") + 8, secretArn.length() - 7); - tenantResources.put("ACTIVE_DIRECTORY_CREDENTIALS", Map.of( - "name", secretName, - "arn", secretArn, - "consoleUrl", AwsResource.SECRET.formatUrl(AWS_REGION, secretName) - )); - LOGGER.info("Publishing update tenant resources event for tenant {} " - + "ACTIVE_DIRECTORY_CREDENTIALS {}", tenantId, secretName); - } else if ("ActiveDirectoryId".equals(adStackOutput.outputKey())) { - String directoryId = adStackOutput.outputValue(); - tenantResources.put("ACTIVE_DIRECTORY_ID", Map.of( - "name", directoryId, - "arn", AwsResource.MANAGED_AD.formatArn(partition, AWS_REGION, accountId, directoryId), - "consoleUrl", AwsResource.MANAGED_AD.formatUrl(AWS_REGION, directoryId) - )); - LOGGER.info("Publishing update tenant resources event for tenant {} " - + "ACTIVE_DIRECTORY_ID {}", tenantId, directoryId); - } - } - } else { - // Match on the resource type and build the console url - for (AwsResource awsResource : AwsResource.values()) { - if (awsResource.getResourceType().equalsIgnoreCase(resourceType)) { - if ("AWS::ElasticLoadBalancingV2::LoadBalancer".equals(resourceType)) { - // CloudFormation returns the ARN for the physical id of the load balancer - // The console url can use the name of the load balancer as a search string - // and the name is the short tenant id - tenantResources.put(awsResource.name(), Map.of( - "name", physicalResourceId.substring(physicalResourceId.indexOf(":loadbalancer/") + 14), - "arn", physicalResourceId, - "consoleUrl", AwsResource.LOAD_BALANCER.formatUrl(AWS_REGION, "sb-" + SAAS_BOOST_ENV + "-tenant-" + tenantId.split("-")[0])) - ); - } else if ("AWS::ElasticLoadBalancingV2::Listener".equals(resourceType)) { - if ("HttpListener".equals(logicalId)) { - LOGGER.info("Saving HTTP listener {} {}", logicalId, physicalResourceId); - tenantResources.put(AwsResource.HTTP_LISTENER.name(), Map.of( - "name", physicalResourceId, - "arn", physicalResourceId, - // Same URL as the load balancer - "consoleUrl", AwsResource.LOAD_BALANCER.formatUrl(AWS_REGION, "sb-" + SAAS_BOOST_ENV + "-tenant-" + tenantId.split("-")[0])) - ); - } else if ("HttpsListener".equals(logicalId)) { - LOGGER.info("Saving HTTPS listener {} {}", logicalId, physicalResourceId); - tenantResources.put(AwsResource.HTTPS_LISTENER.name(), Map.of( - "name", physicalResourceId, - "arn", physicalResourceId, - // Same URL as the load balancer - "consoleUrl", AwsResource.LOAD_BALANCER.formatUrl(AWS_REGION, "sb-" + SAAS_BOOST_ENV + "-tenant-" + tenantId.split("-")[0])) - ); - } - } else if ("AWS::Logs::LogGroup".equals(resourceType)) { - //need to replace / with $252F for the url path - //physicalResourceId = physicalResourceId.replaceAll("/", Matcher.quoteReplacement("$252F")); - } else { - // Don't overwrite something we've already set - if (!tenantResources.containsKey(awsResource.name())) { - LOGGER.info("Saving {} {} {}", awsResource.name(), logicalId, physicalResourceId); - tenantResources.put(awsResource.name(), Map.of( - "name", physicalResourceId, - "arn", awsResource.formatArn(partition, AWS_REGION, accountId, physicalResourceId), - "consoleUrl", awsResource.formatUrl(AWS_REGION, physicalResourceId)) - ); - } - } - } - } - } - } - } - - // Fire a tenant resources updated event - LOGGER.info("Updating tenant resources AWS console links"); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Tenant Resources Changed", - Map.of("tenantId", tenantId, "resources", Utils.toJson(tenantResources)) - ); - -// // If there's a billing plan for this tenant, publish the event so they get -// // wired up to the 3rd party system -// if (Utils.isNotBlank(billingPlan)) { -// LOGGER.info("Triggering tenant billing setup"); -// Map updateBillingPlanEventDetail = new HashMap<>(); -// updateBillingPlanEventDetail.put("tenantId", tenantId); -// updateBillingPlanEventDetail.put("planId", billingPlan); -// publishEvent(updateBillingPlanEventDetail, BILLING_SETUP); -// } - - } - - // Fire a stack status change event - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Onboarding Stack Status Changed", - Map.of("tenantId", tenantId, "stackId", stackId, "stackStatus", stackStatus)); - - //TODO deal with a deleted stack canceling billing subscription - //TODO deal with a created stack creating a billing subscription - } else { - //LOGGER.info("Skipping CloudFormation notification {} {} {} {}", stackId, type, stackName, stackStatus); - } - return null; - } - - protected static boolean filter(CloudFormationEvent cloudFormationEvent) { - return ("AWS::CloudFormation::Stack".equals(cloudFormationEvent.getResourceType()) - && STACK_NAME_PATTERN.matcher(cloudFormationEvent.getStackName()).matches() - && STACK_NAME_PATTERN.matcher(cloudFormationEvent.getLogicalResourceId()).matches() - && EVENTS_OF_INTEREST.contains(cloudFormationEvent.getResourceStatus())); - } - -} diff --git a/functions/onboarding-stack-listener/src/main/resources/lambda-assembly.xml b/functions/onboarding-stack-listener/src/main/resources/lambda-assembly.xml deleted file mode 100644 index 26364854..00000000 --- a/functions/onboarding-stack-listener/src/main/resources/lambda-assembly.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - lambda - - zip - - false - - - - ${project.build.outputDirectory} - - com/amazon/aws/partners/saasfactory/** - log4j2.xml - git.properties - - - - - - false - true - lib - - - \ No newline at end of file diff --git a/functions/onboarding-stack-listener/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingStackListenerTest.java b/functions/onboarding-stack-listener/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingStackListenerTest.java deleted file mode 100644 index 144daac0..00000000 --- a/functions/onboarding-stack-listener/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingStackListenerTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.events.SNSEvent; -import org.junit.Test; - -import java.util.Collections; - -public class OnboardingStackListenerTest { - - @Test - public void testHandleRequest() { - SNSEvent event = new SNSEvent(); - SNSEvent.SNSRecord record = new SNSEvent.SNSRecord(); - SNSEvent.SNS sns = new SNSEvent.SNS(); - CloudFormationEvent cloudFormationEvent = CloudFormationEvent.builder() - .stackId("arn:aws:cloudformation:us-west-2:111111111111:stack/sb-multi-tenant-6cad89f5/3e92db10-8ba1-11ec-97ef-06246f7d706f") - .logicalResourceId("sb-multi-tenant-6cad89f5") - .physicalResourceId("arn:aws:cloudformation:us-west-2:111111111111:stack/sb-multi-tenant-6cad89f5/3e92db10-8ba1-11ec-97ef-06246f7d706f") - .resourceStatus("CREATE_COMPLETE") - .stackName("sb-multi-tenant-6cad89f5") - .resourceType("AWS::CloudFormation::Stack") - .build(); - StringBuilder message = new StringBuilder(); - message.append("StackId='"); - message.append(cloudFormationEvent.getStackId()); - message.append("'\n"); - message.append("LogicalResourceId='"); - message.append(cloudFormationEvent.getLogicalResourceId()); - message.append("'\n"); - message.append("PhysicalResourceId='"); - message.append(cloudFormationEvent.getPhysicalResourceId()); - message.append("'\n"); - message.append("ResourceStatus='"); - message.append(cloudFormationEvent.getResourceStatus()); - message.append("'\n"); - message.append("ResourceType='"); - message.append(cloudFormationEvent.getResourceType()); - message.append("'\n"); - message.append("StackName='"); - message.append(cloudFormationEvent.getStackName()); - message.append("'"); - - //System.out.println(Utils.toJson(cloudFormationEvent)); - //System.out.println(message.toString()); - sns.setMessage(message.toString()); - record.setSns(sns); - event.setRecords(Collections.singletonList(record)); - //System.out.println(Utils.toJson(event)); - } -} diff --git a/functions/onboarding-stack-listener/update.sh b/functions/onboarding-stack-listener/update.sh deleted file mode 100755 index 982852e1..00000000 --- a/functions/onboarding-stack-listener/update.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -echo "AWS Region = $MY_AWS_REGION" - -if [ "X$1" = "X" ]; then - echo "usage: $0 " - exit 2 -fi -ENVIRONMENT=$1 -LAMBDA_STAGE_FOLDER=$2 -if [ "X$LAMBDA_STAGE_FOLDER" = "X" ]; then - LAMBDA_STAGE_FOLDER="lambdas" -fi - -LAMBDA_CODE=OnboardingStackListener-lambda.zip - -#set this for V2 AWS CLI to disable paging -export AWS_PAGER="" - -SAAS_BOOST_BUCKET=`aws ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query "Parameter.Value" --output text` -echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" -if [ "X$SAAS_BOOST_BUCKET" = "X" ]; then - echo "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET SSM parameter not read from AWS env" - exit 1 -fi - - - -mvn -if [ $? -ne 0 ]; then - echo "Error building project" - exit 1 -fi - -aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ - -eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-onboarding-listener\`)] | [].FunctionName' --output text"\) - -for FUNCTION in ${FUNCTIONS[@]}; do - #echo $FUNCTION - aws lambda --region $MY_AWS_REGION update-function-code --function-name $FUNCTION --s3-bucket $SAAS_BOOST_BUCKET --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE -done diff --git a/functions/pom.xml b/functions/pom.xml index 2ac95475..e0000695 100644 --- a/functions/pom.xml +++ b/functions/pom.xml @@ -15,15 +15,11 @@ authorizer codepipeline-wait-handler - core-stack-listener ecs-service-update - ecs-shutdown-services - ecs-startup-services - onboarding-app-stack-listener - onboarding-stack-listener - system-rest-api-client - workload-deploy + + ${project.basedir}/.. + Apache-2.0 @@ -48,12 +44,31 @@ aws-lambda-java-log4j2 - junit - junit + org.junit.jupiter + junit-jupiter-engine + + + org.junit.jupiter + junit-jupiter-api + + + com.amazonaws + aws-lambda-java-tests + + + org.mockito + mockito-core org.slf4j slf4j-nop + + com.amazon.aws.partners.saasfactory.saasboost + Utils + 1.0.0 + + provided + diff --git a/functions/system-rest-api-client/pom.xml b/functions/system-rest-api-client/pom.xml deleted file mode 100644 index cdfcf31b..00000000 --- a/functions/system-rest-api-client/pom.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - 4.0.0 - - com.amazon.aws.partners.saasfactory.saasboost - saasboost-functions - 1.0.0 - - SystemRestApiClient - 1.0.0 - jar - - - Apache-2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - - - 0 - - - - ${project.artifactId} - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - org.apache.maven.plugins - maven-assembly-plugin - - - io.github.git-commit-id - git-commit-id-maven-plugin - - - - - - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - - - com.amazon.aws.partners.saasfactory.saasboost - ApiGatewayHelper - 1.0.0 - - provided - - - diff --git a/functions/system-rest-api-client/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiRequestEvent.java b/functions/system-rest-api-client/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiRequestEvent.java deleted file mode 100644 index 658de154..00000000 --- a/functions/system-rest-api-client/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiRequestEvent.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.time.Instant; -import java.util.List; - -public class ApiRequestEvent { - - private String version; - private String id; - @JsonProperty("detail-type") - private String detailType; - private String source; - private String account; - private Instant time; - private String region; - private List resources; - private ApiRequest detail; - - public ApiRequestEvent() { - } - - public String getVersion() { - return version; - } - - public void setVersion(String version) { - this.version = version; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getDetailType() { - return detailType; - } - - public void setDetailType(String detailType) { - this.detailType = detailType; - } - - public String getSource() { - return source; - } - - public void setSource(String source) { - this.source = source; - } - - public String getAccount() { - return account; - } - - public void setAccount(String account) { - this.account = account; - } - - public Instant getTime() { - return time; - } - - public void setTime(Instant time) { - this.time = time; - } - - public String getRegion() { - return region; - } - - public void setRegion(String region) { - this.region = region; - } - - public List getResources() { - return resources; - } - - public void setResources(List resources) { - this.resources = resources; - } - - public ApiRequest getDetail() { - return detail; - } - - public void setDetail(ApiRequest detail) { - this.detail = detail; - } -} diff --git a/functions/system-rest-api-client/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SystemRestApiClient.java b/functions/system-rest-api-client/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SystemRestApiClient.java deleted file mode 100644 index cd462872..00000000 --- a/functions/system-rest-api-client/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SystemRestApiClient.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestStreamHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.http.SdkHttpFullRequest; - -import java.io.InputStream; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.nio.charset.StandardCharsets; - -public class SystemRestApiClient implements RequestStreamHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(SystemRestApiClient.class); - private static final String API_GATEWAY_HOST = System.getenv("API_GATEWAY_HOST"); - private static final String API_GATEWAY_STAGE = System.getenv("API_GATEWAY_STAGE"); - private static final String API_TRUST_ROLE = System.getenv("API_TRUST_ROLE"); - - public SystemRestApiClient() { - final long startTimeMillis = System.currentTimeMillis(); - if (Utils.isBlank(API_GATEWAY_HOST)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_HOST"); - } - if (Utils.isBlank(API_GATEWAY_STAGE)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_STAGE"); - } - if (Utils.isBlank(API_TRUST_ROLE)) { - throw new IllegalStateException("Missing required environment variable API_TRUST_ROLE"); - } - LOGGER.info("Version Info: {}", Utils.version(this.getClass())); - LOGGER.info("Constructor init: {}", System.currentTimeMillis() - startTimeMillis); - } - - @Override - public void handleRequest(InputStream input, OutputStream output, Context context) { - // Using a RequestSteamHandler here because there doesn't seem to be a way to get - // a hold of the internal Jackson ObjectMapper from AWS to adjust it to deal with - // the new java.time classes used in the EventBridge event object - ApiRequestEvent event = Utils.fromJson(input, ApiRequestEvent.class); - if (null == event) { - throw new RuntimeException("responseBody is invalid"); - } - LOGGER.info(Utils.toJson(event)); - - try { - String responseBody = ApiGatewayHelper.signAndExecuteApiRequest( - ApiGatewayHelper.getApiRequest(API_GATEWAY_HOST, API_GATEWAY_STAGE, event.getDetail()), - API_TRUST_ROLE, - context.getAwsRequestId() - ); - try (Writer writer = new OutputStreamWriter(output, StandardCharsets.UTF_8)) { - writer.write(responseBody); - writer.flush(); - } - } catch (Exception e) { - LOGGER.error(Utils.getFullStackTrace(e)); - throw new RuntimeException(e.getMessage()); - } - } -} diff --git a/functions/system-rest-api-client/src/main/resources/log4j2.xml b/functions/system-rest-api-client/src/main/resources/log4j2.xml deleted file mode 100644 index 04128110..00000000 --- a/functions/system-rest-api-client/src/main/resources/log4j2.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} %X{AWSRequestId} %-5p %C{1} - %m%n - - - - - - - - - - - - diff --git a/functions/system-rest-api-client/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SystemRestApiClientTest.java b/functions/system-rest-api-client/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SystemRestApiClientTest.java deleted file mode 100644 index 48278e75..00000000 --- a/functions/system-rest-api-client/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SystemRestApiClientTest.java +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Ignore; -import org.junit.Test; - -import java.net.MalformedURLException; -import java.net.URL; - -import static org.junit.Assert.*; - -public class SystemRestApiClientTest { - - static String host; - static String protocol; - static String stage; - - @BeforeClass - public static void setup() { - host = "123456789.execute-api.us-west-2.amazonaws.com"; - protocol = "https"; - stage = "v1"; - } - - @Test - public void testUrlParse() throws MalformedURLException { - String resource = stage + "/settings?readOnly=false&foo=bar"; - - URL url = new URL(protocol, host, resource); - assertEquals(stage + "/settings", url.getPath()); - assertEquals("readOnly=false&foo=bar", url.getQuery()); - } - - @Test - public void testEventBridgeJson() throws JsonProcessingException { - String json = "{\n" + - " \"version\": \"0\",\n" + - " \"id\": \"343a3b5e-7e5d-960f-b1cc-f69ee52edaaa\",\n" + - " \"detail-type\": \"System API Call\",\n" + - " \"source\": \"saasboost\",\n" + - " \"account\": \"914245659875\",\n" + - " \"time\": \"2020-07-02T22:30:56Z\",\n" + - " \"region\": \"us-west-2\",\n" + - " \"resources\": [],\n" + - " \"detail\": {\n" + - " \"resource\": \"settings\",\n" + - " \"method\": \"GET\",\n" + - " \"body\": null\n" + - " }\n" + - "}"; - ObjectMapper mapper = new ObjectMapper(); - mapper.findAndRegisterModules(); - ApiRequestEvent event = mapper.readValue(json, ApiRequestEvent.class); - - System.out.println(mapper.writeValueAsString(event)); - } -} diff --git a/functions/system-rest-api-client/update.sh b/functions/system-rest-api-client/update.sh deleted file mode 100755 index 8b0c0bdb..00000000 --- a/functions/system-rest-api-client/update.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -echo "AWS Region = $MY_AWS_REGION" - -if [ "X$1" = "X" ]; then - echo "usage: $0 " - exit 2 -fi -ENVIRONMENT=$1 - -LAMBDA_CODE=SystemRestApiClient-lambda.zip -LAMBDA_STAGE_FOLDER=lambdas - -#set this for V2 AWS CLI to disable paging -export AWS_PAGER="" - -SAAS_BOOST_BUCKET=`aws ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query "Parameter.Value" --output text` -echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" -if [ "X$SAAS_BOOST_BUCKET" = "X" ]; then - echo "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET SSM parameter not read from AWS env" - exit 1 -fi - - - -mvn -if [ $? -ne 0 ]; then - echo "Error building project" - exit 1 -fi - -aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ - -eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-private-api-client\`)] | [].FunctionName' --output text"\) - -for FUNCTION in ${FUNCTIONS[@]}; do - #echo $FUNCTION - aws lambda --region $MY_AWS_REGION update-function-code --function-name $FUNCTION --s3-bucket $SAAS_BOOST_BUCKET --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE -done diff --git a/functions/workload-deploy/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/WorkloadDeploy.java b/functions/workload-deploy/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/WorkloadDeploy.java deleted file mode 100644 index bb547866..00000000 --- a/functions/workload-deploy/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/WorkloadDeploy.java +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.core.exception.SdkServiceException; -import software.amazon.awssdk.core.sync.RequestBody; -import software.amazon.awssdk.services.codepipeline.CodePipelineClient; -import software.amazon.awssdk.services.codepipeline.model.StartPipelineExecutionResponse; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.PutObjectRequest; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -public class WorkloadDeploy implements RequestHandler, Object> { - - private static final Logger LOGGER = LoggerFactory.getLogger(WorkloadDeploy.class); - private static final String AWS_REGION = System.getenv("AWS_REGION"); - private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); - private static final String API_GATEWAY_HOST = System.getenv("API_GATEWAY_HOST"); - private static final String API_GATEWAY_STAGE = System.getenv("API_GATEWAY_STAGE"); - private static final String API_TRUST_ROLE = System.getenv("API_TRUST_ROLE"); - private static final String CODE_PIPELINE_BUCKET = System.getenv("CODE_PIPELINE_BUCKET"); - private final S3Client s3; - private final CodePipelineClient codepipeline; - - public WorkloadDeploy() { - if (Utils.isBlank(AWS_REGION)) { - throw new IllegalStateException("Missing required environment variable AWS_REGION"); - } - if (Utils.isBlank(SAAS_BOOST_ENV)) { - throw new IllegalStateException("Missing required environment variable SAAS_BOOST_ENV"); - } - if (Utils.isBlank(API_GATEWAY_HOST)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_HOST"); - } - if (Utils.isBlank(API_GATEWAY_STAGE)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_STAGE"); - } - if (Utils.isBlank(API_TRUST_ROLE)) { - throw new IllegalStateException("Missing required environment variable API_TRUST_ROLE"); - } - if (Utils.isBlank(CODE_PIPELINE_BUCKET)) { - throw new IllegalStateException("Missing required environment variable CODE_PIPELINE_BUCKET"); - } - LOGGER.info("Version Info: {}", Utils.version(this.getClass())); - this.s3 = Utils.sdkClient(S3Client.builder(), S3Client.SERVICE_NAME); - this.codepipeline = Utils.sdkClient(CodePipelineClient.builder(), CodePipelineClient.SERVICE_NAME); - } - - @Override - public Object handleRequest(Map event, Context context) { - Utils.logRequestEvent(event); - if (validEvent(event)) { - List deployments = getDeployments(event, context); - if (!deployments.isEmpty()) { - LOGGER.info("Deploying for " + deployments.size() + " tenants"); - for (Deployment deployment : deployments) { - try { - String tenantId = deployment.getTenantId(); - - // Create an imagedefinitions.json document for the newly pushed image - byte[] zip = codePipelineArtifact(deployment.getImageName(), deployment.getImageUri()); - - // Write the imagedefinitions.json document to the artifact bucket - writeToArtifactBucket(s3, CODE_PIPELINE_BUCKET, tenantId, deployment.getImageName(), zip); - - // Trigger CodePipeline for this tenant - triggerPipeline(codepipeline, tenantId, deployment.getPipeline()); - } catch (Exception e) { - LOGGER.error("Deployment failed {}", Utils.toJson(deployment)); - LOGGER.error(Utils.getFullStackTrace(e)); - } - } - } else { - LOGGER.warn("No deployments to trigger"); - } - } else { - LOGGER.error("Unrecognized event"); - } - return null; - } - - List getDeployments(Map event, Context context) { - Map detail = (Map) event.get("detail"); - String repo = (String) detail.get("repository-name"); - String tag = (String) detail.get("image-tag"); - - // First, fetch the app config and make sure we're trying to deploy an image from a repo that's - // in the config and for a tag that's in the config - String serviceName = null; - Map appConfig = getAppConfig(context); - Map services = (Map) appConfig.get("services"); - for (Map.Entry serviceConfig : services.entrySet()) { - Map service = (Map) serviceConfig.getValue(); - Map compute = (Map) service.get("compute"); - String containerRepo = (String) compute.get("containerRepo"); - String containerTag = (String) compute.get("containerTag"); - if (repo.equals(containerRepo)) { - if (!tag.equals(containerTag)) { - LOGGER.error("Image tag in event {} does not match appConfig {}", tag, containerTag); - } else { - serviceName = serviceConfig.getKey(); - } - } - } - - List deployments = new ArrayList<>(); - if (serviceName == null) { - LOGGER.error("Can't find event repository in appConfig {}", repo); - } else { - List> tenants = null; - String source = (String) event.get("source"); - if ("aws.ecr".equals(source)) { - tenants = getTenants(context); - } else if ("saas-boost".equals(source)) { - String tenantId = (String) detail.get("tenantId"); - if (Utils.isNotEmpty(tenantId)) { - tenants = getTenants(tenantId, context); - } - } - if (tenants != null && !tenants.isEmpty()) { - for (Map tenant : tenants) { - String tenantId = (String) tenant.get("id"); - String pipelineKey = "SERVICE_" + Utils.toUpperSnakeCase(serviceName) + "_CODE_PIPELINE"; - Map resources = (Map) tenant.get("resources"); - if (resources.containsKey(pipelineKey)) { - Map codePipelineResource = (Map) resources.get(pipelineKey); - String imageName = imageName(tenantId, serviceName); - String imageUri = imageUri(event); - String pipeline = codePipelineResource.get("name"); - Deployment deployment = new Deployment(tenantId, imageName, imageUri, pipeline); - deployments.add(deployment); - } else { - LOGGER.error("Can't find CodePipeline resource {} for tenant {}", pipelineKey, tenantId); - } - } - } else { - LOGGER.warn("No active, provisioned tenants"); - } - } - - return deployments; - } - - protected Map getAppConfig(Context context) { - // Fetch all of the services configured for this application - LOGGER.info("Calling settings service get app config API"); - String getAppConfigResponseBody = ApiGatewayHelper.signAndExecuteApiRequest( - ApiGatewayHelper.getApiRequest( - API_GATEWAY_HOST, - API_GATEWAY_STAGE, - ApiRequest.builder() - .resource("settings/config") - .method("GET") - .build() - ), - API_TRUST_ROLE, - context.getAwsRequestId() - ); - Map appConfig = Utils.fromJson(getAppConfigResponseBody, LinkedHashMap.class); - return appConfig; - } - - protected List> getTenants(Context context) { - return getTenants(null, context); - } - - protected List> getTenants(String tenantId, Context context) { - // Fetch one or all tenants - LOGGER.info("Calling tenants service get tenants API"); - String resource = Utils.isNotEmpty(tenantId) ? "tenants/" + tenantId : "tenants?status=provisioned"; - String getTenantsResponseBody = ApiGatewayHelper.signAndExecuteApiRequest( - ApiGatewayHelper.getApiRequest( - API_GATEWAY_HOST, - API_GATEWAY_STAGE, - ApiRequest.builder() - .resource(resource) - .method("GET") - .build() - ), - API_TRUST_ROLE, - context.getAwsRequestId() - ); - List> tenants; - if (Utils.isNotEmpty(tenantId)) { - tenants = Collections.singletonList(Utils.fromJson(getTenantsResponseBody, LinkedHashMap.class)); - } else { - tenants = Utils.fromJson(getTenantsResponseBody, ArrayList.class); - } - return tenants; - } - - protected static boolean validEvent(Map event) { - boolean validEvent = false; - Map detail = (Map) event.get("detail"); - if (detail != null) { - if ("aws.ecr".equals(event.get("source")) && detail.containsKey("action-type") - && detail.containsKey("result")) { - // Parsing an ECR image event - String action = detail.get("action-type"); - String result = detail.get("result"); - if ("PUSH".equals(action) && "SUCCESS".equals(result)) { - validEvent = true; - } - } else if ("saas-boost".equals(event.get("source")) && detail.containsKey("tenantId")) { - // Parsing an onboarding event - validEvent = true; - } - } - return validEvent; - } - - protected static String imageName(String tenantId, String serviceName) { - // Must match the name in the Task Definition for this imageUri - // In CloudFormation we manipulate the service name to conform to rules on resource names - serviceName = serviceName.replaceAll("[^0-9A-Za-z-]", "").toLowerCase(); - return "sb-" + SAAS_BOOST_ENV + "-tenant-" + tenantId.substring(0, tenantId.indexOf("-")) + "-" + serviceName; - } - - protected static String imageUri(Map event) { - String imageUri = null; - Map detail = (Map) event.get("detail"); - String accountId = (String) event.get("account"); - String region = (String) event.get("region"); - String repo = (String) detail.get("repository-name"); - String tag = (String) detail.get("image-tag"); - if (Utils.isNotBlank(accountId) && Utils.isNotBlank(region) - && Utils.isNotBlank(repo) && Utils.isNotBlank(tag)) { - imageUri = accountId + ".dkr.ecr." + region + "." + Utils.endpointSuffix(region) + "/" + repo + ":" + tag; - } - return imageUri; - } - - protected static byte[] codePipelineArtifact(String imageName, String imageUri) { - LOGGER.info("Creating imagedefinitions.json"); - String imageDefinitions = Utils.toJson(Collections.singletonList( - Map.of("name", imageName, "imageUri", imageUri) - )); - LOGGER.info(imageDefinitions); - - // CodePipeline expects source input artifacts to be in a ZIP file - LOGGER.info("Creating ZIP archive for CodePipeline"); - byte[] zip = zip(imageDefinitions); - return zip; - } - - protected static byte[] zip(String imagedefinitions) { - byte[] archive; - try { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - ZipOutputStream zip = new ZipOutputStream(stream); - ZipEntry entry = new ZipEntry("imagedefinitions.json"); - zip.putNextEntry(entry); - zip.write(imagedefinitions.getBytes(StandardCharsets.UTF_8)); - zip.closeEntry(); - zip.close(); - archive = stream.toByteArray(); - } catch (IOException ioe) { - LOGGER.error("Zip archive generation failed"); - throw new RuntimeException(Utils.getFullStackTrace(ioe)); - } - return archive; - } - - protected static void writeToArtifactBucket(S3Client s3, String bucket, String tenantId, - String imageName, byte[] artifact) { - String key = tenantId + "/" + imageName; - LOGGER.info("Putting CodePipeline source artifact to S3 " + bucket + "/" + key); - try { - s3.putObject(PutObjectRequest.builder() - .bucket(bucket) - .key(key) - .build(), - RequestBody.fromBytes(artifact) - ); - } catch (SdkServiceException s3error) { - LOGGER.error("s3:PutObject " + Utils.getFullStackTrace(s3error)); - throw s3error; - } - } - - private static void triggerPipeline(CodePipelineClient codepipeline, String tenantId, String pipeline) { - try { - StartPipelineExecutionResponse response = codepipeline.startPipelineExecution(r -> r.name(pipeline)); - LOGGER.info("Started tenant {} pipeline {} {}", tenantId, pipeline, response.pipelineExecutionId()); - } catch (SdkServiceException codepipelineError) { - LOGGER.error("codepipeline:StartPipeline", codepipelineError); - LOGGER.error(Utils.getFullStackTrace(codepipelineError)); - throw codepipelineError; - } - } - - private static class Deployment { - private final String tenantId; - private final String imageName; - private final String imageUri; - private final String pipeline; - - public Deployment(String tenantId, String imageName, String imageUri, String pipeline) { - this.tenantId = tenantId; - this.imageName = imageName; - this.imageUri = imageUri; - this.pipeline = pipeline; - } - - public String getTenantId() { - return tenantId; - } - - public String getImageName() { - return imageName; - } - - public String getImageUri() { - return imageUri; - } - - public String getPipeline() { - return pipeline; - } - } - -} \ No newline at end of file diff --git a/functions/workload-deploy/src/main/resources/log4j2.xml b/functions/workload-deploy/src/main/resources/log4j2.xml deleted file mode 100644 index 04128110..00000000 --- a/functions/workload-deploy/src/main/resources/log4j2.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} %X{AWSRequestId} %-5p %C{1} - %m%n - - - - - - - - - - - - diff --git a/functions/workload-deploy/update.sh b/functions/workload-deploy/update.sh deleted file mode 100755 index 7c407ed2..00000000 --- a/functions/workload-deploy/update.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -echo "AWS Region = $MY_AWS_REGION" - -if [ "X$1" = "X" ]; then - echo "usage: $0 " - exit 2 -fi -ENVIRONMENT=$1 -LAMBDA_STAGE_FOLDER=$2 -if [ "X$LAMBDA_STAGE_FOLDER" = "X" ]; then - LAMBDA_STAGE_FOLDER="lambdas" -fi - -LAMBDA_CODE=WorkloadDeploy-lambda.zip - -#set this for V2 AWS CLI to disable paging -export AWS_PAGER="" - -SAAS_BOOST_BUCKET=`aws ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query "Parameter.Value" --output text` -echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" -if [ "X$SAAS_BOOST_BUCKET" = "X" ]; then - echo "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET SSM parameter not read from AWS env" - exit 1 -fi - - - -mvn -if [ $? -ne 0 ]; then - echo "Error building project" - exit 1 -fi - -aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ - -FUNCTIONS=("sb-${ENVIRONMENT}-workload-deploy" - ) -eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-workload-deploy\`)] | [].FunctionName' --output text"\) - -for FUNCTION in ${FUNCTIONS[@]}; do - #echo $FUNCTION - aws lambda --region $MY_AWS_REGION update-function-code --function-name $FUNCTION --s3-bucket $SAAS_BOOST_BUCKET --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE -done diff --git a/install.sh b/install.sh index 3e9268fb..259273a8 100755 --- a/install.sh +++ b/install.sh @@ -23,6 +23,10 @@ if [ ! -d "${CURRENT_DIR}/layers/utils" ]; then echo "Directory ${CURRENT_DIR}/layers/utils not found." exit 2 fi +if [ ! -d "${CURRENT_DIR}/layers/saas-boost-api-client-helper/java" ]; then + echo "Directory ${CURRENT_DIR}/layers/saas-boost-api-client-helper not found." + exit 2 +fi # Check for installer dir if [ ! -d "${CURRENT_DIR}/installer" ]; then @@ -38,7 +42,7 @@ fi # check for Java if ! command -v java >/dev/null 2>&1; then - echo "Java version 11 or higher must be installed." + echo "Java version 17 or higher must be installed." exit 2 fi @@ -87,6 +91,14 @@ if [ $? -ne 0 ]; then exit 2 fi +cd ${CURRENT_DIR}/layers/saas-boost-api-client-helper/java +echo "Building SaaS Boost API helper..." +mvn --quiet -Dspotbugs.skip > /dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "Error building SaaS Boost API helper for SaaS Boost." + exit 2 +fi + cd ${CURRENT_DIR}/installer echo "Building installer..." mvn --quiet -Dspotbugs.skip > /dev/null 2>&1 diff --git a/installer/pom.xml b/installer/pom.xml index 99327ec7..d2056399 100644 --- a/installer/pom.xml +++ b/installer/pom.xml @@ -34,6 +34,7 @@ limitations under the License. + ${project.basedir}/.. 90 @@ -97,29 +98,53 @@ limitations under the License. - junit - junit + com.amazon.aws.partners.saasfactory.saasboost + SaaSBoostApiClientHelper + 1.0.0 org.slf4j - slf4j-nop + slf4j-api + + + org.junit.jupiter + junit-jupiter-engine + + + org.junit.jupiter + junit-jupiter-api org.mockito mockito-core + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + org.slf4j + slf4j-nop + org.apache.logging.log4j log4j-slf4j-impl software.amazon.awssdk - cloudformation + apache-client + ${aws.java.sdk.version} + + + software.amazon.awssdk + iam-policy-builder ${aws.java.sdk.version} software.amazon.awssdk - quicksight + cloudformation ${aws.java.sdk.version} @@ -157,11 +182,6 @@ limitations under the License. sts ${aws.java.sdk.version} - - software.amazon.awssdk - secretsmanager - ${aws.java.sdk.version} - software.amazon.awssdk route53 diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostArtifactsBucket.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostArtifactsBucket.java index be197c0a..98684b63 100644 --- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostArtifactsBucket.java +++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostArtifactsBucket.java @@ -20,6 +20,7 @@ import org.slf4j.LoggerFactory; import software.amazon.awssdk.core.exception.SdkServiceException; import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.policybuilder.iam.*; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.*; @@ -34,16 +35,30 @@ public class SaaSBoostArtifactsBucket { private final String bucketName; private final Region region; + private final String appPlaneAccountId; public SaaSBoostArtifactsBucket(String bucketName, Region region) { + this(bucketName, region, null); + } + + public SaaSBoostArtifactsBucket(String bucketName, Region region, String appPlaneAccountId) { this.bucketName = bucketName; this.region = region; + this.appPlaneAccountId = appPlaneAccountId; } public String getBucketName() { return bucketName; } + public Region getRegion() { + return region; + } + + public String getAppPlaneAccountId() { + return appPlaneAccountId; + } + public String toString() { return getBucketName(); } @@ -62,7 +77,7 @@ public void putFile(S3Client s3, Path localPath, Path remotePath) { s3.putObject(PutObjectRequest.builder() .bucket(bucketName) // java.nio.file.Path will use OS dependent file separators, so when we run the installer on - // Windows, the S3 key will have back slashes instead of forward slashes. The CloudFormation + // Windows, the S3 key will have backslashes instead of forward slashes. The CloudFormation // definitions of the Lambda functions will always use forward slashes for the S3Key property. .key(remotePath.toString().replace('\\', '/')) .build(), RequestBody.fromFile(localPath) @@ -74,9 +89,9 @@ public void putFile(S3Client s3, Path localPath, Path remotePath) { } } - protected static SaaSBoostArtifactsBucket createS3ArtifactBucket(S3Client s3, String envName, Region awsRegion) { + protected static SaaSBoostArtifactsBucket createS3ArtifactBucket(S3Client s3, String envName, Region awsRegion + , String appPlaneAccountId) { String s3ArtifactBucketName = "sb-" + envName + "-artifacts-" + Utils.randomString(12, "[^a-z0-9]"); - LOGGER.info("Creating S3 Artifact Bucket {}", s3ArtifactBucketName); try { CreateBucketRequest.Builder createBucketRequestBuilder = CreateBucketRequest.builder(); // LocationConstraint is not valid in US_EAST_1 @@ -86,13 +101,16 @@ protected static SaaSBoostArtifactsBucket createS3ArtifactBucket(S3Client s3, St config.locationConstraint(BucketLocationConstraint.fromValue(awsRegion.id()))); } createBucketRequestBuilder.bucket(s3ArtifactBucketName); + LOGGER.info("Creating S3 artifact bucket {}", s3ArtifactBucketName); s3.createBucket(createBucketRequestBuilder.build()); + LOGGER.info("Enabling EventBridge bucket notifications {}", s3ArtifactBucketName); s3.putBucketNotificationConfiguration(PutBucketNotificationConfigurationRequest.builder() .bucket(s3ArtifactBucketName) .notificationConfiguration(NotificationConfiguration.builder() .eventBridgeConfiguration(EventBridgeConfiguration.builder().build()) .build()) .build()); + LOGGER.info("Setting default bucket encryption {}", s3ArtifactBucketName); s3.putBucketEncryption(PutBucketEncryptionRequest.builder() .bucket(s3ArtifactBucketName) .serverSideEncryptionConfiguration(ServerSideEncryptionConfiguration.builder() @@ -103,35 +121,42 @@ protected static SaaSBoostArtifactsBucket createS3ArtifactBucket(S3Client s3, St .build()) .build()) .build()); - final String partitionName = awsRegion.metadata().partition().id(); + String partitionName = awsRegion.metadata().partition().id(); + IamPolicy policy = IamPolicy.builder() + .addStatement(statement -> statement + .sid("DenyNonHttps") + .effect(IamEffect.DENY) + .addPrincipal(IamPrincipal.ALL) + .addAction("s3:*") + .addResource("arn:" + partitionName + ":s3:::" + s3ArtifactBucketName + "/*") + .addResource("arn:" + partitionName + ":s3:::" + s3ArtifactBucketName) + .addCondition(condition -> condition + .operator(IamConditionOperator.BOOL) + .key("aws:SecureTransport") + .value("false") + ) + ) + .addStatement(statement -> statement + .sid("AppPlaneAccountQuickLink") + .effect(IamEffect.ALLOW) + .addPrincipal(IamPrincipalType.AWS, "arn:aws:iam::" + appPlaneAccountId + ":root") + .addAction("s3:GetObject") + .addResource("arn:" + partitionName + ":s3:::" + s3ArtifactBucketName + "/saas-boost-app-integration.yaml") + ) + .build(); + String bucketPolicy = policy.toJson(IamPolicyWriter.builder().prettyPrint(true).build()); + LOGGER.info("Creating bucket policy {}", s3ArtifactBucketName); + LOGGER.info(bucketPolicy); s3.putBucketPolicy(PutBucketPolicyRequest.builder() - .policy("{\n" - + " \"Version\": \"2012-10-17\",\n" - + " \"Statement\": [\n" - + " {\n" - + " \"Sid\": \"DenyNonHttps\",\n" - + " \"Effect\": \"Deny\",\n" - + " \"Principal\": \"*\",\n" - + " \"Action\": \"s3:*\",\n" - + " \"Resource\": [\n" - + " \"arn:" + partitionName + ":s3:::" + s3ArtifactBucketName + "/*\",\n" - + " \"arn:" + partitionName + ":s3:::" + s3ArtifactBucketName + "\"\n" - + " ],\n" - + " \"Condition\": {\n" - + " \"Bool\": {\n" - + " \"aws:SecureTransport\": \"false\"\n" - + " }\n" - + " }\n" - + " }\n" - + " ]\n" - + "}") + .policy(bucketPolicy) .bucket(s3ArtifactBucketName) .build()); } catch (SdkServiceException s3Error) { LOGGER.error("s3 error {}", s3Error.getMessage()); LOGGER.error(getFullStackTrace(s3Error)); + //TODO delete bucket if that step worked but the other settings failed throw s3Error; } - return new SaaSBoostArtifactsBucket(s3ArtifactBucketName, awsRegion); + return new SaaSBoostArtifactsBucket(s3ArtifactBucketName, awsRegion, appPlaneAccountId); } } diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java index aa731180..b61d3668 100644 --- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java +++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java @@ -16,7 +16,6 @@ package com.amazon.aws.partners.saasfactory.saasboost; -import com.amazon.aws.partners.saasfactory.saasboost.clients.AwsClientBuilderFactory; import com.amazon.aws.partners.saasfactory.saasboost.model.Environment; import com.amazon.aws.partners.saasfactory.saasboost.model.EnvironmentLoadException; import com.amazon.aws.partners.saasfactory.saasboost.model.ExistingEnvironmentFactory; @@ -24,9 +23,10 @@ import com.amazon.aws.partners.saasfactory.saasboost.workflow.Workflow; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.core.exception.SdkServiceException; import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.http.apache.ApacheHttpClient; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.acm.AcmClient; import software.amazon.awssdk.services.acm.model.*; @@ -34,21 +34,11 @@ import software.amazon.awssdk.services.cloudformation.CloudFormationClient; import software.amazon.awssdk.services.cloudformation.model.*; import software.amazon.awssdk.services.cloudformation.model.Parameter; -import software.amazon.awssdk.services.cloudformation.model.ResourceStatus; import software.amazon.awssdk.services.cloudformation.model.Stack; import software.amazon.awssdk.services.ecr.EcrClient; import software.amazon.awssdk.services.ecr.model.*; import software.amazon.awssdk.services.iam.IamClient; import software.amazon.awssdk.services.iam.model.*; -import software.amazon.awssdk.services.lambda.LambdaClient; -import software.amazon.awssdk.services.lambda.model.InvocationType; -import software.amazon.awssdk.services.lambda.model.InvokeResponse; -import software.amazon.awssdk.services.quicksight.QuickSightClient; -import software.amazon.awssdk.services.quicksight.model.*; -import software.amazon.awssdk.services.quicksight.model.ListUsersRequest; -import software.amazon.awssdk.services.quicksight.model.ListUsersResponse; -import software.amazon.awssdk.services.quicksight.model.Tag; -import software.amazon.awssdk.services.quicksight.model.User; import software.amazon.awssdk.services.route53.Route53Client; import software.amazon.awssdk.services.route53.model.HostedZone; import software.amazon.awssdk.services.route53.model.ListHostedZonesByNameRequest; @@ -56,16 +46,14 @@ import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.*; import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; -import software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest; -import software.amazon.awssdk.services.secretsmanager.model.ResourceNotFoundException; -import software.amazon.awssdk.services.secretsmanager.model.SecretsManagerException; import software.amazon.awssdk.services.ssm.SsmClient; import software.amazon.awssdk.services.ssm.model.*; +import software.amazon.awssdk.services.sts.StsClient; import java.io.*; -import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.stream.Collectors; @@ -82,19 +70,19 @@ public class SaaSBoostInstall { + static { + System.setProperty("logger.timestamp", + DateTimeFormatter.ofPattern("yyyy-MM-dd-HH:mm:ss").format(LocalDateTime.now())); + } + private static final Logger LOGGER = LoggerFactory.getLogger(SaaSBoostInstall.class); - private final AwsClientBuilderFactory awsClientBuilderFactory; private final ApiGatewayClient apigw; private final CloudFormationClient cfn; private final EcrClient ecr; private final IamClient iam; - // TODO do we need to reassign the quicksight client between getQuickSightUsername and setupQuicksight? - private QuickSightClient quickSight; private final S3Client s3; private final SsmClient ssm; - private final LambdaClient lambda; - private final SecretsManagerClient secretsManager; private final Route53Client route53; private final AcmClient acm; @@ -106,19 +94,15 @@ public class SaaSBoostInstall { private String lambdaSourceFolder = "lambdas"; private String stackName; private Map baseStackDetails = new HashMap<>(); - private boolean useAnalyticsModule = false; - private boolean useQuickSight = false; - private String quickSightUsername; - private String quickSightUserArn; + private SaaSBoostApiHelper api; protected enum ACTION { - INSTALL(1, "New AWS SaaS Boost install.", false), - ADD_ANALYTICS(2, "Install Metrics and Analytics into existing AWS SaaS Boost deployment.", true), - UPDATE_WEB_APP(3, "Update Web Application for existing AWS SaaS Boost deployment.", true), - UPDATE(4, "Update existing AWS SaaS Boost deployment.", true), - DELETE(5, "Delete existing AWS SaaS Boost deployment.", true), - CANCEL(6, "Exit installer.", false); - //DEBUG(7, "Debug", false); + INSTALL(1, "Install AWS SaaS Boost", false), + UPDATE(2, "Update AWS SaaS Boost", true), + UPDATE_WEB_APP(3, "Update Admin Web Application", true), + DELETE(4, "Delete AWS SaaS Boost", true), + CANCEL(5, "Exit", false), + DEBUG(6, "Debug", false); private final int choice; private final String prompt; @@ -151,23 +135,16 @@ public static ACTION ofChoice(int choice) { } public SaaSBoostInstall() { - awsClientBuilderFactory = AwsClientBuilderFactory.builder() - .region(AWS_REGION) - .build(); - - apigw = awsClientBuilderFactory.apiGatewayBuilder().build(); - cfn = awsClientBuilderFactory.cloudFormationBuilder().build(); - ecr = awsClientBuilderFactory.ecrBuilder().build(); - iam = awsClientBuilderFactory.iamBuilder().build(); - lambda = awsClientBuilderFactory.lambdaBuilder().build(); - quickSight = awsClientBuilderFactory.quickSightBuilder().build(); - s3 = awsClientBuilderFactory.s3Builder().build(); - ssm = awsClientBuilderFactory.ssmBuilder().build(); - secretsManager = awsClientBuilderFactory.secretsManagerBuilder().build(); - route53 = awsClientBuilderFactory.route53Builder().build(); - acm = awsClientBuilderFactory.acmBuilder().build(); - - accountId = awsClientBuilderFactory.stsBuilder().build().getCallerIdentity().account(); + apigw = Utils.sdkClient(ApiGatewayClient.builder(), ApiGatewayClient.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create()); + cfn = Utils.sdkClient(CloudFormationClient.builder(), CloudFormationClient.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create()); + ecr = Utils.sdkClient(EcrClient.builder(), EcrClient.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create()); + iam = Utils.sdkClient(IamClient.builder(), IamClient.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create()); + s3 = Utils.sdkClient(S3Client.builder(), S3Client.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create()); + ssm = Utils.sdkClient(SsmClient.builder(), SsmClient.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create()); + route53 = Utils.sdkClient(Route53Client.builder(), Route53Client.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create()); + acm = Utils.sdkClient(AcmClient.builder(), AcmClient.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create()); + StsClient sts = Utils.sdkClient(StsClient.builder(), StsClient.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create()); + accountId = sts.getCallerIdentity().account(); } public static void main(String[] args) { @@ -181,13 +158,51 @@ public static void main(String[] args) { } catch (Exception e) { outputMessage("==========================================================="); outputMessage("Installation Error: " + e.getLocalizedMessage()); - outputMessage("Please see detailed log file saas-boost-install.log"); - LOGGER.error(getFullStackTrace(e)); + outputMessage("Please see detailed log file installer-" + + System.getProperty("logger.timestamp") + ".log"); + LOGGER.error(Utils.getFullStackTrace(e)); } } protected void debug(String existingBucket) { - copyAdminWebAppSourceToS3(workingDir, null, null); + while (true) { + System.out.print("Enter name of the AWS SaaS Boost environment to deploy (dev, test, uat, prod, etc...): "); + this.envName = Keyboard.readString(); + if (validateEnvironmentName(this.envName)) { + LOGGER.info("Setting SaaS Boost environment = [{}]", this.envName); + break; + } else { + outputMessage("Entered value is invalid, maximum of 10 alphanumeric characters, and cannot be AWS," + + " Amazon, or Cognito. Please try again."); + } + } + if (existingBucket != null) { + saasBoostArtifactsBucket = new SaaSBoostArtifactsBucket(existingBucket, AWS_REGION); + try { + s3.headBucket(request -> request.bucket(saasBoostArtifactsBucket.getBucketName())); + } catch (SdkServiceException s3error) { + outputMessage("Bucket " + existingBucket + " does not exist!"); + throw s3error; + } + } +// else { +// saasBoostArtifactsBucket = SaaSBoostArtifactsBucket.createS3ArtifactBucket(s3, envName, AWS_REGION); +// outputMessage("Created S3 artifacts bucket: " + saasBoostArtifactsBucket); +// } +// +// // Copy the CloudFormation templates +// outputMessage("Uploading CloudFormation templates to S3 artifacts bucket"); +// copyResourcesToS3(); +// +// // Compile all the source code +// outputMessage("Compiling Lambda functions and uploading to S3 artifacts bucket. This will take some time..."); +// processLambdas(); +// +// // Copy the source files up to S3 where CloudFormation resources expect them to be +// outputMessage("Uploading admin web app source files to S3"); +// copyAdminWebAppSourceToS3(workingDir, saasBoostArtifactsBucket.getBucketName(), s3); + outputMessage("Log into the Application Plane AWS Account and run the CloudFormation Integration Stack"); + outputMessage(quickCreateLink()); } public void start(String existingBucket) { @@ -195,7 +210,7 @@ public void start(String existingBucket) { outputMessage("Welcome to the AWS SaaS Boost Installer"); outputMessage("Installer Version: " + VERSION); - // Do we have Maven, Node and AWS CLI on the PATH? + // Do we have Maven and the AWS CLI on the PATH? checkEnvironment(); ACTION installOption; @@ -228,38 +243,22 @@ public void start(String existingBucket) { switch (installOption) { case INSTALL: - installSaaSBoost(existingBucket); + install(existingBucket); break; case UPDATE: - workflow = new UpdateWorkflow( - this.workingDir, - this.environment, - this.awsClientBuilderFactory, - doesCfnMacroResourceExist()); + workflow = new UpdateWorkflow(this.workingDir, this.environment, this.s3, this.cfn, this.apigw); break; case UPDATE_WEB_APP: SaaSBoostInstall.copyAdminWebAppSourceToS3(this.workingDir, this.saasBoostArtifactsBucket.getBucketName(), this.s3); break; - case ADD_ANALYTICS: - this.useAnalyticsModule = true; - System.out.print("Would you like to setup Amazon Quicksight for the Analytics module?" - + "You must have already registered for Quicksight in your account (y or n)? "); - this.useQuickSight = Keyboard.readBoolean(); - if (this.useQuickSight) { - getQuickSightUsername(); - } - installAnalyticsModule(); - break; case DELETE: - deleteSaasBoostInstallation(); + delete(); break; - case CANCEL: - cancel(); + case DEBUG: + debug(existingBucket); break; - //case DEBUG: - // debug(existingBucket); - // break; + case CANCEL: default: cancel(); } @@ -270,7 +269,7 @@ public void start(String existingBucket) { } } - protected void installSaaSBoost(String existingBucket) { + protected void install(String existingBucket) { LOGGER.info("Performing new installation of AWS SaaS Boost"); while (true) { System.out.print("Enter name of the AWS SaaS Boost environment to deploy (Ex. dev, test, uat, prod, etc.): "); @@ -304,16 +303,17 @@ protected void installSaaSBoost(String existingBucket) { String systemIdentityProvider; while (true) { - System.out.print("Enter the identity provider to use for system users (Cognito or Keycloak) Press Enter for 'Cognito': "); + System.out.print("Enter the identity provider to use for system users (Cognito, Keycloak, or Auth0) Press Enter for 'Cognito': "); systemIdentityProvider = Keyboard.readString(); if (isNotBlank(systemIdentityProvider)) { if (systemIdentityProvider.toUpperCase().equals("COGNITO") - || systemIdentityProvider.toUpperCase().equals("KEYCLOAK")) { + || systemIdentityProvider.toUpperCase().equals("KEYCLOAK") + || systemIdentityProvider.toUpperCase().equals("AUTH0")) { systemIdentityProvider = systemIdentityProvider.toUpperCase(); LOGGER.info("Setting Identity Provider = [{}]", systemIdentityProvider); break; } else { - outputMessage("Invalid identity provider. Enter either Cognito or Keycloak."); + outputMessage("Invalid identity provider. Enter either Cognito, Keycloak, or Auth0."); } } else { systemIdentityProvider = "COGNITO"; @@ -407,10 +407,19 @@ protected void installSaaSBoost(String existingBucket) { } } - boolean useCustomDomainForAdminWebApp = Utils.isChinaRegion(AWS_REGION); + String auth0ApiKey = ""; + String auth0ApiClientId = ""; + if ("AUTH0".equals(systemIdentityProvider)) { + + } + + Boolean useCustomDomainForAdminWebApp = Utils.isChinaRegion(AWS_REGION); if (!useCustomDomainForAdminWebApp) { - System.out.print("Would you like to use a custom domain name for the SaaS Boost admin web console (y or n)? "); + System.out.print("Would you like to use a custom domain name for the SaaS Boost admin web console (y or n) Press Enter for 'n'? "); useCustomDomainForAdminWebApp = Keyboard.readBoolean(); + if (useCustomDomainForAdminWebApp == null) { + useCustomDomainForAdminWebApp = Boolean.FALSE; + } } String adminWebAppCustomDomain = null; String adminWebAppHostedZone = null; @@ -549,16 +558,16 @@ protected void installSaaSBoost(String existingBucket) { } } - System.out.print("Would you like to install the metrics and analytics module of AWS SaaS Boost (y or n)? "); - this.useAnalyticsModule = Keyboard.readBoolean(); - - // If installing the analytics module, ask about QuickSight. - if (useAnalyticsModule) { - System.out.print("Would you like to setup Amazon Quicksight for the Analytics module? You must have already registered for Quicksight in your account (y or n)? "); - this.useQuickSight = Keyboard.readBoolean(); - } - if (this.useQuickSight) { - getQuickSightUsername(); + String appPlaneAccountId; + while (true) { + System.out.print("Enter the AWS Account ID when you will install the application plane components for this SaaS Boost control plane: "); + appPlaneAccountId = Objects.toString(Keyboard.readString(), "").replace("-", ""); + if (validateAwsAccountId(appPlaneAccountId)) { + LOGGER.info("Setting app plane account = [{}]", appPlaneAccountId); + break; + } else { + outputMessage("Entered value is invalid. Enter a 12 digit AWS Account ID. Please try again."); + } } System.out.println(); @@ -574,12 +583,7 @@ protected void installSaaSBoost(String existingBucket) { + (isNotBlank(identityProviderCustomDomain) ? identityProviderCustomDomain : "N/A")); outputMessage("Custom Domain for SaaS Boost Admin Web Console: " + (isNotBlank(adminWebAppCustomDomain) ? adminWebAppCustomDomain : "N/A")); - outputMessage("Install optional Analytics Module: " + this.useAnalyticsModule); - if (this.useAnalyticsModule && isNotBlank(this.quickSightUsername)) { - outputMessage("Amazon QuickSight user for Analytics Module: " + this.quickSightUsername); - } else { - outputMessage("Amazon QuickSight user for Analytics Module: N/A"); - } + outputMessage("Application Plane Account ID: " + appPlaneAccountId); System.out.println(); System.out.print("Continue (y or n)? "); @@ -600,7 +604,8 @@ protected void installSaaSBoost(String existingBucket) { if (existingBucket == null) { // Create the S3 artifacts bucket outputMessage("Creating S3 artifacts bucket"); - saasBoostArtifactsBucket = SaaSBoostArtifactsBucket.createS3ArtifactBucket(s3, envName, AWS_REGION); + saasBoostArtifactsBucket = SaaSBoostArtifactsBucket.createS3ArtifactBucket(s3, envName, AWS_REGION + , appPlaneAccountId); outputMessage("Created S3 artifacts bucket: " + saasBoostArtifactsBucket); // Copy the CloudFormation templates @@ -612,59 +617,63 @@ protected void installSaaSBoost(String existingBucket) { processLambdas(); } else { outputMessage("Reusing existing artifacts bucket " + existingBucket); - saasBoostArtifactsBucket = new SaaSBoostArtifactsBucket(existingBucket, AWS_REGION); - outputMessage("Uploading CloudFormation templates to S3 artifacts bucket"); - copyResourcesToS3(); + saasBoostArtifactsBucket = new SaaSBoostArtifactsBucket(existingBucket, AWS_REGION, appPlaneAccountId); try { s3.headBucket(request -> request.bucket(saasBoostArtifactsBucket.getBucketName())); } catch (SdkServiceException s3error) { outputMessage("Bucket " + existingBucket + " does not exist!"); throw s3error; } + outputMessage("Uploading CloudFormation templates to S3 artifacts bucket"); + copyResourcesToS3(); } // Copy the source files up to S3 where CloudFormation resources expect them to be outputMessage("Uploading admin web app source files to S3"); copyAdminWebAppSourceToS3(workingDir, saasBoostArtifactsBucket.getBucketName(), s3); + // Copy the Api docs (SwaggerUI) source files up to S3 where CloudFormation resources expect them to be + outputMessage("Uploading api docs source files to S3"); + copyApiDocsSourceToS3(workingDir, saasBoostArtifactsBucket.getBucketName(), s3); + // Run CloudFormation create stack outputMessage("Running CloudFormation"); this.stackName = "sb-" + envName; createSaaSBoostStack(stackName, emailAddress, systemIdentityProvider, identityProviderCustomDomain, identityProviderHostedZone, identityProviderCertificate, adminWebAppCustomDomain, - adminWebAppHostedZone, adminWebAppCertificate); + adminWebAppHostedZone, adminWebAppCertificate, appPlaneAccountId); this.environment = ExistingEnvironmentFactory.findExistingEnvironment( ssm, cfn, this.envName, this.accountId); this.baseStackDetails = environment.getBaseCloudFormationStackInfo(); - if (useAnalyticsModule) { - LOGGER.info("Install metrics and analytics module"); - // The analytics module stack reads baseStackDetails for its CloudFormation template parameters - // because we're not yet creating the analytics resources as a nested child stack of the main stack - installAnalyticsModule(); - } - outputMessage("Check the admin email box for the temporary password."); + outputMessage("Check the admin email inbox for the temporary password."); outputMessage("AWS SaaS Boost Artifacts Bucket: " + saasBoostArtifactsBucket); outputMessage("AWS SaaS Boost Console URL is: " + baseStackDetails.get("AdminWebUrl")); + + outputMessage("Log into the Application Plane AWS Account and run the CloudFormation Integration Stack:"); + outputMessage(quickCreateLink()); } - protected void deleteSaasBoostInstallation() { + protected void delete() { // Confirm delete outputMessage("****** W A R N I N G"); - outputMessage("Deleting the AWS SaaS Boost environment is IRREVERSIBLE and ALL deployed tenant resources will be deleted!"); + outputMessage("Deleting the AWS SaaS Boost environment is IRREVERSIBLE and " + + "ALL deployed tenant resources will be deleted!"); while (true) { System.out.print("Enter the SaaS Boost environment name to confirm: "); String confirmEnvName = Keyboard.readString(); if (isNotBlank(confirmEnvName) && this.envName.equalsIgnoreCase(confirmEnvName)) { - System.out.println("SaaS Boost environment " + this.envName + " for AWS Account " + this.accountId + " in region " + AWS_REGION + " will be deleted. This action cannot be undone!"); + System.out.println("SaaS Boost environment " + this.envName + " for AWS Account " + this.accountId + + " in region " + AWS_REGION + " will be deleted. This action cannot be undone!"); break; } else { outputMessage("Entered value is incorrect, please try again."); } } - System.out.print("Are you sure you want to delete the SaaS Boost environment " + this.envName + "? Enter y to continue or n to cancel: "); + System.out.print("Are you sure you want to delete the SaaS Boost environment " + + this.envName + "? Enter y to continue or n to cancel: "); boolean continueDelete = Keyboard.readBoolean(); if (!continueDelete) { outputMessage("Canceled Delete of AWS SaaS Boost environment"); @@ -674,48 +683,27 @@ protected void deleteSaasBoostInstallation() { } // Delete all the provisioned tenants - List> tenants = getProvisionedTenants(); - for (LinkedHashMap tenant : tenants) { + List> tenants = getProvisionedTenants(); + LOGGER.debug("Deleting {} provisioned tenants", tenants.size()); + for (Map tenant : tenants) { outputMessage("Deleting AWS SaaS Boost tenant " + tenant.get("id")); deleteProvisionedTenant(tenant); } // Clear all the images from ECR or CloudFormation won't be able to delete the repository - try { - for (String ecrRepo : getEcrRepositories()) { - outputMessage("Deleting images from ECR repository " + ecrRepo); - deleteEcrImages(ecrRepo); - } - } catch (SdkServiceException ssmError) { - LOGGER.error("ssm:GetParameter error", ssmError); - LOGGER.error(getFullStackTrace(ssmError)); - // throw ssmError; + LOGGER.debug("Getting list of ECR repos to clear"); + for (String ecrRepo : getEcrRepositories()) { + outputMessage("Deleting images from ECR repository " + ecrRepo); + deleteEcrImages(ecrRepo); } // Clear all the Parameter Store entries for this environment that CloudFormation doesn't own + LOGGER.debug("Deleting AppConfig"); deleteApplicationConfig(); - // Delete the analytics stack if it exists - String analyticsStackName = analyticsStackName(); - if (checkCloudFormationStack(analyticsStackName)) { - outputMessage("Deleting AWS SaaS Boost Analytics Module stack: " + analyticsStackName); - deleteCloudFormationStack(analyticsStackName); - } - // Delete the SaaS Boost stack outputMessage("Deleting AWS SaaS Boost stack: " + this.stackName); deleteCloudFormationStack(this.stackName); - // Delete the ActiveDirectory password in SecretsManager if it exists - try { - secretsManager.deleteSecret(request -> request - .forceDeleteWithoutRecovery(true) - .secretId("/saas-boost/" + envName + "/ACTIVE_DIRECTORY_PASSWORD") - .build() - ); - outputMessage("ActiveDirectory secretsManager secret deleted."); - } catch (ResourceNotFoundException rnfe) { - // there is no ACTIVE_DIRECTORY_PASSWORD secret, so there is nothing to delete - } // Finally, remove the S3 artifacts bucket that this installer created outside of CloudFormation LOGGER.info("Clean up s3 bucket: " + saasBoostArtifactsBucket); @@ -745,185 +733,12 @@ protected void deleteSaasBoostInstallation() { } catch (SdkServiceException ssmError) { outputMessage("Failed to delete all Parameter Store entries"); LOGGER.error("ssm:DeleteParameters error", ssmError); - LOGGER.error(getFullStackTrace(ssmError)); + LOGGER.error(Utils.getFullStackTrace(ssmError)); } outputMessage("Delete of SaaS Boost environment " + this.envName + " complete."); } - private List getEcrRepositories() { - List repos = new ArrayList<>(); - Map systemApiRequest = new HashMap<>(); - Map detail = new HashMap<>(); - detail.put("resource", "settings/config"); - detail.put("method", "GET"); - systemApiRequest.put("detail", detail); - final byte[] payload = Utils.toJson(systemApiRequest).getBytes(); - try { - LOGGER.info("Invoking getSettings API"); - InvokeResponse response = lambda.invoke(request -> request - .functionName("sb-" + this.envName + "-private-api-client") - .invocationType(InvocationType.REQUEST_RESPONSE) - .payload(SdkBytes.fromByteArray(payload)) - ); - if (response.sdkHttpResponse().isSuccessful()) { - LOGGER.error("got response back: {}", response); - String configJson = response.payload().asUtf8String(); - HashMap config = Utils.fromJson(configJson, HashMap.class); - HashMap services = (HashMap) config.get("services"); - for (String serviceName : services.keySet()) { - HashMap service = (HashMap) services.get(serviceName); - Map compute = (Map) service.get("compute"); - repos.add((String) compute.get("containerRepo")); - } - } else { - LOGGER.warn("Private API client Lambda returned HTTP " + response.sdkHttpResponse().statusCode()); - throw new RuntimeException(response.sdkHttpResponse().statusText().get()); - } - } catch (SdkServiceException lambdaError) { - LOGGER.error("lambda:Invoke error", lambdaError); - LOGGER.error(getFullStackTrace(lambdaError)); - throw lambdaError; - } - return repos; - } - - protected void installAnalyticsModule() { - LOGGER.info("Installing Analytics module into existing AWS SaaS Boost installation."); - outputMessage("Analytics will be deployed into the existing AWS SaaS Boost environment " + this.envName + "."); - - String metricsStackName = analyticsStackName(); - try { - DescribeStacksResponse metricsStackResponse = cfn.describeStacks(request -> request.stackName(metricsStackName)); - if (metricsStackResponse.hasStacks()) { - outputMessage("AWS SaaS Boost Analytics stack with name: " + metricsStackName + " is already deployed"); - System.exit(2); - } - } catch (CloudFormationException cfnError) { - // Calling describe-stacks on a stack name that doesn't exist is an exception - if (!cfnError.getMessage().contains("Stack with id " + metricsStackName + " does not exist")) { - LOGGER.error("cloudformation:DescribeStacks error {}", cfnError.getMessage()); - LOGGER.error(getFullStackTrace(cfnError)); - throw cfnError; - } - } - - outputMessage("==========================================================="); - outputMessage(""); - outputMessage("Would you like to continue the Analytics module installation with the following options?"); - outputMessage("Existing AWS SaaS Boost environment : " + envName); - if (useQuickSight) { - outputMessage("Amazon QuickSight user for Analytics Module: " + quickSightUsername); - } else { - outputMessage("Amazon QuickSight user for Analytics Module: N/A"); - } - - System.out.print("Continue (y or n)? "); - boolean continueInstall = Keyboard.readBoolean(); - if (!continueInstall) { - outputMessage("Canceled installation of AWS SaaS Boost Analytics"); - cancel(); - } - outputMessage("Continuing installation of AWS SaaS Boost Analytics"); - outputMessage("==========================================================="); - outputMessage("Installing AWS SaaS Boost Metrics and Analytics Module"); - outputMessage("==========================================================="); - - // Generate a password for the RedShift database if we don't already have one - String dbPassword; - String dbPasswordParam = "/saas-boost/" + this.envName + "/REDSHIFT_MASTER_PASSWORD"; - try { - GetParameterResponse existingDbPasswordResponse = ssm.getParameter(GetParameterRequest.builder() - .name(dbPasswordParam) - .withDecryption(true) - .build() - ); - // We actually need the secret value because we need to give it to QuickSight - dbPassword = existingDbPasswordResponse.parameter().value(); - // And, we'll add the parameter version to the end of the name just in case it's greater than 1 - // so that CloudFormation can properly fetch the secret value - dbPasswordParam = dbPasswordParam + ":" + existingDbPasswordResponse.parameter().version(); - LOGGER.info("Reusing existing RedShift password for Analytics"); - } catch (SdkServiceException noSuchParameter) { - LOGGER.info("Generating new random RedShift password for Analytics"); - // Save the database password as a secret - dbPassword = generatePassword(16); - try { - LOGGER.info("Saving RedShift password secret to Parameter Store"); - ssm.putParameter(PutParameterRequest.builder() - .name(dbPasswordParam) - .type(ParameterType.SECURE_STRING) - .overwrite(true) - .value(dbPassword) - .build() - ); - } catch (SdkServiceException ssmError) { - LOGGER.error("ssm:PutParamter error {}", ssmError.getMessage()); - LOGGER.error(getFullStackTrace(ssmError)); - throw ssmError; - } - // CloudFormation ssm-secure resolution needs a version number, which is guaranteed to be 1 - // in this case where we just created it - dbPasswordParam = dbPasswordParam + ":1"; - } - outputMessage("Redshift Database User Password stored in secure SSM Parameter: " + dbPasswordParam); - - // Run CloudFormation - outputMessage("Creating CloudFormation stack " + metricsStackName + " for Analytics Module"); - String databaseName = "sb_analytics_" + this.envName.replaceAll("-", "_"); - createMetricsStack(metricsStackName, dbPasswordParam, databaseName); - - // TODO Why doesn't the CloudFormation template own this? - LOGGER.info("Update SSM param METRICS_ANALYTICS_DEPLOYED to true"); - try { - ssm.putParameter(request -> request - .name("/saas-boost/" + this.envName + "/METRICS_ANALYTICS_DEPLOYED") - .type(ParameterType.STRING) - .overwrite(true) - .value("true") - ); - } catch (SdkServiceException ssmError) { - LOGGER.error("ssm:PutParameter error {}", ssmError.getMessage()); - LOGGER.error(getFullStackTrace(ssmError)); - throw ssmError; - } - - // Upload the JSON path file for Redshift to the bucket provisioned by CloudFormation - Map outputs = getMetricStackOutputs(metricsStackName); - String metricsBucket = outputs.get("MetricsBucket"); - Path jsonPathFile = workingDir.resolve(Path.of("metrics-analytics", "deploy", "artifacts", "metrics_redshift_jsonpath.json")); - - LOGGER.info("Copying json files for Metrics and Analytics from {} to {}", jsonPathFile.toString(), metricsBucket); - try { - s3.putObject(PutObjectRequest.builder() - .bucket(metricsBucket) - .key("metrics_redshift_jsonpath.json") - .contentType("text/json") - .build(), RequestBody.fromFile(jsonPathFile) - ); - } catch (SdkServiceException s3Error) { - LOGGER.error("s3:PutObject error {}", s3Error.getMessage()); - LOGGER.error(getFullStackTrace(s3Error)); - outputMessage("Error copying " + jsonPathFile.toString() + " to " + metricsBucket); - // TODO Why don't we bail here if that file is required? - outputMessage("Continuing with installation so you will need to manually upload that file."); - } - - // Setup the quicksight dataset - if (useQuickSight) { - outputMessage("Set up Amazon Quicksight for Analytics Module"); - try { - // TODO does this fail if it's run more than once? - setupQuickSight(metricsStackName, outputs, dbPassword); - } catch (Exception e) { - outputMessage("Error with setup of Quicksight datasource and dataset. Check log file."); - outputMessage("Message: " + e.getMessage()); - LOGGER.error(getFullStackTrace(e)); - System.exit(2); - } - } - } - protected void cancel() { outputMessage("Cancelling."); System.exit(0); @@ -955,202 +770,98 @@ protected static Path getWorkingDirectory() { return workingDir; } - protected void getQuickSightUsername() { - Region quickSightRegion; - QuickSightClient oldClient = null; - while (true) { - System.out.print("Region where you registered for Amazon QuickSight (Press Enter for " + AWS_REGION.id() + "): "); - String quickSightAccountRegion = Keyboard.readString(); - if (isBlank(quickSightAccountRegion)) { - quickSightRegion = AWS_REGION; - } else { - // Make sure we got a valid AWS region string - quickSightRegion = Region.regions().stream().filter(request -> request - .id() - .equals(quickSightAccountRegion)) - .findAny() - .orElse(null); - } - if (quickSightRegion != null) { - // Update the SDK client for the proper AWS region if we need to - if (!AWS_REGION.equals(quickSightRegion)) { - oldClient = quickSight; - quickSight = awsClientBuilderFactory.quickSightBuilder().region(quickSightRegion).build(); - } - // See if there are QuickSight users in this account in this region - LinkedHashMap quickSightUsers = getQuickSightUsers(); - if (!quickSightUsers.isEmpty()) { - String defaultQuickSightUsername = quickSightUsers.keySet().stream().findFirst().orElse(null); - System.out.print("Amazon Quicksight user name (Press Enter for '" + defaultQuickSightUsername + "'): "); - this.quickSightUsername = Keyboard.readString(); - if (isBlank(this.quickSightUsername)) { - this.quickSightUsername = defaultQuickSightUsername; - } - if (quickSightUsers.containsKey(this.quickSightUsername)) { - this.quickSightUserArn = quickSightUsers.get(this.quickSightUsername).arn(); - break; - } else { - outputMessage("Entered value is not a valid Quicksight user in your account, please try again."); - } - } else { - outputMessage("No users found in QuickSight. Please register in your AWS Account and try install again."); - System.exit(2); - } - } else { - outputMessage("Entered value is not a region, please try again."); - } + protected SaaSBoostApiHelper api() { + if (api == null) { + SaaSBoostApiHelper.SaaSBoostApiHelperDependencyFactory init = () -> + Utils.sdkClient(SecretsManagerClient.builder(), SecretsManagerClient.SERVICE_NAME, + ApacheHttpClient.builder(), DefaultCredentialsProvider.create()); + String secretId = "/saas-boost/" + envName + "/PRIVATE_API_APP_CLIENT"; + api = new SaaSBoostApiHelper(init, secretId); } - // If we changed the QuickSight SDK client region to look up the username, put it back - if (oldClient != null) { - quickSight = oldClient; + return api; + } + + protected List> getProvisionedTenants() { + LOGGER.info("Calling tenant service to fetch all provisioned tenants"); + String getTenantsResponseBody = api().authorizedRequest("GET", "tenants?status=provisioned"); + List> tenants = Utils.fromJson(getTenantsResponseBody, ArrayList.class); + if (tenants == null) { + tenants = new ArrayList<>(); } + return tenants; } - protected List> getProvisionedTenants() { - List> provisionedTenants = new ArrayList<>(); - Map systemApiRequest = new HashMap<>(); - Map detail = new HashMap<>(); - detail.put("resource", "tenants"); - detail.put("method", "GET"); - systemApiRequest.put("detail", detail); - final byte[] payload = Utils.toJson(systemApiRequest).getBytes(StandardCharsets.UTF_8); - try { - LOGGER.info("Invoking get provisioned tenants API"); - InvokeResponse response = lambda.invoke(request -> request - .functionName("sb-" + this.envName + "-private-api-client") - .invocationType(InvocationType.REQUEST_RESPONSE) - .payload(SdkBytes.fromByteArray(payload)) - ); - if (response.sdkHttpResponse().isSuccessful()) { - String responseBody = response.payload().asUtf8String(); - LOGGER.info("Response Body"); - LOGGER.info(responseBody); - provisionedTenants = Utils.fromJson(responseBody, ArrayList.class); - LOGGER.info("Loaded " + provisionedTenants.size() + " tenants"); - } else { - LOGGER.warn("Private API client Lambda returned HTTP " + response.sdkHttpResponse().statusCode()); - throw new RuntimeException(response.sdkHttpResponse().statusText().get()); - } - } catch (SdkServiceException lambdaError) { - LOGGER.error("lambda:Invoke error", lambdaError); - LOGGER.error(getFullStackTrace(lambdaError)); - throw lambdaError; + protected Map getTenant(String tenantId) { + LOGGER.info("Calling tenant service to fetch tenant {}", tenantId); + String getTenantResponseBody = api().authorizedRequest("GET", "tenants/" + tenantId); + Map tenant = Utils.fromJson(getTenantResponseBody, HashMap.class); + if (tenant == null) { + return Collections.emptyMap(); } - return provisionedTenants; + return tenant; } - private LinkedHashMap getTenant(String tenantId) { - LinkedHashMap tenantDetail = new LinkedHashMap<>(); - Map systemApiRequest = new HashMap<>(); - Map detail = new HashMap<>(); - detail.put("resource", "tenants/" + tenantId); - detail.put("method", "GET"); - systemApiRequest.put("detail", detail); - final byte[] payload = Utils.toJson(systemApiRequest).getBytes(); - try { - LOGGER.info("Invoking get tenant by id API"); - InvokeResponse response = lambda.invoke(request -> request - .functionName("sb-" + this.envName + "-private-api-client") - .invocationType(InvocationType.REQUEST_RESPONSE) - .payload(SdkBytes.fromByteArray(payload)) - ); - if (response.sdkHttpResponse().isSuccessful()) { - String responseBody = response.payload().asUtf8String(); - LOGGER.info("Response Body"); - LOGGER.info(responseBody); - tenantDetail = Utils.fromJson(responseBody, LinkedHashMap.class); - } else { - LOGGER.warn("Private API client Lambda returned HTTP " + response.sdkHttpResponse().statusCode()); - throw new RuntimeException(response.sdkHttpResponse().statusText().get()); - } - } catch (SdkServiceException lambdaError) { - LOGGER.error("lambda:Invoke error", lambdaError); - LOGGER.error(getFullStackTrace(lambdaError)); - throw lambdaError; + protected Map getAppConfig() { + LOGGER.info("Calling appConfig service to fetch appConfig"); + String getAppConfigResponseBody = api().authorizedRequest("GET", "appconfig"); + Map appConfig = Utils.fromJson(getAppConfigResponseBody, HashMap.class); + if (appConfig == null) { + return Collections.emptyMap(); } - return tenantDetail; + return appConfig; } protected void deleteApplicationConfig() { - Map systemApiRequest = new HashMap<>(); - Map detail = new HashMap<>(); - detail.put("resource", "settings/config"); - detail.put("method", "DELETE"); - systemApiRequest.put("detail", detail); - final byte[] payload = Utils.toJson(systemApiRequest).getBytes(StandardCharsets.UTF_8); + LOGGER.info("Calling appConfig service to delete appConfig"); try { - LOGGER.info("Invoking delete application config API"); - InvokeResponse response = lambda.invoke(request -> request - .functionName("sb-" + this.envName + "-private-api-client") - .invocationType(InvocationType.REQUEST_RESPONSE) - .payload(SdkBytes.fromByteArray(payload)) - ); - if (!response.sdkHttpResponse().isSuccessful()) { - LOGGER.warn("Private API client Lambda returned HTTP " + response.sdkHttpResponse().statusCode()); - throw new RuntimeException(response.sdkHttpResponse().statusText().get()); - } - } catch (SdkServiceException lambdaError) { - LOGGER.error("lambda:Invoke error", lambdaError); - LOGGER.error(getFullStackTrace(lambdaError)); - throw lambdaError; + api().authorizedRequest("DELETE", "appconfig"); + } catch (Exception apiError) { + LOGGER.error(apiError.getMessage()); } } - protected void deleteProvisionedTenant(LinkedHashMap tenant) { + protected void deleteProvisionedTenant(Map tenant) { // TODO we can parallelize to improve performance with lots of tenants - Map detail = new HashMap<>(); - detail.put("resource", "tenants/" + tenant.get("id")); - detail.put("method", "DELETE"); - String tenantId = (String) tenant.get("id"); - Map tenantIdOnly = new HashMap<>(); - tenantIdOnly.put("id", tenantId); - detail.put("body", Utils.toJson(tenantIdOnly)); - Map systemApiRequest = new HashMap<>(); - systemApiRequest.put("detail", detail); - final byte[] payload = Utils.toJson(systemApiRequest).getBytes(); + LOGGER.info("Calling tenant service to delete tenant {}", tenant.get("id")); try { - LOGGER.info("Invoking delete tenant API"); - InvokeResponse response = lambda.invoke(request -> request - .functionName("sb-" + this.envName + "-private-api-client") - .invocationType(InvocationType.REQUEST_RESPONSE) - .payload(SdkBytes.fromByteArray(payload)) - ); - if (response.sdkHttpResponse().isSuccessful()) { - LOGGER.info("got response back: {}", response); - // wait for tenant to reach deleted - final String DELETED = "deleted"; - LocalDateTime timeout = LocalDateTime.now().plus(60, ChronoUnit.MINUTES); - String tenantStatus = (String) getTenant(tenantId).get("onboardingStatus"); - boolean deleted = tenantStatus.equalsIgnoreCase(DELETED); - while (!deleted) { - if (LocalDateTime.now().compareTo(timeout) > 0) { - // we've timed out retrying - outputMessage("Timed out waiting for tenant " + tenantId + " to reach deleted state. " - + "Please check CloudFormation in your AWS Console for more details."); - // if a tenant delete fails, trying to delete the rest of the stack is guaranteed to fail - // due to Tenant resources having cross-dependencies with other resources. stop here to let - // the user figure out what went wrong - throw new RuntimeException("Delete failed."); - } - outputMessage("Waiting 1 minute for tenant " + tenantId - + " to reach deleted from " + tenantStatus); - Thread.sleep(60 * 1000L); // 1 minute - tenantStatus = (String) getTenant(tenantId).get("onboardingStatus"); - deleted = tenantStatus.equalsIgnoreCase(DELETED); + String tenantId = (String) tenant.get("id"); + api().authorizedRequest("DELETE", "tenants/" + tenant.get("id")); + // wait for tenant to reach deleted + final String DELETED = "deleted"; + LocalDateTime timeout = LocalDateTime.now().plus(60, ChronoUnit.MINUTES); + String tenantStatus = (String) getTenant(tenantId).get("onboardingStatus"); + boolean deleted = tenantStatus.equalsIgnoreCase(DELETED); + while (!deleted) { + if (LocalDateTime.now().compareTo(timeout) > 0) { + // we've timed out retrying + outputMessage("Timed out waiting for tenant " + tenantId + " to reach deleted state. " + + "Please check CloudFormation in your AWS Console for more details."); + // if a tenant delete fails, trying to delete the rest of the stack is guaranteed to fail + // due to Tenant resources having cross-dependencies with other resources. stop here to let + // the user figure out what went wrong + throw new RuntimeException("Delete failed."); } - } else { - LOGGER.warn("Private API client Lambda returned HTTP " + response.sdkHttpResponse().statusCode()); - throw new RuntimeException(response.sdkHttpResponse().statusText().get()); + outputMessage("Waiting 1 minute for tenant " + tenantId + + " to reach deleted from " + tenantStatus); + Thread.sleep(60 * 1000L); // 1 minute + tenantStatus = (String) getTenant(tenantId).get("onboardingStatus"); + deleted = tenantStatus.equalsIgnoreCase(DELETED); } - } catch (SdkServiceException lambdaError) { - LOGGER.error("lambda:Invoke error", lambdaError); - LOGGER.error(getFullStackTrace(lambdaError)); - throw lambdaError; - } catch (InterruptedException ie) { - LOGGER.error("Exception in waiting"); - LOGGER.error(getFullStackTrace(ie)); - throw new RuntimeException(ie); + } catch (Exception e) { + LOGGER.error(e.getMessage()); + } + } + + protected List getEcrRepositories() { + List repos = new ArrayList<>(); + Map appConfig = getAppConfig(); + Map services = (Map) appConfig.get("services"); + for (String serviceName : services.keySet()) { + Map service = (Map) services.get(serviceName); + Map compute = (Map) service.get("compute"); + repos.add((String) compute.get("containerRepo")); } + return repos; } protected void deleteEcrImages(String ecrRepo) { @@ -1171,7 +882,7 @@ protected void deleteEcrImages(String ecrRepo) { token = response.nextToken(); } catch (SdkServiceException ecrError) { LOGGER.error("ecr:ListImages error", ecrError); - LOGGER.error(getFullStackTrace(ecrError)); + LOGGER.error(Utils.getFullStackTrace(ecrError)); throw ecrError; } } while (token != null); @@ -1189,142 +900,14 @@ protected void deleteEcrImages(String ecrRepo) { } } catch (SdkServiceException ecrError) { LOGGER.error("ecr:batchDeleteImage error", ecrError); - LOGGER.error(getFullStackTrace(ecrError)); + LOGGER.error(Utils.getFullStackTrace(ecrError)); throw ecrError; } } } - protected Map getMetricStackOutputs(String stackName) { - // Get the Redshift outputs from the metrics CloudFormation stack - Map outputs = null; - try { - DescribeStacksResponse stacksResponse = cfn.describeStacks(DescribeStacksRequest.builder().stackName(stackName).build()); - outputs = stacksResponse.stacks().get(0).outputs().stream().collect(Collectors.toMap(Output::outputKey, Output::outputValue)); - for (String requiredOutput : Arrays.asList("RedshiftDatabaseName", "RedshiftEndpointAddress", "RedshiftCluster", "RedshiftEndpointPort", "MetricsBucket")) { - if (outputs.get(requiredOutput) == null) { - outputMessage("Error, CloudFormation stack: " + stackName + " missing required output: " + requiredOutput); - outputMessage(("Aborting the installation due to error")); - System.exit(2); - } - } - } catch (SdkServiceException cloudFormationError) { - LOGGER.error("cloudformation:DescribeStack error", cloudFormationError); - LOGGER.error(getFullStackTrace(cloudFormationError)); - outputMessage("getMetricStackOutputs: Unable to load Metrics and Analytics CloudFormation stack: " + stackName); - System.exit(2); - } - return outputs; - } - - protected void setupQuickSight(String stackName, Map outputs, String dbPassword) { - /* TODO Note that this entire QuickSight setup is not owned by CloudFormation like most everything - * else and therefore won't be cleaned up properly when SaaS Boost is deleted/uninstalled. - */ - LOGGER.info("User for QuickSight: " + this.quickSightUsername); - LOGGER.info("Create data source in QuickSight for metrics Redshift table in Region: " + AWS_REGION.id()); - final CreateDataSourceResponse createDataSourceResponse = quickSight.createDataSource(CreateDataSourceRequest.builder() - .dataSourceId("sb-" + this.envName + "-metrics-source") - .name("sb-" + this.envName + "-metrics-source") - .awsAccountId(accountId) - .type(DataSourceType.REDSHIFT) - .dataSourceParameters(DataSourceParameters.builder() - .redshiftParameters(RedshiftParameters.builder() - .database(outputs.get("RedshiftDatabaseName")) - .host(outputs.get("RedshiftEndpointAddress")) - .clusterId(outputs.get("RedshiftCluster")) - .port(Integer.valueOf(outputs.get("RedshiftEndpointPort"))) - .build() - ) - .build() - ) - .credentials(DataSourceCredentials.builder() - .credentialPair(CredentialPair.builder() - .username("metricsadmin") - .password(dbPassword) - .build() - ) - .build() - ) - .permissions(ResourcePermission.builder() - .principal(this.quickSightUserArn) - .actions("quicksight:DescribeDataSource","quicksight:DescribeDataSourcePermissions", - "quicksight:PassDataSource","quicksight:UpdateDataSource","quicksight:DeleteDataSource", - "quicksight:UpdateDataSourcePermissions") - .build() - ) - .sslProperties(SslProperties.builder() - .disableSsl(false) - .build() - ) - .tags(Tag.builder() - .key("Name") - .value(stackName) - .build() - ) - .build() - ); - - // Define the physical table for QuickSight - List inputColumns = new ArrayList<>(); - Stream.of("type", "workload", "context", "tenant_id", "tenant_name", "tenant_tier", "metric_name", "metric_unit", "meta_data") - .map(column -> InputColumn.builder() - .name(column) - .type(InputColumnDataType.STRING) - .build() - ) - .forEachOrdered(inputColumns::add); - inputColumns.add(InputColumn.builder() - .name("metric_value") - .type(InputColumnDataType.INTEGER) - .build() - ); - inputColumns.add(InputColumn.builder() - .name("timerecorded") - .type(InputColumnDataType.DATETIME) - .build() - ); - - PhysicalTable physicalTable = PhysicalTable.builder() - .relationalTable(RelationalTable.builder() - .dataSourceArn(createDataSourceResponse.arn()) - .schema("public") - .name("sb_metrics") - .inputColumns(inputColumns) - .build() - ) - .build(); - - Map physicalTableMap = new HashMap<>(); - physicalTableMap.put("string", physicalTable); - - LOGGER.info("Create dataset for sb_metrics table in Quicksight in Region " + AWS_REGION.id()); - quickSight.createDataSet(CreateDataSetRequest.builder() - .awsAccountId(accountId) - .dataSetId("sb-" + this.envName + "-metrics") - .name("sb-" + this.envName + "-metrics") - .physicalTableMap(physicalTableMap) - .importMode(DataSetImportMode.DIRECT_QUERY) - .permissions(ResourcePermission.builder() - .principal(this.quickSightUserArn) - .actions("quicksight:DescribeDataSet","quicksight:DescribeDataSetPermissions", - "quicksight:PassDataSet","quicksight:DescribeIngestion","quicksight:ListIngestions", - "quicksight:UpdateDataSet","quicksight:DeleteDataSet","quicksight:CreateIngestion", - "quicksight:CancelIngestion","quicksight:UpdateDataSetPermissions") - .build() - ) - .tags(Tag.builder() - .key("Name") - .value(stackName) - .build() - ) - .build() - ); - } - - /* - Create Service Roles necessary for Tenant Stack Deployment - */ + // TODO Technically these may only be necessary now if we're installing Keycloak + // Create Service Roles necessary for Tenant Stack Deployment protected void setupAwsServiceRoles() { /* aws iam get-role --role-name "AWSServiceRoleForElasticLoadBalancing" || aws iam create-service-linked-role --aws-service-name "elasticloadbalancing.amazonaws.com" @@ -1355,7 +938,7 @@ protected void setupAwsServiceRoles() { iam.createServiceLinkedRole(request -> request.awsServiceName(serviceRole)); } catch (SdkServiceException iamError) { LOGGER.error("iam:CreateServiceLinkedRole error", iamError); - LOGGER.error(getFullStackTrace(iamError)); + LOGGER.error(Utils.getFullStackTrace(iamError)); throw iamError; } } @@ -1377,7 +960,7 @@ protected void copyResourcesToS3() { } } catch (IOException ioe) { LOGGER.error("Error listing resources directory", ioe); - LOGGER.error(getFullStackTrace(ioe)); + LOGGER.error(Utils.getFullStackTrace(ioe)); throw new RuntimeException(ioe); } try (Stream stream = Files.walk(resourcesDir.resolve("keycloak"))) { @@ -1389,7 +972,7 @@ protected void copyResourcesToS3() { } } catch (IOException ioe) { LOGGER.error("Error walking keycloak directory", ioe); - LOGGER.error(getFullStackTrace(ioe)); + LOGGER.error(Utils.getFullStackTrace(ioe)); // TODO while this is an invalid state, maybe we only want to fail out if // KEYCLOAK actually needs to be installed for this environment throw new RuntimeException(ioe); @@ -1404,7 +987,7 @@ protected static void printResults(Process process) { } } catch (IOException ioe) { LOGGER.error("Error reading from runtime exec process", ioe); - LOGGER.error(getFullStackTrace(ioe)); + LOGGER.error(Utils.getFullStackTrace(ioe)); throw new RuntimeException(ioe); } } @@ -1451,7 +1034,6 @@ protected void loadExistingSaaSBoostEnvironment() { this.lambdaSourceFolder = environment.getLambdasFolderName(); this.stackName = environment.getBaseCloudFormationStackName(); this.baseStackDetails = environment.getBaseCloudFormationStackInfo(); - this.useAnalyticsModule = environment.isMetricsAnalyticsDeployed(); } protected String getExistingSaaSBoostEnvironment() { @@ -1467,7 +1049,8 @@ protected String getExistingSaaSBoostEnvironment() { } } try { - ssm.getParameter(GetParameterRequest.builder().name("/saas-boost/" + environment + "/SAAS_BOOST_ENVIRONMENT").build()); + String envParamsExist = "/saas-boost/" + environment + "/STACK_NAME"; + ssm.getParameter(request -> request.name(envParamsExist)); } catch (ParameterNotFoundException ssmError) { outputMessage("Cannot find existing SaaS Boost environment " + environment + " in this AWS account and region."); @@ -1476,10 +1059,10 @@ protected String getExistingSaaSBoostEnvironment() { return environment; } - protected static boolean validateEmail(String emailAddress) { + protected static boolean validateEmail(String email) { boolean valid = false; - if (emailAddress != null) { - valid = emailAddress.matches("^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"); + if (email != null) { + valid = email.matches("^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"); } return valid; } @@ -1506,6 +1089,14 @@ protected static boolean validateDomainName(String domain) { return valid; } + protected static boolean validateAwsAccountId(String accountId) { + boolean valid = false; + if (accountId != null) { + valid = accountId.replace("-", "").matches("^[0-9]{12}$"); + } + return valid; + } + protected static List existingHostedZones(Route53Client route53, String domain) { List hostedZones = new ArrayList<>(); String nextDnsName = null; @@ -1596,8 +1187,11 @@ protected void processLambdas() { // Now add the separate layers directories to the list so we can upload the lambda // package to S3 below. Build utils before anything else. sourceDirectories.add(workingDir.resolve(Path.of("layers", "utils"))); + // TODO make this a list of everything in the layers folder that's not utils sourceDirectories.add(workingDir.resolve(Path.of("layers", "apigw-helper"))); sourceDirectories.add(workingDir.resolve(Path.of("layers", "cloudformation-utils"))); + sourceDirectories.add(workingDir.resolve(Path.of("layers", "keycloak-helper"))); + sourceDirectories.add(workingDir.resolve(Path.of("layers", "saas-boost-api-client-helper"))); DirectoryStream functions = Files.newDirectoryStream(workingDir.resolve(Path.of("functions")), Files::isDirectory); functions.forEach(sourceDirectories::add); @@ -1608,14 +1202,19 @@ protected void processLambdas() { DirectoryStream services = Files.newDirectoryStream(workingDir.resolve(Path.of("services")), Files::isDirectory); services.forEach(sourceDirectories::add); - sourceDirectories.add(workingDir.resolve(Path.of("metering-billing", "lambdas"))); - final PathMatcher filter = FileSystems.getDefault().getPathMatcher("glob:**.zip"); outputMessage("Uploading " + sourceDirectories.size() + " Lambda functions to S3"); for (ListIterator iter = sourceDirectories.listIterator(); iter.hasNext();) { int progress = iter.nextIndex(); Path sourceDirectory = iter.next(); - if (Files.exists(sourceDirectory.resolve("pom.xml"))) { + if (sourceDirectory.endsWith("saas-boost-api-client-helper")) { + executeCommand("sh build.sh", null, sourceDirectory.toFile()); + Path zipFile = sourceDirectory.resolve("build/SaaSBoostApiClientHelper-lambda.zip"); + LOGGER.info("Uploading Lambda source package to S3 " + zipFile.toString() + " -> " + this.lambdaSourceFolder + "/" + zipFile.getFileName().toString()); + System.out.printf("%2d. %s%n", (progress + 1), zipFile.getFileName().toString()); + saasBoostArtifactsBucket.putFile(s3, zipFile, + Path.of(this.lambdaSourceFolder, zipFile.getFileName().toString())); + } else if (Files.exists(sourceDirectory.resolve("pom.xml"))) { executeCommand("mvn", null, sourceDirectory.toFile()); final Path targetDir = sourceDirectory.resolve("target"); try (Stream stream = Files.list(targetDir)) { @@ -1630,12 +1229,12 @@ protected void processLambdas() { } } } else { - LOGGER.warn("No POM file found in {}", sourceDirectory.toString()); + LOGGER.warn("No POM file found in {}", sourceDirectory); } } } catch (IOException ioe) { LOGGER.error("Error processing Lambda source folders", ioe); - LOGGER.error(getFullStackTrace(ioe)); + LOGGER.error(Utils.getFullStackTrace(ioe)); throw new RuntimeException(ioe); } } @@ -1643,7 +1242,7 @@ protected void processLambdas() { protected void createSaaSBoostStack(final String stackName, String adminEmail, String systemIdentityProvider, String identityProviderCustomDomain, String identityProviderHostedZone, String identityProviderCertificate, String adminWebAppCustomDomain, - String adminWebAppHostedZone, String adminWebAppCertificate) { + String adminWebAppHostedZone, String adminWebAppCertificate, String appPlaneAccount) { // Note - most params the default is used from the CloudFormation stack List templateParameters = new ArrayList<>(); templateParameters.add(Parameter.builder().parameterKey("Environment").parameterValue(envName).build()); @@ -1660,7 +1259,8 @@ protected void createSaaSBoostStack(final String stackName, String adminEmail, S //templateParameters.add(Parameter.builder().parameterKey("ApiDomain").parameterValue(Objects.toString(apiCustomDomaine, "")).build()); //templateParameters.add(Parameter.builder().parameterKey("ApiHostedZone").parameterValue(Objects.toString(apiHostedZone, "")).build()); //templateParameters.add(Parameter.builder().parameterKey("ApiCertificate").parameterValue(Objects.toString(apiCertificate, "")).build()); - templateParameters.add(Parameter.builder().parameterKey("CreateMacroResources").parameterValue(Boolean.toString(!doesCfnMacroResourceExist())).build()); + //templateParameters.add(Parameter.builder().parameterKey("CreateMacroResources").parameterValue(Boolean.toString(!doesCfnMacroResourceExist())).build()); + templateParameters.add(Parameter.builder().parameterKey("AppPlaneAccountId").parameterValue(appPlaneAccount).build()); LOGGER.info("createSaaSBoostStack::create stack " + stackName); String stackId = null; @@ -1668,8 +1268,6 @@ protected void createSaaSBoostStack(final String stackName, String adminEmail, S CreateStackResponse cfnResponse = cfn.createStack(CreateStackRequest.builder() .stackName(stackName) .disableRollback(true) - //.onFailure("DO_NOTHING") // TODO bug on roll back? - //.timeoutInMinutes(90) .capabilitiesWithStrings("CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND") .templateURL(saasBoostArtifactsBucket.getBucketUrl() + "saas-boost.yaml") .parameters(templateParameters) @@ -1701,67 +1299,7 @@ protected void createSaaSBoostStack(final String stackName, String adminEmail, S } while (!stackCompleted); } catch (SdkServiceException cfnError) { LOGGER.error("cloudformation error", cfnError); - LOGGER.error(getFullStackTrace(cfnError)); - throw cfnError; - } - } - - protected void createMetricsStack(final String stackName, final String dbPasswordSsmParameter, final String databaseName) { - LOGGER.info("Creating CloudFormation stack {} with database name {}", stackName, databaseName); - List templateParameters = new ArrayList<>(); - templateParameters.add(Parameter.builder().parameterKey("Environment").parameterValue(this.envName).build()); - templateParameters.add(Parameter.builder().parameterKey("LambdaSourceFolder").parameterValue(this.lambdaSourceFolder).build()); - templateParameters.add(Parameter.builder().parameterKey("MetricUserPasswordSSMParameter").parameterValue(dbPasswordSsmParameter).build()); - templateParameters.add(Parameter.builder().parameterKey("SaaSBoostBucket").parameterValue(saasBoostArtifactsBucket.getBucketName()).build()); - templateParameters.add(Parameter.builder().parameterKey("LoggingBucket").parameterValue(baseStackDetails.get("LoggingBucket")).build()); - templateParameters.add(Parameter.builder().parameterKey("DatabaseName").parameterValue(databaseName).build()); - templateParameters.add(Parameter.builder().parameterKey("PublicSubnet1").parameterValue(baseStackDetails.get("PublicSubnet1")).build()); - templateParameters.add(Parameter.builder().parameterKey("PublicSubnet2").parameterValue(baseStackDetails.get("PublicSubnet2")).build()); - templateParameters.add(Parameter.builder().parameterKey("PrivateSubnet1").parameterValue(baseStackDetails.get("PrivateSubnet1")).build()); - templateParameters.add(Parameter.builder().parameterKey("PrivateSubnet2").parameterValue(baseStackDetails.get("PrivateSubnet2")).build()); - templateParameters.add(Parameter.builder().parameterKey("VPC").parameterValue(baseStackDetails.get("EgressVpc")).build()); - - // Now run the stack to provision the infrastructure for Metrics and Analytics - LOGGER.info("createMetricsStack::stack " + stackName); - - String stackId; - try { - CreateStackResponse cfnResponse = cfn.createStack(CreateStackRequest.builder() - .stackName(stackName) - //.onFailure("DO_NOTHING") // TODO bug on roll back? - //.timeoutInMinutes(90) - .capabilitiesWithStrings("CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND") - .templateURL(saasBoostArtifactsBucket.getBucketUrl() + "saas-boost-metrics-analytics.yaml") - .parameters(templateParameters) - .build() - ); - stackId = cfnResponse.stackId(); - LOGGER.info("createMetricsStack::stack id " + stackId); - - boolean stackCompleted = false; - long sleepTime = 5L; - do { - DescribeStacksResponse response = cfn.describeStacks(request -> request.stackName(stackName)); - Stack stack = response.stacks().get(0); - if ("CREATE_COMPLETE".equalsIgnoreCase(stack.stackStatusAsString())) { - outputMessage("CloudFormation stack: " + stackName + " completed successfully."); - stackCompleted = true; - } else if ("CREATE_FAILED".equalsIgnoreCase(stack.stackStatusAsString())) { - outputMessage("CloudFormation stack: " + stackName + " failed."); - throw new RuntimeException("Error with CloudFormation stack " + stackName + ". Check the events in the AWS CloudFormation Console"); - } else { - outputMessage("Awaiting CloudFormation Stack " + stackName + " to complete. Sleep " + sleepTime + " minute(s)..."); - try { - Thread.sleep(sleepTime * 60 * 1000); - } catch (Exception e) { - LOGGER.error("Error with sleep"); - } - sleepTime = 1L; //set to 1 minute after kick off of 5 minute - } - } while (!stackCompleted); - } catch (SdkServiceException cfnError) { - LOGGER.error("cloudformation error", cfnError); - LOGGER.error(getFullStackTrace(cfnError)); + LOGGER.error(Utils.getFullStackTrace(cfnError)); throw cfnError; } } @@ -1778,7 +1316,7 @@ protected void deleteCloudFormationStack(final String stackName) { } } catch (SdkServiceException cfnError) { LOGGER.error("cloudformation:DescribeStacks error", cfnError); - LOGGER.error(getFullStackTrace(cfnError)); + LOGGER.error(Utils.getFullStackTrace(cfnError)); throw cfnError; } try { @@ -1815,14 +1353,14 @@ protected void deleteCloudFormationStack(final String stackName) { } catch (SdkServiceException cfnError) { if (!cfnError.getMessage().contains("does not exist")) { LOGGER.error("cloudformation:DescribeStacks error", cfnError); - LOGGER.error(getFullStackTrace(cfnError)); + LOGGER.error(Utils.getFullStackTrace(cfnError)); throw cfnError; } } } } catch (SdkServiceException cfnError) { LOGGER.error("cloudformation:DeleteStack error", cfnError); - LOGGER.error(getFullStackTrace(cfnError)); + LOGGER.error(Utils.getFullStackTrace(cfnError)); throw cfnError; } } @@ -1836,7 +1374,7 @@ protected boolean checkCloudFormationStack(final String stackName) { } catch (SdkServiceException cfnError) { if (!cfnError.getMessage().contains("does not exist")) { LOGGER.error("cloudformation:DescribeStacks error", cfnError); - LOGGER.error(getFullStackTrace(cfnError)); + LOGGER.error(Utils.getFullStackTrace(cfnError)); throw cfnError; } } @@ -1851,7 +1389,7 @@ public static void copyAdminWebAppSourceToS3(Path workingDir, String artifactsBu } // Sync files to the web bucket - outputMessage("Synchronizing AWS SaaS Boost web application files to s3 web bucket"); + outputMessage("Synchronizing AWS SaaS Boost web application files to s3"); List filesToUpload; try (Stream stream = Files.walk(webDir)) { filesToUpload = stream @@ -1892,7 +1430,7 @@ public static void copyAdminWebAppSourceToS3(Path workingDir, String artifactsBu ); } catch (SdkServiceException s3Error) { LOGGER.error("s3:PutObject error", s3Error); - LOGGER.error(getFullStackTrace(s3Error)); + LOGGER.error(Utils.getFullStackTrace(s3Error)); throw s3Error; } } catch (IOException ioe) { @@ -1901,7 +1439,77 @@ public static void copyAdminWebAppSourceToS3(Path workingDir, String artifactsBu } } catch (IOException ioe) { LOGGER.error("Error walking client/web directory", ioe); - LOGGER.error(getFullStackTrace(ioe)); + LOGGER.error(Utils.getFullStackTrace(ioe)); + throw new RuntimeException(ioe); + } + } + + public static void copyApiDocsSourceToS3(Path workingDir, String artifactsBucket, S3Client s3) { + Path webDir = workingDir.resolve(Path.of("resources", "api-docs")); + if (!Files.isDirectory(webDir)) { + outputMessage("Error, can't find resources/api-docs directory at " + webDir.toAbsolutePath().toString()); + System.exit(2); + } + + // Sync files to the web bucket + outputMessage("Synchronizing AWS SaaS Boost API Docs (Swagger) files to s3"); + List filesToUpload; + try (Stream stream = Files.walk(webDir)) { + filesToUpload = stream + .filter(file -> + Files.isRegularFile(file) && ( + file.startsWith("resources/api-docs/app.js") + || file.startsWith("resources/api-docs/package.json") + || file.startsWith("resources/api-docs/package-lock.json") + || file.startsWith("resources/api-docs/buildspec_no_post_build.yaml") + || file.startsWith("resources/api-docs/update.sh")) + ) + .collect(Collectors.toList()); + outputMessage("Uploading " + filesToUpload.size() + " files to S3"); + + // Create a ZIP archive of the source files so we only call s3 put object once + // and so we can trigger the CodeBuild project off of that single s3 event + // (instead of triggering CodeBuild 180+ times -- once for each file put to s3). + try { + ByteArrayOutputStream src = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(src); + for (Path fileToUpload : filesToUpload) { + // java.nio.file.Path will use OS dependent file separators + String fileName = fileToUpload.toFile().toString().replace('\\', '/'); + ZipEntry entry = new ZipEntry(fileName); + zip.putNextEntry(entry); + zip.write(Files.readAllBytes(fileToUpload)); // all of our files are very small + zip.closeEntry(); + } + zip.close(); + try { + // Now copy the Swagger source files up to the artifacts bucket + // This will trigger a CodeBuild project to build and deploy the app + // if done after the initial install of SaaS Boost + s3.putObject(PutObjectRequest.builder() + .bucket(artifactsBucket) + .key("api-docs/src.zip") + .build(), RequestBody.fromBytes(src.toByteArray()) + ); + // Copy the swagger definition file up to the artifacts bucket separately + // because we use it as an event source to trigger future builds of the + // api docs site + s3.putObject(PutObjectRequest.builder() + .bucket(artifactsBucket) + .key("api-docs/swagger.json") + .build(), workingDir.resolve(Path.of("resources", "api-docs", "swagger.json"))); + } catch (SdkServiceException s3Error) { + LOGGER.error("s3:PutObject error", s3Error); + LOGGER.error(Utils.getFullStackTrace(s3Error)); + throw s3Error; + } + } catch (IOException ioe) { + LOGGER.error("ZIP archive generation failed"); + throw new RuntimeException(Utils.getFullStackTrace(ioe)); + } + } catch (IOException ioe) { + LOGGER.error("Error walking resources/api-docs directory", ioe); + LOGGER.error(Utils.getFullStackTrace(ioe)); throw new RuntimeException(ioe); } } @@ -1923,7 +1531,7 @@ public static void executeCommand(String command, String[] environment, File dir printResults(process); } catch (Exception e) { LOGGER.error("Error running command: " + command); - LOGGER.error(getFullStackTrace(e)); + LOGGER.error(Utils.getFullStackTrace(e)); throw new RuntimeException("Error running command: " + command); } @@ -1944,38 +1552,6 @@ public static void executeCommand(String command, String[] environment, File dir process.destroy(); } - protected LinkedHashMap getQuickSightUsers() { - LOGGER.info("Load Quicksight users"); - LinkedHashMap users = new LinkedHashMap<>(); - try { - String nextToken = null; - do { - ListUsersResponse response = quickSight.listUsers(ListUsersRequest.builder() - .awsAccountId(accountId) - .namespace("default") - .nextToken(nextToken) - .build() - ); - if (response.hasUserList()) { - for (User quickSightUser : response.userList()) { - users.put(quickSightUser.userName(), quickSightUser); - } - } - nextToken = response.nextToken(); - } while (nextToken != null); - } catch (SdkServiceException quickSightError) { - LOGGER.error("quickSight:ListUsers error {}", quickSightError.getMessage()); - LOGGER.error(getFullStackTrace(quickSightError)); - throw quickSightError; - } - LOGGER.info("Completed load of QuickSight users"); - return users; - } - - protected String analyticsStackName() { - return this.stackName + "-analytics"; - } - protected static void cleanUpS3(S3Client s3, String bucket, String prefix) { // The list of objects in the bucket to delete List toDelete = new ArrayList<>(); @@ -1983,7 +1559,8 @@ protected static void cleanUpS3(S3Client s3, String bucket, String prefix) { prefix = prefix + "/"; } GetBucketVersioningResponse versioningResponse = s3.getBucketVersioning(request -> request.bucket(bucket)); - if (BucketVersioningStatus.ENABLED == versioningResponse.status() || BucketVersioningStatus.SUSPENDED == versioningResponse.status()) { + if (BucketVersioningStatus.ENABLED == versioningResponse.status() + || BucketVersioningStatus.SUSPENDED == versioningResponse.status()) { LOGGER.info("Bucket " + bucket + " is versioned (" + versioningResponse.status() + ")"); ListObjectVersionsResponse listObjectResponse; String keyMarker = null; @@ -2021,6 +1598,15 @@ protected static void cleanUpS3(S3Client s3, String bucket, String prefix) { .build() ) .forEachOrdered(toDelete::add); + listObjectResponse.deleteMarkers() + .stream() + .map(marker -> + ObjectIdentifier.builder() + .key(marker.key()) + .versionId(marker.versionId()) + .build() + ) + .forEachOrdered(toDelete::add); } while (listObjectResponse.isTruncated()); } else { LOGGER.info("Bucket " + bucket + " is not versioned (" + versioningResponse.status() + ")"); @@ -2078,50 +1664,6 @@ protected static void cleanUpS3(S3Client s3, String bucket, String prefix) { } } - private boolean doesCfnMacroResourceExist() { - // this assumes that the macro resource exists in CloudFormation if and only if all requisite resources also - // exist, i.e. the macro Lambda function, execution role, and log group. this should always be true, since the - // macro resource will never be deleted unless each of the others are deleted thanks to CloudFormation - // dependency analysis - List stackNamesToCheck = new ArrayList<>(); - String paginationToken = null; - do { - ListStacksResponse listStacksResponse = cfn.listStacks( - ListStacksRequest.builder().nextToken(paginationToken).build()); - stackNamesToCheck.addAll(listStacksResponse.stackSummaries().stream() - .filter(summary -> summary.stackStatus() != StackStatus.DELETE_COMPLETE - && summary.stackStatus() != StackStatus.DELETE_IN_PROGRESS) - .map(StackSummary::stackName) - .collect(Collectors.toList())); - paginationToken = listStacksResponse.nextToken(); - } while (paginationToken != null); - // for each stack, look for Macro Resource (either by listing all or getResource by logical id) - for (String stackName : stackNamesToCheck) { - try { - StackResourceDetail stackResourceDetail = cfn.describeStackResource(request -> request - .stackName(stackName) - .logicalResourceId("ApplicationServicesMacro")).stackResourceDetail(); - if (stackResourceDetail.resourceStatus() != ResourceStatus.DELETE_COMPLETE) { - LOGGER.info("Found the ApplicationServicesMacro resource in {}", stackName); - return true; - } - } catch (CloudFormationException cfne) { - if (cfne.getMessage().contains("Stack '" + stackName + "' does not exist")) { - // if stacks are being deleted - } - } - } - LOGGER.info("Could not find any ApplicationServicesMacro resource"); - return false; - } - - public static String getFullStackTrace(Exception e) { - final StringWriter sw = new StringWriter(); - final PrintWriter pw = new PrintWriter(sw, true); - e.printStackTrace(pw); - return sw.getBuffer().toString(); - } - public static boolean isWindows() { return (OS.contains("win")); } @@ -2130,32 +1672,65 @@ public static boolean isMac() { return (OS.contains("mac")); } - /** - * Generate a random password that matches the password policy of the Cognito user pool - * @return a random password that matches the password policy of the Cognito user pool - */ - public static String generatePassword(int passwordLength) { - if (passwordLength < 8) { - throw new IllegalArgumentException("Invalid password length. Minimum of 8 characters is required."); - } - - // Split the classes of characters into separate buckets so we can be sure to use - // the correct amount of each type - final char[][] requiredCharacterBuckets = { - {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}, - {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}, - {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} - }; - - Random random = new Random(); - StringBuilder password = new StringBuilder(passwordLength); + protected String quickCreateLink() { + StringBuilder quickCreateLink = new StringBuilder(); + quickCreateLink.append("https://"); + quickCreateLink.append(AWS_REGION); + quickCreateLink.append(".console.aws.amazon.com/cloudformation/home?region="); + quickCreateLink.append(AWS_REGION); + quickCreateLink.append("#/stacks/create/review?"); + quickCreateLink.append("templateURL="); + quickCreateLink.append(saasBoostArtifactsBucket.getBucketUrl()); + quickCreateLink.append("saas-boost-app-integration.yaml"); + quickCreateLink.append("&stackName="); + quickCreateLink.append("sb-"); + quickCreateLink.append(envName); + quickCreateLink.append("-integration"); + quickCreateLink.append("¶m_Environment="); + quickCreateLink.append(envName); + Map params = getQuickCreateLinkParameters(); + quickCreateLink.append("¶m_EventBusArn="); + quickCreateLink.append(params.get("EVENT_BUS")); + quickCreateLink.append("¶m_ApiAppClientSecretArn="); + quickCreateLink.append(params.get("API_APP_CLIENT_SECRET")); + quickCreateLink.append("¶m_EncryptionKeyArn="); + quickCreateLink.append(params.get("API_APP_CLIENT_KEY")); + quickCreateLink.append("¶m_UtilsLayerArn="); + quickCreateLink.append(params.get("UTILS_LAYER")); + quickCreateLink.append("¶m_CloudFormationUtilsLayerArn="); + quickCreateLink.append(params.get("CFN_UTILS_LAYER")); + quickCreateLink.append("¶m_ApiHelperLayerArn="); + quickCreateLink.append(params.get("API_CLIENT_HELPER_LAYER")); + return quickCreateLink.toString(); + } - // Randomly select one character from each of the required character types - for (char[] requiredCharacterBucket : requiredCharacterBuckets) { - password.append(requiredCharacterBucket[random.nextInt(requiredCharacterBucket.length)]); + protected Map getQuickCreateLinkParameters() { + Map params = new HashMap<>(); + try { + GetParametersResponse response = ssm.getParameters(request -> request + .names(List.of( + "/saas-boost/" + envName + "/EVENT_BUS", + "/saas-boost/" + envName + "/API_APP_CLIENT_SECRET", + "/saas-boost/" + envName + "/API_APP_CLIENT_KEY", + "/saas-boost/" + envName + "/UTILS_LAYER", + "/saas-boost/" + envName + "/CFN_UTILS_LAYER", + "/saas-boost/" + envName + "/API_CLIENT_HELPER_LAYER" + )) + ); + response.parameters() + .stream() + .forEach(parameter -> params.put( + parameter.name().substring(parameter.name().lastIndexOf("/") + 1), parameter.value())); + // ParameterStore only has the name of the event bus, but we need the whole ARN + params.put("EVENT_BUS", + "arn:" + AWS_REGION.metadata().partition().id() + ":events:" + AWS_REGION.id() + + ":" + accountId + ":event-bus/" + params.get("EVENT_BUS")); + } catch (SdkServiceException ssmError) { + LOGGER.error("ssm getParameters failed", ssmError); + LOGGER.error(Utils.getFullStackTrace(ssmError)); + throw ssmError; } - - // build the remaining password using Utils.randomString - return password.append(Utils.randomString(passwordLength - requiredCharacterBuckets.length)).toString(); + return params; } + } diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactory.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactory.java deleted file mode 100644 index 98fc477b..00000000 --- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactory.java +++ /dev/null @@ -1,247 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost.clients; - -import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; -import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder; -import software.amazon.awssdk.awscore.retry.AwsRetryPolicy; -import software.amazon.awssdk.core.SdkClient; -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; -import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting; -import software.amazon.awssdk.core.retry.RetryPolicy; -import software.amazon.awssdk.core.retry.backoff.BackoffStrategy; -import software.amazon.awssdk.core.retry.conditions.RetryCondition; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.acm.AcmClient; -import software.amazon.awssdk.services.acm.AcmClientBuilder; -import software.amazon.awssdk.services.apigateway.ApiGatewayClient; -import software.amazon.awssdk.services.apigateway.ApiGatewayClientBuilder; -import software.amazon.awssdk.services.apigateway.model.CreateDeploymentRequest; -import software.amazon.awssdk.services.cloudformation.CloudFormationClient; -import software.amazon.awssdk.services.cloudformation.CloudFormationClientBuilder; -import software.amazon.awssdk.services.ecr.EcrClient; -import software.amazon.awssdk.services.ecr.EcrClientBuilder; -import software.amazon.awssdk.services.iam.IamClient; -import software.amazon.awssdk.services.iam.IamClientBuilder; -import software.amazon.awssdk.services.lambda.LambdaClient; -import software.amazon.awssdk.services.lambda.LambdaClientBuilder; -import software.amazon.awssdk.services.quicksight.QuickSightClient; -import software.amazon.awssdk.services.quicksight.QuickSightClientBuilder; -import software.amazon.awssdk.services.route53.Route53Client; -import software.amazon.awssdk.services.route53.Route53ClientBuilder; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.S3ClientBuilder; -import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; -import software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder; -import software.amazon.awssdk.services.ssm.SsmClient; -import software.amazon.awssdk.services.ssm.SsmClientBuilder; -import software.amazon.awssdk.services.sts.StsClient; -import software.amazon.awssdk.services.sts.StsClientBuilder; - -import java.net.URI; -import java.time.Duration; - -public class AwsClientBuilderFactory { - - private static final AwsCredentialsProvider DEFAULT_CREDENTIALS_PROVIDER = - RefreshingProfileDefaultCredentialsProvider.builder().build(); - - private final Region awsRegion; - private final AwsCredentialsProvider credentialsProvider; - - private ApiGatewayClientBuilder cachedApiGatewayBuilder; - private CloudFormationClientBuilder cachedCloudFormationBuilder; - private EcrClientBuilder cachedEcrBuilder; - private IamClientBuilder cachedIamBuilder; - private LambdaClientBuilder cachedLambdaBuilder; - private QuickSightClientBuilder cachedQuickSightBuilder; - private S3ClientBuilder cachedS3Builder; - private SsmClientBuilder cachedSsmBuilder; - private StsClientBuilder cachedStsBuilder; - private SecretsManagerClientBuilder cachedSecretsManagerClientBuilder; - private Route53ClientBuilder cachedRoute53ClientBuilder; - private AcmClientBuilder cachedAcmClientBuilder; - - AwsClientBuilderFactory() { - // for testing - this.awsRegion = null; - this.credentialsProvider = null; - } - - private AwsClientBuilderFactory(Builder builder) { - // passing no region or a null region to any of the AWS Client Builders - // leads to the default region from the configured profile being used - this.awsRegion = builder.defaultRegion; - this.credentialsProvider = builder.awsCredentialsProvider != null - ? builder.awsCredentialsProvider - : DEFAULT_CREDENTIALS_PROVIDER; - } - - // VisibleForTesting - > B decorateBuilderWithDefaults(B builder) { - return builder - .credentialsProvider(credentialsProvider) - .region(awsRegion); - } - - public ApiGatewayClientBuilder apiGatewayBuilder() { - if (cachedApiGatewayBuilder == null) { - // override throttling policy to wait 5 seconds if we're throttled on CreateDeployment - // https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html - cachedApiGatewayBuilder = decorateBuilderWithDefaults(ApiGatewayClient.builder()) - .overrideConfiguration(config -> config.retryPolicy(AwsRetryPolicy.addRetryConditions( - RetryPolicy.builder().throttlingBackoffStrategy(retryPolicyContext -> { - if (retryPolicyContext.originalRequest() instanceof CreateDeploymentRequest) { - return Duration.ofSeconds(5); - } - return null; - }).build()))); - } - - return cachedApiGatewayBuilder; - } - - public CloudFormationClientBuilder cloudFormationBuilder() { - if (cachedCloudFormationBuilder == null) { - cachedCloudFormationBuilder = decorateBuilderWithDefaults(CloudFormationClient.builder()); - } - return cachedCloudFormationBuilder; - } - - public EcrClientBuilder ecrBuilder() { - if (cachedEcrBuilder == null) { - cachedEcrBuilder = decorateBuilderWithDefaults(EcrClient.builder()); - } - return cachedEcrBuilder; - } - - public IamClientBuilder iamBuilder() { - if (cachedIamBuilder == null) { - Region region = Region.of(System.getenv("AWS_REGION")); - if (Utils.isChinaRegion(region)) { - // China's IAM endpoints point to Beijing region - // See https://docs.amazonaws.cn/en_us/aws/latest/userguide/iam.html - cachedIamBuilder = decorateBuilderWithDefaults(IamClient.builder()).region(Region.AWS_CN_GLOBAL); - } else { - // IAM in the commercial regions use the AWS_GLOBAL - // ref: https://docs.aws.amazon.com/general/latest/gr/iam-service.html - cachedIamBuilder = decorateBuilderWithDefaults(IamClient.builder()).region(Region.AWS_GLOBAL); - } - } - return cachedIamBuilder; - } - - public LambdaClientBuilder lambdaBuilder() { - if (cachedLambdaBuilder == null) { - cachedLambdaBuilder = decorateBuilderWithDefaults(LambdaClient.builder()); - } - return cachedLambdaBuilder; - } - - public QuickSightClientBuilder quickSightBuilder() { - if (cachedQuickSightBuilder == null) { - cachedQuickSightBuilder = decorateBuilderWithDefaults(QuickSightClient.builder()); - } - return cachedQuickSightBuilder; - } - - public S3ClientBuilder s3Builder() { - if (cachedS3Builder == null) { - cachedS3Builder = decorateBuilderWithDefaults(S3Client.builder()); - } - return cachedS3Builder; - } - - public SsmClientBuilder ssmBuilder() { - if (cachedSsmBuilder == null) { - cachedSsmBuilder = decorateBuilderWithDefaults(SsmClient.builder()); - } - return cachedSsmBuilder; - } - - public StsClientBuilder stsBuilder() { - if (cachedStsBuilder == null) { - cachedStsBuilder = decorateBuilderWithDefaults(StsClient.builder()); - } - return cachedStsBuilder; - } - - public SecretsManagerClientBuilder secretsManagerBuilder() { - if (cachedSecretsManagerClientBuilder == null) { - cachedSecretsManagerClientBuilder = decorateBuilderWithDefaults(SecretsManagerClient.builder()); - } - return cachedSecretsManagerClientBuilder; - } - - public Route53ClientBuilder route53Builder() { - if (cachedRoute53ClientBuilder == null) { - // Route53 is a global service and uses a different region setting than the default - Region region; - String endpoint; - Builder factory = builder() - .region(Region.of(System.getenv("AWS_REGION"))) - .credentialsProvider(DEFAULT_CREDENTIALS_PROVIDER); - if (!Utils.isChinaRegion(factory.defaultRegion)) { - region = Region.US_EAST_1; - endpoint = "https://route53.amazonaws.com"; - } else { - region = Region.CN_NORTHWEST_1; - endpoint = "https://route53.amazonaws.com.cn"; - } - cachedRoute53ClientBuilder = Route53Client.builder() - .region(region) - .endpointOverride(URI.create(endpoint)) - .credentialsProvider(factory.awsCredentialsProvider); - } - return cachedRoute53ClientBuilder; - } - - public AcmClientBuilder acmBuilder() { - if (cachedAcmClientBuilder == null) { - cachedAcmClientBuilder = decorateBuilderWithDefaults(AcmClient.builder()); - } - return cachedAcmClientBuilder; - } - - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - private Region defaultRegion; - private AwsCredentialsProvider awsCredentialsProvider; - - private Builder() { - - } - - public Builder region(Region defaultRegion) { - this.defaultRegion = defaultRegion; - return this; - } - - public Builder credentialsProvider(AwsCredentialsProvider awsCredentialsProvider) { - this.awsCredentialsProvider = awsCredentialsProvider; - return this; - } - - public AwsClientBuilderFactory build() { - return new AwsClientBuilderFactory(this); - } - } -} diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/clients/RefreshingProfileDefaultCredentialsProvider.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/clients/RefreshingProfileDefaultCredentialsProvider.java deleted file mode 100644 index 82e835ed..00000000 --- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/clients/RefreshingProfileDefaultCredentialsProvider.java +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost.clients; - -import software.amazon.awssdk.auth.credentials.AwsCredentials; -import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; -import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; -import software.amazon.awssdk.profiles.ProfileFile; - -import java.io.File; -import java.nio.file.Path; - -/** - * This class provides the exact same functionality as the {@link DefaultCredentialsProvider} but without any - * caching to support profile files that refresh from outside the JVM. To be explicit, this - * {@link RefreshingProfileDefaultCredentialsProvider} creates a new {@link DefaultCredentialsProvider} from scratch - * each time {@link RefreshingProfileDefaultCredentialsProvider#resolveCredentials()} is called. - * - * In some cases (such as in Cloud9, see #137) the - * credentials being returned by the credentials provider will expire and will not be able to be refreshed. For example, - * if the credentials being used are coming from the default
    .aws/credentials
    file and are updated during the - * lifetime of this process, the {@link software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider} will return - * expired credentials, leading to SaaS Boost erroring out. - * - * This class addresses this case by creating a new {@link DefaultCredentialsProvider} (note, explicitly using the - * builder, because the .create() function returns a static singleton) each time resolve credentials is installed. - * This ensures that any new credentials added to the configured profile will be picked up for any resolve credentials - * call. - * - * This obviously comes with a performance hit: we're creating a new object each time resolveCredentials is called - * rather than relying on in-memory values. In experimentation this equates to a roughly 100x difference in - * performance: the refreshingCredentialsProvider will average ~0.1-0.2 ms per resolveCredentials call vs the - * Default in-memory's ~0.0001ms runtime (YMMV based on CPU clock speed). We have considered this performance change - * to be acceptable: resolveCredentials should only be called once per SDK call, and so this is equivalent to a - * linear difference in runtime performance of the installer, likely adding significantly less than one second to - * an already very long-running process (on the order of minutes to upload Lambda function artifacts to S3 and wait - * for CloudFormation templates to finish). - * - * @see SaaS Boost Issue #137 - * @see AWS Java SDK v2 Issue #1754 - */ -public class RefreshingProfileDefaultCredentialsProvider implements AwsCredentialsProvider { - private final String profileFilename; - private final DefaultCredentialsProvider.Builder curriedBuilder; - - private RefreshingProfileDefaultCredentialsProvider(RefreshingProfileDefaultCredentialsProvider.Builder builder) { - curriedBuilder = DefaultCredentialsProvider.builder(); - curriedBuilder.reuseLastProviderEnabled(builder.reuseLastProviderEnabled); - curriedBuilder.asyncCredentialUpdateEnabled(builder.asyncCredentialUpdateEnabled); - curriedBuilder.profileName(builder.profileName); - profileFilename = builder.profileFilename; - } - - /** - * @see AwsCredentialsProvider#resolveCredentials() - */ - @Override - public AwsCredentials resolveCredentials() { - if (profileFilename == null) { - return curriedBuilder.build().resolveCredentials(); - } - curriedBuilder.profileFile(ProfileFile.builder() - .type(ProfileFile.Type.CREDENTIALS) - .content(Path.of(new File(profileFilename).toURI())) - .build()); - return curriedBuilder.build().resolveCredentials(); - } - - public static Builder builder() { - return new Builder(); - } - - /** - * @see DefaultCredentialsProvider.Builder - */ - public static class Builder { - private String profileFilename; - private String profileName; - private boolean reuseLastProviderEnabled; - private boolean asyncCredentialUpdateEnabled; - - private Builder() { - - } - - public RefreshingProfileDefaultCredentialsProvider.Builder profileFilename(String profileFilename) { - this.profileFilename = profileFilename; - return this; - } - - public RefreshingProfileDefaultCredentialsProvider.Builder profileName(String profileName) { - this.profileName = profileName; - return this; - } - - public RefreshingProfileDefaultCredentialsProvider.Builder reuseLastProviderEnabled( - Boolean reuseLastProviderEnabled) { - this.reuseLastProviderEnabled = reuseLastProviderEnabled; - return this; - } - - public RefreshingProfileDefaultCredentialsProvider.Builder asyncCredentialUpdateEnabled( - Boolean asyncCredentialUpdateEnabled) { - this.asyncCredentialUpdateEnabled = asyncCredentialUpdateEnabled; - return this; - } - - public RefreshingProfileDefaultCredentialsProvider build() { - return new RefreshingProfileDefaultCredentialsProvider(this); - } - } -} diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/Environment.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/Environment.java index a8c81db1..8391d745 100644 --- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/Environment.java +++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/Environment.java @@ -35,7 +35,6 @@ public final class Environment { private String lambdasFolderName; private String baseCloudFormationStackName; private Map baseCloudFormationStackInfo; - private boolean metricsAnalyticsDeployed; private Environment(Builder b) { this.name = b.name; @@ -44,7 +43,6 @@ private Environment(Builder b) { this.lambdasFolderName = b.lambdasFolderName; this.baseCloudFormationStackName = b.baseCloudFormationStackName; this.baseCloudFormationStackInfo = b.baseCloudFormationStackInfo; - this.metricsAnalyticsDeployed = b.metricsAnalyticsDeployed; } public String getName() { @@ -95,14 +93,6 @@ public void setBaseCloudFormationStackInfo(Map baseCloudFormatio this.baseCloudFormationStackInfo = baseCloudFormationStackInfo; } - public boolean isMetricsAnalyticsDeployed() { - return this.metricsAnalyticsDeployed; - } - - public void setMetricsAnalyticsDeployed(boolean metricsAnalyticsDeployed) { - this.metricsAnalyticsDeployed = metricsAnalyticsDeployed; - } - /** * Retrieves a new empty instance of the {@link Environment.Builder}. * @@ -139,7 +129,6 @@ public static final class Builder { private String lambdasFolderName; private String baseCloudFormationStackName; private Map baseCloudFormationStackInfo; - private boolean metricsAnalyticsDeployed; private Builder() { @@ -175,11 +164,6 @@ public Builder baseCloudFormationStackInfo(Map baseCloudFormatio return this; } - public Builder metricsAnalyticsDeployed(boolean metricsAnalyticsDeployed) { - this.metricsAnalyticsDeployed = metricsAnalyticsDeployed; - return this; - } - public Environment build() { return new Environment(this); } diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/ExistingEnvironmentFactory.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/ExistingEnvironmentFactory.java index af9d4165..c102e6b3 100644 --- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/ExistingEnvironmentFactory.java +++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/ExistingEnvironmentFactory.java @@ -40,11 +40,8 @@ public final class ExistingEnvironmentFactory { private static final Logger LOGGER = LoggerFactory.getLogger(ExistingEnvironmentFactory.class); - public static Environment findExistingEnvironment( - SsmClient ssm, - CloudFormationClient cfn, - String environmentName, - String accountId) { + public static Environment findExistingEnvironment(SsmClient ssm, CloudFormationClient cfn, String environmentName, + String accountId) { if (Utils.isBlank(environmentName)) { throw new EnvironmentLoadException("EnvironmentName cannot be blank."); } @@ -56,17 +53,14 @@ public static Environment findExistingEnvironment( .baseCloudFormationStackName(baseCloudFormationStackName) .baseCloudFormationStackInfo(getExistingSaaSBoostStackDetails(cfn, baseCloudFormationStackName)) .lambdasFolderName(getExistingSaaSBoostLambdasFolder(ssm, environmentName)) - .metricsAnalyticsDeployed(getExistingSaaSBoostAnalyticsDeployed(ssm, environmentName)) .name(environmentName) .accountId(accountId) .build(); } // VisibleForTesting - static SaaSBoostArtifactsBucket getExistingSaaSBoostArtifactBucket( - SsmClient ssm, - String environmentName, - Region region) { + static SaaSBoostArtifactsBucket getExistingSaaSBoostArtifactBucket(SsmClient ssm, String environmentName, + Region region) { LOGGER.debug("Getting existing SaaS Boost artifact bucket name from Parameter Store"); String artifactsBucket = null; try { @@ -95,7 +89,7 @@ static String getExistingSaaSBoostStackName(SsmClient ssm, String environmentNam String stackName = null; try { GetParameterResponse response = ssm.getParameter(request -> request - .name("/saas-boost/" + environmentName + "/SAAS_BOOST_STACK") + .name("/saas-boost/" + environmentName + "/STACK_NAME") ); stackName = response.parameter().value(); } catch (ParameterNotFoundException paramStoreError) { @@ -112,13 +106,12 @@ static String getExistingSaaSBoostStackName(SsmClient ssm, String environmentNam } // VisibleForTesting - static Map getExistingSaaSBoostStackDetails( - CloudFormationClient cfn, - String baseCloudFormationStackName) { + static Map getExistingSaaSBoostStackDetails(CloudFormationClient cfn, + String baseCloudFormationStackName) { LOGGER.debug("Getting CloudFormation stack details for SaaS Boost stack {}", baseCloudFormationStackName); Map details = new HashMap<>(); - List requiredOutputs = List.of("PublicSubnet1", "PublicSubnet2", "PrivateSubnet1", - "PrivateSubnet2", "EgressVpc", "LoggingBucket"); + // TODO not sure we need this + List requiredOutputs = List.of("LoggingBucket"); try { DescribeStacksResponse response = cfn.describeStacks( request -> request.stackName(baseCloudFormationStackName)); @@ -153,12 +146,12 @@ static String getExistingSaaSBoostLambdasFolder(SsmClient ssm, String environmen String lambdasFolder = null; try { GetParameterResponse response = ssm.getParameter(request -> request - .name("/saas-boost/" + environmentName + "/SAAS_BOOST_LAMBDAS_FOLDER") + .name("/saas-boost/" + environmentName + "/LAMBDAS_FOLDER") ); lambdasFolder = response.parameter().value(); } catch (ParameterNotFoundException paramStoreError) { LOGGER.warn("Parameter /saas-boost/" + environmentName - + "/SAAS_BOOST_LAMBDAS_FOLDER not found setting to default 'lambdas'"); + + "/LAMBDAS_FOLDER not found setting to default 'lambdas'"); lambdasFolder = "lambdas"; } catch (SdkServiceException ssmError) { LOGGER.error("ssm:GetParameter error {}", ssmError.getMessage()); @@ -169,24 +162,4 @@ static String getExistingSaaSBoostLambdasFolder(SsmClient ssm, String environmen return lambdasFolder; } - // VisibleForTesting - static boolean getExistingSaaSBoostAnalyticsDeployed(SsmClient ssm, String environmentName) { - LOGGER.debug("Getting existing SaaS Boost Analytics module deployed from Parameter Store"); - boolean analyticsDeployed = false; - try { - GetParameterResponse response = ssm.getParameter(request -> request - .name("/saas-boost/" + environmentName + "/METRICS_ANALYTICS_DEPLOYED") - ); - analyticsDeployed = Boolean.parseBoolean(response.parameter().value()); - } catch (ParameterNotFoundException paramStoreError) { - // this means the parameter doesn't exist, so ignore - } catch (SdkServiceException ssmError) { - // TODO CloudFormation should own this parameter, not the installer... - LOGGER.error("ssm:GetParameter error {}", ssmError.getMessage()); - LOGGER.error(Utils.getFullStackTrace(ssmError)); - throw ssmError; - } - LOGGER.info("Loaded analytics deployed {}", analyticsDeployed); - return analyticsDeployed; - } } diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflow.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflow.java index aa5802a5..40efd587 100644 --- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflow.java +++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflow.java @@ -21,7 +21,6 @@ import com.amazon.aws.partners.saasfactory.saasboost.Keyboard; import com.amazon.aws.partners.saasfactory.saasboost.SaaSBoostInstall; import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import com.amazon.aws.partners.saasfactory.saasboost.clients.AwsClientBuilderFactory; import com.amazon.aws.partners.saasfactory.saasboost.model.Environment; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; @@ -38,6 +37,7 @@ import software.amazon.awssdk.services.cloudformation.model.StackStatus; import software.amazon.awssdk.services.cloudformation.model.UpdateStackRequest; import software.amazon.awssdk.services.cloudformation.model.UpdateStackResponse; +import software.amazon.awssdk.services.s3.S3Client; import java.io.BufferedReader; import java.io.File; @@ -63,18 +63,16 @@ public class UpdateWorkflow extends AbstractWorkflow { private final Environment environment; private final Path workingDir; - private final AwsClientBuilderFactory clientBuilderFactory; - private final boolean doesCfnMacroResourceExist; - - public UpdateWorkflow( - Path workingDir, - Environment environment, - AwsClientBuilderFactory clientBuilderFactory, - boolean doesCfnMacroResourceExist) { + private final S3Client s3; + private final CloudFormationClient cfn; + private final ApiGatewayClient apigw; + + public UpdateWorkflow(Path workingDir, Environment environment, S3Client s3, CloudFormationClient cfn, ApiGatewayClient apigw) { this.environment = environment; this.workingDir = workingDir; - this.clientBuilderFactory = clientBuilderFactory; - this.doesCfnMacroResourceExist = doesCfnMacroResourceExist; + this.s3 = s3; + this.cfn = cfn; + this.apigw = apigw; } private boolean confirm() { @@ -115,7 +113,7 @@ public void run() { outputMessage("Updating admin web application..."); SaaSBoostInstall.copyAdminWebAppSourceToS3(workingDir, environment.getArtifactsBucket().getBucketName(), - clientBuilderFactory.s3Builder().build()); + s3); break; } case CUSTOM_RESOURCES: @@ -127,56 +125,35 @@ public void run() { for (String target : action.getTargets()) { // TODO update this logic for windows File updatedDirectory = new File(action.getDirectoryName(), target); - outputMessage("Updating " + updatedDirectory + " using " - + new File(updatedDirectory, "update.sh")); - // if this fails because update.sh does not exist, does not have the proper - // permissions or any other reason, a runtimeException will be thrown, exiting - // the run() execution - SaaSBoostInstall.executeCommand( - "./update.sh " + environment.getName(), // command to execute - null, // environment to use - updatedDirectory.getAbsoluteFile()); // directory to execute from + if (updatedDirectory.exists()) { + outputMessage("Updating " + updatedDirectory + " using " + + new File(updatedDirectory, "update.sh")); + // if this fails because update.sh does not exist, does not have the proper + // permissions or any other reason, a runtimeException will be thrown, exiting + // the run() execution + SaaSBoostInstall.executeCommand( + "./update.sh " + environment.getName(), // command to execute + null, // environment to use + updatedDirectory.getAbsoluteFile()); // directory to execute from + } else { + outputMessage("Warning! Directory to update " + + action.getDirectoryName() + " does not exist!"); + } } break; } case RESOURCES: { // upload the template to the Boost Artifacts bucket for (String target : action.getTargets()) { - outputMessage("Updating CloudFormation template: " + target); - environment.getArtifactsBucket().putFile( - clientBuilderFactory.s3Builder().build(), // s3 client - Path.of(action.getDirectoryName(), target), // local path - Path.of(target)); // remote path - if (target.equals("saas-boost-metrics-analytics.yaml") - && environment.isMetricsAnalyticsDeployed()) { - // the metrics-analytics stack is not a child stack of the base stack, - // so just updating the base stack won't update. update it manually. - String analyticsStackName = environment.getBaseCloudFormationStackName() + "-analytics"; - // Load up the existing parameters from CloudFormation - Map stackParamsMap = new LinkedHashMap<>(); - try { - DescribeStacksResponse response = clientBuilderFactory.cloudFormationBuilder().build() - .describeStacks(request -> request.stackName(analyticsStackName)); - if (response.hasStacks() && !response.stacks().isEmpty()) { - Stack stack = response.stacks().get(0); - stackParamsMap = stack.parameters().stream() - .collect(Collectors.toMap( - Parameter::parameterKey, Parameter::parameterValue)); - } - } catch (SdkServiceException cfnError) { - if (cfnError.getMessage().contains("does not exist")) { - outputMessage("Analytics module CloudFormation stack " - + analyticsStackName + " not found."); - System.exit(2); - } - LOGGER.error("cloudformation:DescribeStacks error", cfnError); - LOGGER.error(Utils.getFullStackTrace(cfnError)); - throw cfnError; - } - Map paramsMap = getCloudFormationParameterMap( - workingDir.resolve(Path.of("resources", "saas-boost-metrics-analytics.yaml")), - stackParamsMap); - updateCloudFormationStack(analyticsStackName, paramsMap, target); + if (Path.of(action.getDirectoryName(), target).toFile().exists()) { + outputMessage("Updating CloudFormation template: " + target); + environment.getArtifactsBucket().putFile( + s3, // s3 client + Path.of(action.getDirectoryName(), target), // local path + Path.of(target)); // remote path + } else { + outputMessage("Warning! Resource to update " + + Path.of(action.getDirectoryName(), target).toFile() + " does not exist!"); } } break; @@ -193,12 +170,6 @@ public void run() { outputMessage("Updating Version parameter to " + Constants.VERSION); cloudFormationParamMap.put("Version", Constants.VERSION); - // If CloudFormation macro resources do not exist, that means that another environment that had previously - // owned those resources was deleted. In this case we should make sure to create them. - if (!doesCfnMacroResourceExist) { - cloudFormationParamMap.put("CreateMacroResources", Boolean.TRUE.toString()); - } - // Always call update stack outputMessage("Executing CloudFormation update stack on: " + environment.getBaseCloudFormationStackName()); updateCloudFormationStack( @@ -481,7 +452,6 @@ private void updateCloudFormationStack(String stackName, Map par .map(entry -> Parameter.builder().parameterKey(entry.getKey()).parameterValue(entry.getValue()).build()) .collect(Collectors.toList()); - CloudFormationClient cfn = clientBuilderFactory.cloudFormationBuilder().build(); LOGGER.info("Executing CloudFormation update stack for " + stackName); try { UpdateStackResponse updateStackResponse = cfn.updateStack(UpdateStackRequest.builder() @@ -510,6 +480,7 @@ private void updateCloudFormationStack(String stackName, Map par StackStatus.UPDATE_ROLLBACK_FAILED); if (stackStatus == StackStatus.UPDATE_COMPLETE) { outputMessage("CloudFormation stack: " + stackName + " updated successfully."); + outputMessage("You may need to update the CloudFormation Integration Stack in the Application Plane AWS Account."); break; } else if (failureStatuses.contains(stackStatus)) { outputMessage("CloudFormation stack: " + stackName + " update failed."); @@ -543,23 +514,18 @@ protected void runApiGatewayDeployment(Map cloudFormationParamMa // CloudFormation will not redeploy an API Gateway stage on update outputMessage("Updating API Gateway deployment for stages"); try { - String publicApiName = "sb-" + environment.getName() + "-public-api"; - String privateApiName = "sb-" + environment.getName() + "-private-api"; - ApiGatewayClient apigw = clientBuilderFactory.apiGatewayBuilder().build(); + String apiName = "sb-" + environment.getName() + "-api"; GetRestApisResponse response = apigw.getRestApis(); if (response.hasItems()) { for (RestApi api : response.items()) { - String apiName = api.name(); - boolean isPublicApi = publicApiName.equals(apiName); - boolean isPrivateApi = privateApiName.equals(apiName); - if (isPublicApi || isPrivateApi) { - String stage = isPublicApi ? cloudFormationParamMap.get("PublicApiStage") - : cloudFormationParamMap.get("PrivateApiStage"); + if (apiName.equals(api.name())) { + String stage = cloudFormationParamMap.get("ApiStage"); outputMessage("Updating API Gateway deployment for " + apiName + " to stage: " + stage); apigw.createDeployment(request -> request .restApiId(api.id()) .stageName(stage) ); + break; } } } diff --git a/installer/src/main/resources/log4j2.xml b/installer/src/main/resources/log4j2.xml index 4de39b98..5b08184a 100644 --- a/installer/src/main/resources/log4j2.xml +++ b/installer/src/main/resources/log4j2.xml @@ -17,16 +17,15 @@ limitations under the License. - + - - - + + - + diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostArtifactsBucketTest.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostArtifactsBucketTest.java index e01a9eb6..f210a090 100644 --- a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostArtifactsBucketTest.java +++ b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostArtifactsBucketTest.java @@ -16,31 +16,33 @@ package com.amazon.aws.partners.saasfactory.saasboost; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.policybuilder.iam.IamPolicy; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.*; import java.nio.file.Path; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class SaaSBoostArtifactsBucketTest { private static final String ENV_NAME = "env-name"; + private static final String APP_PLANE_ACCOUNT = "012345678901"; @Mock S3Client mockS3; - @Before + @BeforeEach public void reset() { Mockito.reset(mockS3); } @@ -52,17 +54,18 @@ public void putFileTest() throws Exception { ArgumentCaptor requestBodyArgumentCaptor = ArgumentCaptor.forClass(RequestBody.class); SaaSBoostArtifactsBucket testBucket = - SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_EAST_1); + SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_EAST_1, APP_PLANE_ACCOUNT); Path localPathToTestPut = Path.of(this.getClass().getClassLoader().getResource("template.yaml").toURI()); Path exampleRemotePath = Path.of("dir", "dir2"); testBucket.putFile(mockS3, localPathToTestPut, exampleRemotePath); Mockito.verify(mockS3).putObject(putObjectRequestArgumentCaptor.capture(), requestBodyArgumentCaptor.capture()); - assertEquals("Put object to the wrong bucket.", - testBucket.getBucketName(), putObjectRequestArgumentCaptor.getValue().bucket()); - assertEquals("Put object to the wrong location.", - exampleRemotePath.toString().replace('\\', '/'), putObjectRequestArgumentCaptor.getValue().key()); - assertEquals("Put different length object to remote location. Wrong file?", - localPathToTestPut.toFile().length(), requestBodyArgumentCaptor.getValue().contentLength()); + assertEquals(testBucket.getBucketName(), putObjectRequestArgumentCaptor.getValue().bucket(), + "Put object to the wrong bucket."); + assertEquals(exampleRemotePath.toString().replace('\\', '/'), + putObjectRequestArgumentCaptor.getValue().key(), + "Put object to the wrong location."); + assertEquals(localPathToTestPut.toFile().length(), requestBodyArgumentCaptor.getValue().contentLength(), + "Put different length object to remote location. Wrong file?"); } @Test @@ -70,23 +73,23 @@ public void createBucketLocationConstraintTest() { ArgumentCaptor createBucketRequestArgumentCaptor = ArgumentCaptor.forClass(CreateBucketRequest.class); - SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_EAST_1); + SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_EAST_1, APP_PLANE_ACCOUNT); Mockito.verify(mockS3).createBucket(createBucketRequestArgumentCaptor.capture()); // expected, actual CreateBucketRequest capturedCreateBucketRequest = createBucketRequestArgumentCaptor.getValue(); if (capturedCreateBucketRequest.createBucketConfiguration() != null) { // if no createBucketConfiguration is passed in the createBucketRequest, // there is implicitly no location constraint (because constraint must be part of the config) - assertNull("No location constraint should be provided for buckets in us-east-1", - createBucketRequestArgumentCaptor.getValue().createBucketConfiguration().locationConstraint()); + assertNull(createBucketRequestArgumentCaptor.getValue().createBucketConfiguration().locationConstraint(), + "No location constraint should be provided for buckets in us-east-1"); } Mockito.reset(mockS3); - SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_WEST_2); + SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_WEST_2, APP_PLANE_ACCOUNT); Mockito.verify(mockS3).createBucket(createBucketRequestArgumentCaptor.capture()); - assertEquals("Location constraint should be provided for buckets in us-west-2", - BucketLocationConstraint.US_WEST_2, - createBucketRequestArgumentCaptor.getValue().createBucketConfiguration().locationConstraint()); + assertEquals(BucketLocationConstraint.US_WEST_2, + createBucketRequestArgumentCaptor.getValue().createBucketConfiguration().locationConstraint(), + "Location constraint should be provided for buckets in us-west-2"); } @Test @@ -94,12 +97,12 @@ public void createBucketServerSideEncryptionTest() { ArgumentCaptor putBucketEncryptionRequestArgumentCaptor = ArgumentCaptor.forClass(PutBucketEncryptionRequest.class); SaaSBoostArtifactsBucket createdBucket = - SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_EAST_1); + SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_EAST_1, APP_PLANE_ACCOUNT); Mockito.verify(mockS3).putBucketEncryption(putBucketEncryptionRequestArgumentCaptor.capture()); PutBucketEncryptionRequest capturedPutBucketEncryptionRequest = putBucketEncryptionRequestArgumentCaptor.getValue(); - assertEquals("Put encryption to the wrong bucket.", - createdBucket.getBucketName(), capturedPutBucketEncryptionRequest.bucket()); + assertEquals(createdBucket.getBucketName(), capturedPutBucketEncryptionRequest.bucket(), + "Put encryption to the wrong bucket."); assertNotNull(capturedPutBucketEncryptionRequest.serverSideEncryptionConfiguration()); assertNotNull(capturedPutBucketEncryptionRequest.serverSideEncryptionConfiguration().rules()); assertTrue(capturedPutBucketEncryptionRequest.serverSideEncryptionConfiguration().rules().contains( @@ -113,32 +116,44 @@ public void createBucketBucketPolicyTest() { ArgumentCaptor putBucketPolicyArgumentCaptor = ArgumentCaptor.forClass(PutBucketPolicyRequest.class); SaaSBoostArtifactsBucket createdBucket = - SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_EAST_1); + SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_EAST_1, APP_PLANE_ACCOUNT); Mockito.verify(mockS3).putBucketPolicy(putBucketPolicyArgumentCaptor.capture()); PutBucketPolicyRequest capturedPutBucketPolicyRequest = putBucketPolicyArgumentCaptor.getValue(); - assertEquals("Put bucket policy to the wrong bucket.", - createdBucket.getBucketName(), capturedPutBucketPolicyRequest.bucket()); + assertEquals(createdBucket.getBucketName(), capturedPutBucketPolicyRequest.bucket(), + "Put bucket policy to the wrong bucket."); assertNotNull(capturedPutBucketPolicyRequest.policy()); - assertEquals("{\n" + - " \"Version\": \"2012-10-17\",\n" + - " \"Statement\": [\n" + - " {\n" + - " \"Sid\": \"DenyNonHttps\",\n" + - " \"Effect\": \"Deny\",\n" + - " \"Principal\": \"*\",\n" + - " \"Action\": \"s3:*\",\n" + - " \"Resource\": [\n" + - " \"arn:aws:s3:::" + createdBucket.getBucketName() + "/*\",\n" + - " \"arn:aws:s3:::" + createdBucket.getBucketName() + "\"\n" + - " ],\n" + - " \"Condition\": {\n" + - " \"Bool\": {\n" + - " \"aws:SecureTransport\": \"false\"\n" + - " }\n" + - " }\n" + - " }\n" + - " ]\n" + - "}", capturedPutBucketPolicyRequest.policy()); + String partition = createdBucket.getRegion().metadata().partition().id(); + IamPolicy createdPolicy = IamPolicy.fromJson(capturedPutBucketPolicyRequest.policy()); + IamPolicy expectedPolicy = IamPolicy.fromJson("{\n" + + " \"Version\": \"2012-10-17\",\n" + + " \"Statement\": [\n" + + " {\n" + + " \"Sid\": \"DenyNonHttps\",\n" + + " \"Effect\": \"Deny\",\n" + + " \"Principal\": \"*\",\n" + + " \"Action\": \"s3:*\",\n" + + " \"Resource\": [\n" + + " \"arn:" + partition + ":s3:::" + createdBucket.getBucketName() + "/*\",\n" + + " \"arn:" + partition + ":s3:::" + createdBucket.getBucketName() + "\"\n" + + " ],\n" + + " \"Condition\": {\n" + + " \"Bool\": {\n" + + " \"aws:SecureTransport\": \"false\"\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"Sid\": \"AppPlaneAccountQuickLink\",\n" + + " \"Effect\": \"Allow\",\n" + + " \"Principal\": {\n" + + " \"AWS\": \"arn:aws:iam::" + createdBucket.getAppPlaneAccountId() + ":root\"\n" + + " },\n" + + " \"Action\": \"s3:GetObject\",\n" + + " \"Resource\": \"arn:" + partition + ":s3:::" + createdBucket.getBucketName() + "/saas-boost-app-integration.yaml\"\n" + + " }\n" + + " ]\n" + + "}"); + assertEquals(expectedPolicy, createdPolicy); } @Test @@ -146,31 +161,43 @@ public void createBucketBucketPolicyTest_china() { ArgumentCaptor putBucketPolicyArgumentCaptor = ArgumentCaptor.forClass(PutBucketPolicyRequest.class); SaaSBoostArtifactsBucket createdBucket = - SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.CN_NORTHWEST_1); + SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.CN_NORTHWEST_1, APP_PLANE_ACCOUNT); Mockito.verify(mockS3).putBucketPolicy(putBucketPolicyArgumentCaptor.capture()); PutBucketPolicyRequest capturedPutBucketPolicyRequest = putBucketPolicyArgumentCaptor.getValue(); - assertEquals("Put bucket policy to the wrong bucket.", - createdBucket.getBucketName(), capturedPutBucketPolicyRequest.bucket()); + assertEquals(createdBucket.getBucketName(), capturedPutBucketPolicyRequest.bucket(), + "Put bucket policy to the wrong bucket."); assertNotNull(capturedPutBucketPolicyRequest.policy()); - assertEquals("{\n" + - " \"Version\": \"2012-10-17\",\n" + - " \"Statement\": [\n" + - " {\n" + - " \"Sid\": \"DenyNonHttps\",\n" + - " \"Effect\": \"Deny\",\n" + - " \"Principal\": \"*\",\n" + - " \"Action\": \"s3:*\",\n" + - " \"Resource\": [\n" + - " \"arn:aws-cn:s3:::" + createdBucket.getBucketName() + "/*\",\n" + - " \"arn:aws-cn:s3:::" + createdBucket.getBucketName() + "\"\n" + - " ],\n" + - " \"Condition\": {\n" + - " \"Bool\": {\n" + - " \"aws:SecureTransport\": \"false\"\n" + - " }\n" + - " }\n" + - " }\n" + - " ]\n" + - "}", capturedPutBucketPolicyRequest.policy()); + String partition = createdBucket.getRegion().metadata().partition().id(); + IamPolicy createdPolicy = IamPolicy.fromJson(capturedPutBucketPolicyRequest.policy()); + IamPolicy expectedPolicy = IamPolicy.fromJson("{\n" + + " \"Version\": \"2012-10-17\",\n" + + " \"Statement\": [\n" + + " {\n" + + " \"Sid\": \"DenyNonHttps\",\n" + + " \"Effect\": \"Deny\",\n" + + " \"Principal\": \"*\",\n" + + " \"Action\": \"s3:*\",\n" + + " \"Resource\": [\n" + + " \"arn:" + partition + ":s3:::" + createdBucket.getBucketName() + "/*\",\n" + + " \"arn:" + partition + ":s3:::" + createdBucket.getBucketName() + "\"\n" + + " ],\n" + + " \"Condition\": {\n" + + " \"Bool\": {\n" + + " \"aws:SecureTransport\": \"false\"\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"Sid\": \"AppPlaneAccountQuickLink\",\n" + + " \"Effect\": \"Allow\",\n" + + " \"Principal\": {\n" + + " \"AWS\": \"arn:aws:iam::" + createdBucket.getAppPlaneAccountId() + ":root\"\n" + + " },\n" + + " \"Action\": \"s3:GetObject\",\n" + + " \"Resource\": \"arn:" + partition + ":s3:::" + createdBucket.getBucketName() + "/saas-boost-app-integration.yaml\"\n" + + " }\n" + + " ]\n" + + "}"); + assertEquals(expectedPolicy, createdPolicy); } } diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstallTest.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstallTest.java index 0a922bf8..8db37b46 100644 --- a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstallTest.java +++ b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstallTest.java @@ -15,16 +15,14 @@ */ package com.amazon.aws.partners.saasfactory.saasboost; -import org.junit.After; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.*; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; // Note that these tests will only work if you run them from Maven or if you add // the AWS_REGION environment variable to your IDE's configuration settings @@ -76,7 +74,7 @@ public static void initTemplate() { } */ - @After + @AfterEach public void resetStdIn() { System.setIn(System.in); } diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactoryTest.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactoryTest.java deleted file mode 100644 index 096fde30..00000000 --- a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactoryTest.java +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost.clients; - -import org.junit.After; -import org.junit.BeforeClass; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; -import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; -import software.amazon.awssdk.awscore.client.builder.AwsSyncClientBuilder; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.quicksight.QuickSightClientBuilder; - -import java.lang.reflect.Method; -import java.util.List; - -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; - -public class AwsClientBuilderFactoryTest { - - private static final Region DEFAULT_EXPECTED_REGION = null; - private static final Class DEFAULT_EXPECTED_CREDENTIALS_PROVIDER_CLASS = - RefreshingProfileDefaultCredentialsProvider.class; - - private static QuickSightClientBuilder mockBuilder; - - @BeforeClass - public static void createMockBuilder() { - mockBuilder = mock(QuickSightClientBuilder.class); - when(mockBuilder.credentialsProvider(any())).thenReturn(mockBuilder); - when(mockBuilder.region(any())).thenReturn(mockBuilder); - } - - @After - public void resetMockBuilder() { - clearInvocations(mockBuilder); - } - - @Test - public void buildFactoryWithNoRegion() { - // this test verifies that a null region is automatically filled with the default profile region - // in the SDK. this is assumed by the BoostAwsClientBuilderFactory and will fail should that behavior change - AwsClientBuilderFactory.builder().build().quickSightBuilder().build(); - } - - @Test - public void verifyBuildersHaveDefaults() { - // for each builder, verify it has region and credentials provider as expected - runBoostAwsClientBuilderFactoryTest(AwsClientBuilderFactory.builder().build(), - DEFAULT_EXPECTED_REGION, DEFAULT_EXPECTED_CREDENTIALS_PROVIDER_CLASS); - } - - @Test - public void verifyBuilderRegionOverridden() { - Region expectedRegion = Region.AF_SOUTH_1; - runBoostAwsClientBuilderFactoryTest(AwsClientBuilderFactory.builder().region(expectedRegion).build(), - expectedRegion, DEFAULT_EXPECTED_CREDENTIALS_PROVIDER_CLASS); - } - - @Test - public void verifyBuilderCredentialProviderOverridden() { - Class expectedCredentialsProviderClass = DefaultCredentialsProvider.class; - runBoostAwsClientBuilderFactoryTest( - AwsClientBuilderFactory.builder() - .credentialsProvider(DefaultCredentialsProvider.create()) - .build(), - DEFAULT_EXPECTED_REGION, expectedCredentialsProviderClass); - } - - @Test - public void verifyFactoryCachingForAllBuilders() { - AwsClientBuilderFactory factory = AwsClientBuilderFactory.builder().build(); - // for each method that returns a builder.. - for (Method m : factory.getClass().getMethods()) { - // checking if the return type implements AwsSyncClientBuilder - if (List.of(m.getReturnType().getInterfaces()).contains(AwsSyncClientBuilder.class)) { - try { - AwsSyncClientBuilder b = (AwsSyncClientBuilder) m.invoke(factory); - // invoking the builder function again should not create a new builder - assertEquals(b, m.invoke(factory)); - } catch (Exception e) { - throw new RuntimeException("test failed", e); - } - } - } - } - - private void runBoostAwsClientBuilderFactoryTest( - AwsClientBuilderFactory factory, - Region expectedRegion, - Class expectedCredentialsProviderClass) { - ArgumentCaptor credentialsProviderArgumentCaptor = - ArgumentCaptor.forClass(AwsCredentialsProvider.class); - factory.decorateBuilderWithDefaults(mockBuilder); - - verify(mockBuilder).region(expectedRegion); - verify(mockBuilder).credentialsProvider(credentialsProviderArgumentCaptor.capture()); - assertEquals("Factory instantiated the wrong credentials provider", - expectedCredentialsProviderClass, - credentialsProviderArgumentCaptor.getValue().getClass()); - } -} diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/MockAwsClientBuilderFactory.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/MockAwsClientBuilderFactory.java deleted file mode 100644 index a585618d..00000000 --- a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/MockAwsClientBuilderFactory.java +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - package com.amazon.aws.partners.saasfactory.saasboost.clients; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.net.URI; - -import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; -import software.amazon.awssdk.http.SdkHttpClient; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.cloudformation.CloudFormationClient; -import software.amazon.awssdk.services.cloudformation.CloudFormationClientBuilder; - -public class MockAwsClientBuilderFactory extends AwsClientBuilderFactory { - private final AwsClientBuilderFactory factory = mock(AwsClientBuilderFactory.class); - - public MockAwsClientBuilderFactory() { - - } - - public void mockCfn(CloudFormationClient cfn) { - when(factory.cloudFormationBuilder()).thenReturn(new CloudFormationClientBuilder() { - - @Override - public CloudFormationClientBuilder httpClient(SdkHttpClient httpClient) { - // TODO Auto-generated method stub - return null; - } - - @Override - public CloudFormationClientBuilder httpClientBuilder( - software.amazon.awssdk.http.SdkHttpClient.Builder httpClientBuilder) { - // TODO Auto-generated method stub - return null; - } - - @Override - public CloudFormationClientBuilder credentialsProvider(AwsCredentialsProvider credentialsProvider) { - // TODO Auto-generated method stub - return null; - } - - @Override - public CloudFormationClientBuilder region(Region region) { - // TODO Auto-generated method stub - return null; - } - - @Override - public CloudFormationClientBuilder dualstackEnabled(Boolean dualstackEndpointEnabled) { - // TODO Auto-generated method stub - return null; - } - - @Override - public CloudFormationClientBuilder fipsEnabled(Boolean fipsEndpointEnabled) { - // TODO Auto-generated method stub - return null; - } - - @Override - public CloudFormationClientBuilder overrideConfiguration( - ClientOverrideConfiguration overrideConfiguration) { - // TODO Auto-generated method stub - return null; - } - - @Override - public CloudFormationClientBuilder endpointOverride(URI endpointOverride) { - // TODO Auto-generated method stub - return null; - } - - @Override - public CloudFormationClient build() { - // TODO Auto-generated method stub - return cfn; - } - - }); - } -} diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/ProfileUtils.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/ProfileUtils.java deleted file mode 100644 index 2daa46c5..00000000 --- a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/ProfileUtils.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.amazon.aws.partners.saasfactory.saasboost.clients; - -import software.amazon.awssdk.auth.credentials.AwsCredentials; -import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; -import software.amazon.awssdk.core.SdkSystemSetting; -import software.amazon.awssdk.profiles.Profile; - -import java.io.*; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -public class ProfileUtils { - public static void updateOrCreateProfile(String filename, - String profile, - AwsCredentials newCredentials) - throws IOException { - // this will automatically create the file if it does not already exist - try (FileWriter updatingOutputStream = new FileWriter(filename, false)) { - writeProfileToFileWriter( - Profile.builder() - .name(profile) - .properties(propertiesFromCredentials(newCredentials)) - .build(), - updatingOutputStream); - } - } - - private static void writeProfileToFileWriter(Profile profile, FileWriter fileWriter) throws IOException { - fileWriter.write("[" + profile.name() + "]\n"); - for (Map.Entry property : profile.properties().entrySet()) { - fileWriter.write(property.getKey() + " = " + property.getValue() + "\n"); - } - fileWriter.write("\n"); - } - - private static Map propertiesFromCredentials(AwsCredentials awsCredentials) { - Map properties = new HashMap<>(); - properties.put( - SdkSystemSetting.AWS_ACCESS_KEY_ID.environmentVariable().toLowerCase(), - awsCredentials.accessKeyId()); - properties.put( - SdkSystemSetting.AWS_SECRET_ACCESS_KEY.environmentVariable().toLowerCase(), - awsCredentials.secretAccessKey()); - if (awsCredentials instanceof AwsSessionCredentials) { - properties.put( - SdkSystemSetting.AWS_SESSION_TOKEN.environmentVariable().toLowerCase(), - ((AwsSessionCredentials) awsCredentials).sessionToken()); - } - return properties; - } -} diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/RefreshingProfileDefaultCredentialsProviderTest.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/RefreshingProfileDefaultCredentialsProviderTest.java deleted file mode 100644 index 68f7637e..00000000 --- a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/RefreshingProfileDefaultCredentialsProviderTest.java +++ /dev/null @@ -1,239 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost.clients; - -import org.junit.After; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.auth.credentials.*; -import software.amazon.awssdk.core.SdkSystemSetting; -import software.amazon.awssdk.profiles.ProfileFileSystemSetting; - -import java.io.Closeable; -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import static org.junit.Assert.*; -import static org.junit.Assume.assumeFalse; - -public class RefreshingProfileDefaultCredentialsProviderTest { - private static final Logger LOGGER = LoggerFactory.getLogger(RefreshingProfileDefaultCredentialsProviderTest.class); - - private static final String AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID"; - private static final String AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"; - - private static final String BEFORE_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"; - private static final String AFTER_ACCESS_KEY = "AKIAI44QH8DHBEXAMPLE"; - private static final String BEFORE_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"; - private static final String AFTER_SECRET_KEY = "je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY"; - private static final String BEFORE_SESSION_TOKEN = "fakesessiontoken"; - private static final String AFTER_SESSION_TOKEN = "adifferentfakesessiontoken"; - private static final AwsCredentials BEFORE_PERMANENT = - AwsBasicCredentials.create(BEFORE_ACCESS_KEY, BEFORE_SECRET_KEY); - private static final AwsCredentials AFTER_PERMANENT = - AwsBasicCredentials.create(AFTER_ACCESS_KEY, AFTER_SESSION_TOKEN); - private static final AwsCredentials BEFORE_TEMPORARY = - AwsSessionCredentials.create(BEFORE_ACCESS_KEY, BEFORE_SECRET_KEY, BEFORE_SESSION_TOKEN); - private static final AwsCredentials AFTER_TEMPORARY = - AwsSessionCredentials.create(AFTER_ACCESS_KEY, AFTER_SECRET_KEY, AFTER_SESSION_TOKEN); - private static final AwsCredentials[] TEST_CREDENTIALS = new AwsCredentials[] { - BEFORE_PERMANENT, AFTER_PERMANENT, BEFORE_TEMPORARY, AFTER_TEMPORARY - }; - - private static final String RESOURCES_LOCATION = "src/test/resources/"; - - // this is created to hide all AWS credentials from the test environment that are not profile credentials - // so that the RefreshingProfileDefaultCredentialsProvider can be tested directly. - private static NoSystemPropertyCredentialsTestEnvironment cachedSystemProperties; - - private String fakeProfileFilename; - - public RefreshingProfileDefaultCredentialsProviderTest() { - - } - - /** - * Returns whether to skip these tests based on AWS Credentials Environment Variable configuration. - * - * As stated in the documentation for the NoSystemPropertyCredentialsTestEnvironment, if environment - * variables are configured for this test they will not be hidden and this test cannot verify the - * functionality of the RefreshingProfileDefaultCredentialsProvider. - * - * In most cases it is acceptable to skip these tests, since if users are running these tests with - * the EnvironmentVariableCredentialsProvider that means they will likely be running the Installer - * itself with the same configuration, so the the RefreshingProfileDefaultCredentialsProvider will - * not be used at all. - * - * The only caveat is that any change to the RefreshingProfileDefaultCredentialsProviderTest must - * guarantee that this test is run before being accepted for contribution. - * - * @return true if AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY is configured in environment - */ - private static boolean shouldSkipTests() { - Set environmentKeys = System.getenv().keySet(); - return environmentKeys.contains(AWS_ACCESS_KEY_ID) || environmentKeys.contains(AWS_SECRET_ACCESS_KEY); - } - - @BeforeClass - public static void cacheAndHideSystemProperties() { - cachedSystemProperties = new NoSystemPropertyCredentialsTestEnvironment(); - } - - @AfterClass - public static void resetSystemProperties() { - cachedSystemProperties.close(); - } - - @After - public void cleanUp() { - if (!new File(fakeProfileFilename).delete()) { - LOGGER.error("Failed to delete {} due to an underlying FileSystem error", fakeProfileFilename); - } - } - - /** - * Tests whether the RefreshingDefaultCredentialsProviderTest is still necessary. - * - * For more information, see the class javadoc for {@link RefreshingProfileDefaultCredentialsProvider}. - * - * @throws IOException in case of any errors in File management - */ - @Test - public void defaultCredentialsProviderBugStillExists() throws IOException { - // if we change the profile from under the DefaultCredentials provider, does it return the old credentials? - fakeProfileFilename = getAbsoluteFakeProfileFilename("fake-credentials-default"); - ProfileUtils.updateOrCreateProfile( - fakeProfileFilename, - ProfileFileSystemSetting.AWS_PROFILE.defaultValue(), - TEST_CREDENTIALS[0]); - System.setProperty( - ProfileFileSystemSetting.AWS_SHARED_CREDENTIALS_FILE.property(), - fakeProfileFilename); - System.setProperty( - ProfileFileSystemSetting.AWS_PROFILE.property(), - ProfileFileSystemSetting.AWS_PROFILE.defaultValue()); - DefaultCredentialsProvider defaultCredentialsProvider = DefaultCredentialsProvider.create(); - runUpdatingCredentialsProviderTest(defaultCredentialsProvider, false); - } - - @Test - public void refreshingCredentialsProviderFindsNewCredentials() throws IOException { - fakeProfileFilename = getAbsoluteFakeProfileFilename("fake-credentials-refreshing"); - ProfileUtils.updateOrCreateProfile( - fakeProfileFilename, - ProfileFileSystemSetting.AWS_PROFILE.defaultValue(), - TEST_CREDENTIALS[0]); - RefreshingProfileDefaultCredentialsProvider refreshingCredentialsProvider = RefreshingProfileDefaultCredentialsProvider.builder() - .profileFilename(fakeProfileFilename) - .profileName(ProfileFileSystemSetting.AWS_PROFILE.defaultValue()) - .build(); - runUpdatingCredentialsProviderTest(refreshingCredentialsProvider, true); - } - - private void runUpdatingCredentialsProviderTest( - AwsCredentialsProvider credentialsProviderToTest, - boolean expectUpdate) throws IOException { - assumeFalse("Skipping test due to configuration of AWS credentials as Environment Variables." - + " To run this test unset the environment variables " - + AWS_ACCESS_KEY_ID + " and " + AWS_SECRET_ACCESS_KEY, - shouldSkipTests()); - AwsCredentials expectedCredentials = TEST_CREDENTIALS[0]; - for (int i = 1 ; i < TEST_CREDENTIALS.length ; i++) { - assertEquals(expectedCredentials, credentialsProviderToTest.resolveCredentials()); - ProfileUtils.updateOrCreateProfile( - fakeProfileFilename, - ProfileFileSystemSetting.AWS_PROFILE.defaultValue(), - TEST_CREDENTIALS[i]); - if (expectUpdate) { - expectedCredentials = TEST_CREDENTIALS[i]; - } - } - } - - private String getAbsoluteFakeProfileFilename(String simpleFilename) { - return new File(RESOURCES_LOCATION + simpleFilename).getAbsolutePath(); - } - - /** - * Hides the system property credentials when for the RefreshingDefaultCredentialsProviderTest to force - * the profile credentials provider to be used by the DefaultCredentialsProvider. - * - * For reference, the {@link DefaultCredentialsProvider} reads credentials in the following manner: - * {@link SystemPropertyCredentialsProvider} - * {@link EnvironmentVariableCredentialsProvider} - * {@link WebIdentityTokenFileCredentialsProvider} - * {@link ProfileCredentialsProvider} - * {@link ContainerCredentialsProvider} - * {@link InstanceProfileCredentialsProvider} - * - * To force the underlying {@link DefaultCredentialsProvider} to use the {@link ProfileCredentialsProvider}, we need - * to guarantee that the {@link SystemPropertyCredentialsProvider}, {@link EnvironmentVariableCredentialsProvider}, - * and {@link WebIdentityTokenFileCredentialsProvider} all will return no credentials. If any of these return - * credentials then our test will fail, expecting configured Profile credentials but receiving some others. - * - * Luckily for the {@link SystemPropertyCredentialsProvider} and {@link WebIdentityTokenFileCredentialsProvider}, - * whether any credentials are loaded is controllable via System properties, so this - * NoSystemPropertyCredentialsTestEnvironment class caches and unsets any System properties (using - * {@link System#getProperty(String)}, {@link System#clearProperty(String)}, and - * {@link System#setProperty(String, String)}). - * - * For the {@link EnvironmentVariableCredentialsProvider} however, because environment variables are not - * controllable at runtime, we have no recourse. Adding environment variable credentials to this test will - * currently cause this test to fail. If for some reason we need to add environment variable credentials to - * tests in the installer, this test can be refactored (with the addition of PowerMock to our testing dependencies) - * to statically mock responses to {@link System#getenv(String)} to return nothing for the credential environment - * variable names. - */ - private static class NoSystemPropertyCredentialsTestEnvironment implements Closeable { - private static final String[] PROPERTIES_TO_CHECK = new String[]{ - SdkSystemSetting.AWS_ACCESS_KEY_ID.property(), - SdkSystemSetting.AWS_SECRET_ACCESS_KEY.property(), - SdkSystemSetting.AWS_SESSION_TOKEN.property(), - SdkSystemSetting.AWS_ROLE_SESSION_NAME.property(), - SdkSystemSetting.AWS_ROLE_ARN.property(), - SdkSystemSetting.AWS_WEB_IDENTITY_TOKEN_FILE.property(), - ProfileFileSystemSetting.AWS_SHARED_CREDENTIALS_FILE.property(), - ProfileFileSystemSetting.AWS_PROFILE.property() - }; - - private final Map hiddenProperties; - - public NoSystemPropertyCredentialsTestEnvironment() { - hiddenProperties = new HashMap<>(); - for (String propertyKey : PROPERTIES_TO_CHECK) { - String propertyValue = System.getProperty(propertyKey); - if (propertyValue != null) { - hiddenProperties.put(propertyKey, propertyValue); - System.clearProperty(propertyKey); - } - } - } - - @Override - public void close() { - for (Map.Entry hiddenProperty : hiddenProperties.entrySet()) { - System.setProperty(hiddenProperty.getKey(), hiddenProperty.getValue()); - } - } - } -} diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflowTest.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflowTest.java index 230a87f1..6a38b92a 100644 --- a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflowTest.java +++ b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflowTest.java @@ -16,9 +16,6 @@ package com.amazon.aws.partners.saasfactory.saasboost.workflow; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; @@ -33,13 +30,12 @@ import java.util.Map; import java.util.Set; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import com.amazon.aws.partners.saasfactory.saasboost.clients.AwsClientBuilderFactory; -import com.amazon.aws.partners.saasfactory.saasboost.clients.MockAwsClientBuilderFactory; import com.amazon.aws.partners.saasfactory.saasboost.model.Environment; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; public class UpdateWorkflowTest { @@ -49,12 +45,10 @@ public class UpdateWorkflowTest { .build(); private UpdateWorkflow updateWorkflow; - private AwsClientBuilderFactory clientBuilderFactory; private Path workingDir; - @Before + @BeforeEach public void setup() { - clientBuilderFactory = new MockAwsClientBuilderFactory(); workingDir = Paths.get("../"); try { // location is installer/target/something.jar, so we need @@ -64,10 +58,10 @@ public void setup() { } catch (URISyntaxException urise) { throw new RuntimeException("Failed to determine installation directory for test"); } - updateWorkflow = new UpdateWorkflow(workingDir, testEnvironment, clientBuilderFactory, true); + updateWorkflow = new UpdateWorkflow(workingDir, testEnvironment, null, null, null); } - @After + @AfterEach public void cleanup() { for (UpdateAction action : UpdateAction.values()) { action.resetTargets(); @@ -96,9 +90,9 @@ public void testGetCloudFormationParameterMap() throws Exception { expected.put("DefaultStringParameter", "foobar"); expected.put("NumericParameter", "1"); - assertEquals("Template has 3 parameters", expected.size(), actual.size()); + assertEquals(expected.size(), actual.size(), "Template has 3 parameters"); for (Map.Entry entry : expected.entrySet()) { - assertEquals(entry.getKey() + " equals " + entry.getValue(), entry.getValue(), actual.get(entry.getKey())); + assertEquals(entry.getValue(), actual.get(entry.getKey()), entry.getKey() + " equals " + entry.getValue()); } } @@ -107,14 +101,14 @@ public void testUpdateActionsFromPaths_basic() { Set expectedActions = EnumSet.of(UpdateAction.CLIENT, UpdateAction.FUNCTIONS); List changedPaths = List.of( Path.of("client/web/src/App.js"), - Path.of("functions/onboarding-app-stack-listener/pom.xml")); + Path.of("functions/authorizer/pom.xml")); Collection actualActions = updateWorkflow.getUpdateActionsFromPaths(changedPaths); assertEquals(expectedActions, actualActions); actualActions.forEach(action -> { if (action == UpdateAction.FUNCTIONS) { assertEquals(1, action.getTargets().size()); assertEquals(1, UpdateAction.FUNCTIONS.getTargets().size()); - assertTrue(action.getTargets().contains("onboarding-app-stack-listener")); + assertTrue(action.getTargets().contains("authorizer")); } }); } @@ -124,8 +118,8 @@ public void testUpdateActionsFromPaths_layersFirst() { Set expectedActions = EnumSet.of(UpdateAction.LAYERS, UpdateAction.CLIENT, UpdateAction.FUNCTIONS); List changedPaths = List.of( Path.of("client/web/src/App.js"), - Path.of("functions/onboarding-app-stack-listener/pom.xml"), - Path.of("layers/apigw-helper/pom.xml")); + Path.of("functions/authorizer/pom.xml"), + Path.of("layers/utils/pom.xml")); Collection actualActions = updateWorkflow.getUpdateActionsFromPaths(changedPaths); assertEquals(expectedActions, actualActions); // the first item in the set iterator should always be LAYERS @@ -146,7 +140,7 @@ public void testUpdateActionsFromPaths_customResourcesPath() { Set expectedActions = EnumSet.of(UpdateAction.CUSTOM_RESOURCES, UpdateAction.RESOURCES); List changedPaths = List.of( Path.of("resources/saas-boost.yaml"), - Path.of("resources/custom-resources/app-services-macro/pom.xml")); + Path.of("resources/custom-resources/clear-s3-bucket/pom.xml")); Collection actualActions = updateWorkflow.getUpdateActionsFromPaths(changedPaths); assertEquals(expectedActions, actualActions); actualActions.forEach(action -> { @@ -156,7 +150,7 @@ public void testUpdateActionsFromPaths_customResourcesPath() { } if (action == UpdateAction.CUSTOM_RESOURCES) { assertEquals(1, action.getTargets().size()); - assertTrue(action.getTargets().contains("app-services-macro")); + assertTrue(action.getTargets().contains("clear-s3-bucket")); } }); } diff --git a/layers/apigw-helper/pom.xml b/layers/apigw-helper/pom.xml index 2e28df89..a8a07ec2 100644 --- a/layers/apigw-helper/pom.xml +++ b/layers/apigw-helper/pom.xml @@ -33,6 +33,7 @@ limitations under the License. + ${project.basedir}/../.. 7 @@ -68,10 +69,25 @@ limitations under the License. provided
    + + org.apache.httpcomponents.core5 + httpcore5 + 5.2.2 + software.amazon.awssdk - apache-client + secretsmanager ${aws.java.sdk.version} + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + apache-client + + software.amazon.awssdk diff --git a/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelper.java b/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelper.java index d30484cb..0de0a862 100644 --- a/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelper.java +++ b/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelper.java @@ -16,24 +16,20 @@ package com.amazon.aws.partners.saasfactory.saasboost; -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.net.URIBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.auth.credentials.AwsCredentials; import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; import software.amazon.awssdk.auth.signer.Aws4Signer; import software.amazon.awssdk.auth.signer.params.Aws4SignerParams; -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.exception.SdkServiceException; -import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting; -import software.amazon.awssdk.core.retry.RetryPolicy; -import software.amazon.awssdk.core.retry.backoff.BackoffStrategy; -import software.amazon.awssdk.core.retry.conditions.RetryCondition; import software.amazon.awssdk.http.*; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; import software.amazon.awssdk.services.sts.model.Credentials; @@ -41,12 +37,12 @@ import java.io.*; import java.net.MalformedURLException; -import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Map; +import java.time.Duration; +import java.time.Instant; +import java.util.*; import java.util.stream.Collectors; public class ApiGatewayHelper { @@ -56,23 +52,14 @@ public class ApiGatewayHelper { private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); private static final Aws4Signer SIG_V4 = Aws4Signer.create(); private static SdkHttpClient HTTP_CLIENT = UrlConnectionHttpClient.create(); - private static final StsClient sts = StsClient.builder() - .httpClient(HTTP_CLIENT) - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .region(Region.of(AWS_REGION)) - .endpointOverride(URI.create("https://" + StsClient.SERVICE_NAME + "." + AWS_REGION - + "." + Utils.endpointSuffix(AWS_REGION))) - .overrideConfiguration(ClientOverrideConfiguration.builder() - .retryPolicy(RetryPolicy.builder() - .backoffStrategy(BackoffStrategy.defaultStrategy()) - .throttlingBackoffStrategy(BackoffStrategy.defaultThrottlingStrategy()) - .numRetries(SdkDefaultRetrySetting.defaultMaxAttempts()) - .retryCondition(RetryCondition.defaultRetryCondition()) - .build() - ) - .build() - ) - .build(); + private final Map> CLIENT_CREDENTIALS_CACHE = new HashMap<>(); + private SecretsManagerClient secrets; + private StsClient sts; + private String protocol; + private String host; + private String stage; + private AppClient appClient; + private String signingRole; private ApiGatewayHelper() { if (Utils.isBlank(AWS_REGION)) { @@ -83,28 +70,144 @@ private ApiGatewayHelper() { } } - public static String signAndExecuteApiRequest(SdkHttpFullRequest apiRequest, String assumedRole, String context) { + public static ApiGatewayHelper clientCredentialsHelper(String appClientSecretArn) { + ApiGatewayHelper helper = new ApiGatewayHelper(); + helper.secrets = Utils.sdkClient(SecretsManagerClient.builder(), SecretsManagerClient.SERVICE_NAME); + // Fetch the app client details from SecretsManager + try { + GetSecretValueResponse response = helper.secrets.getSecretValue(request -> request + .secretId(appClientSecretArn) + ); + Map clientDetails = Utils.fromJson(response.secretString(), LinkedHashMap.class); + helper.appClient = AppClient.builder() + .clientName(clientDetails.get("client_name")) + .clientId(clientDetails.get("client_id")) + .clientSecret(clientDetails.get("client_secret")) + .tokenEndpoint(clientDetails.get("token_endpoint")) + .apiEndpoint(clientDetails.get("api_endpoint")) + .build(); + helper.protocol = helper.appClient.getApiEndpointUrl().getProtocol(); + helper.host = helper.appClient.getApiEndpointUrl().getHost(); + helper.stage = helper.appClient.getApiEndpointUrl().getPath().substring(1); + } catch (SdkServiceException secretsManagerError) { + LOGGER.error(Utils.getFullStackTrace(secretsManagerError)); + throw secretsManagerError; + } + return helper; + } + + public static ApiGatewayHelper iamCredentialsHelper(String signingRoleArn, URL apiGatewayUrl) { + ApiGatewayHelper helper = new ApiGatewayHelper(); + helper.sts = Utils.sdkClient(StsClient.builder(), StsClient.SERVICE_NAME); + helper.signingRole = signingRoleArn; + helper.protocol = apiGatewayUrl.getProtocol(); + helper.host = apiGatewayUrl.getHost(); + helper.stage = apiGatewayUrl.getPath().substring(1); + return helper; + } + + public String authorizedRequest(String method, String resource) { + return authorizedRequest(method, resource, null); + } + + public String authorizedRequest(String method, String resource, String body) { + if (appClient == null) { + throw new IllegalStateException("Missing appClient details"); + } + return executeApiRequest( + toSdkHttpFullRequest(HttpRequest.builder() + .protocol(protocol) + .host(host) + .stage(stage) + .headers(Map.of("Authorization", getClientCredentialsBearerToken(appClient))) + .method(method) + .resource(resource) + .body(body) + .build() + ) + ); + } + + public String signedRequest(String method, String resource) { + return signedRequest(method, resource, null); + } + + public String signedRequest(String method, String resource, String body) { + if (Utils.isBlank(signingRole)) { + throw new IllegalStateException("Missing IAM role ARN"); + } + StackWalker.StackFrame frame = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(stack -> stack + .limit(8) + .skip(1) + .findFirst() + .orElse(null) + ); + String assumeRoleSessionName; + if (frame != null) { + assumeRoleSessionName = frame.getClassName() + "." + frame.getMethodName(); + } else { + assumeRoleSessionName = "Unknown caller in " + SAAS_BOOST_ENV; + } + return signAndExecuteApiRequest( + toSdkHttpFullRequest(HttpRequest.builder() + .protocol(protocol) + .host(host) + .stage(stage) + .method(method) + .resource(resource) + .body(body) + .build() + ), + signingRole, + assumeRoleSessionName + ); + } + + public String anonymousRequest(String method, String resource) { + return anonymousRequest(method, resource, null); + } + + public String anonymousRequest(String method, String resource, String body) { + return executeApiRequest( + toSdkHttpFullRequest(HttpRequest.builder() + .protocol(protocol) + .host(host) + .stage(stage) + .method(method) + .resource(resource) + .body(body) + .build() + ) + ); + } + + protected String signAndExecuteApiRequest(SdkHttpFullRequest apiRequest, String assumedRole, String context) { SdkHttpFullRequest signedApiRequest = signApiRequest(apiRequest, assumedRole, context); return executeApiRequest(apiRequest, signedApiRequest); } - public static String executeApiRequest(SdkHttpFullRequest apiRequest) { + protected String executeApiRequest(SdkHttpFullRequest apiRequest) { return executeApiRequest(apiRequest, null); } - private static String executeApiRequest(SdkHttpFullRequest apiRequest, SdkHttpFullRequest signedApiRequest) { - HttpExecuteRequest.Builder requestBuilder = HttpExecuteRequest.builder().request(signedApiRequest != null ? signedApiRequest : apiRequest); - apiRequest.contentStreamProvider().ifPresent(c -> requestBuilder.contentStreamProvider(c)); + protected String executeApiRequest(SdkHttpFullRequest apiRequest, SdkHttpFullRequest signedApiRequest) { + HttpExecuteRequest.Builder requestBuilder = HttpExecuteRequest.builder() + .request(signedApiRequest != null ? signedApiRequest : apiRequest); + apiRequest.contentStreamProvider().ifPresent(requestBuilder::contentStreamProvider); HttpExecuteRequest apiExecuteRequest = requestBuilder.build(); BufferedReader responseReader = null; String responseBody; try { + LOGGER.debug("Executing API Request {}", apiExecuteRequest.httpRequest().getUri().toString()); HttpExecuteResponse apiResponse = HTTP_CLIENT.prepareRequest(apiExecuteRequest).call(); - responseReader = new BufferedReader(new InputStreamReader(apiResponse.responseBody().get(), StandardCharsets.UTF_8)); + responseReader = new BufferedReader(new InputStreamReader(apiResponse.responseBody().get(), + StandardCharsets.UTF_8)); responseBody = responseReader.lines().collect(Collectors.joining()); - LOGGER.info(responseBody); + //LOGGER.debug(responseBody); if (!apiResponse.httpResponse().isSuccessful()) { - throw new RuntimeException("{\"statusCode\":" + apiResponse.httpResponse().statusCode() + ", \"message\":\"" + apiResponse.httpResponse().statusText().get() + "\"}"); + throw new RuntimeException("{\"statusCode\":" + apiResponse.httpResponse().statusCode() + + ", \"message\":\"" + apiResponse.httpResponse().statusText().orElse("") + "\"}"); } } catch (IOException ioe) { LOGGER.error("HTTP Client error {}", ioe.getMessage()); @@ -119,35 +222,25 @@ private static String executeApiRequest(SdkHttpFullRequest apiRequest, SdkHttpFu } } } - return responseBody; } - public static SdkHttpFullRequest getApiRequest(String host, String stage, ApiRequest request) { - return getApiRequest(host, stage, request.getResource(), request.getMethod(), request.getHeaders(), request.getBody()); - } - - public static SdkHttpFullRequest getApiRequest(String host, String stage, String resource, SdkHttpMethod method, Map headers, String body) { + protected SdkHttpFullRequest toSdkHttpFullRequest(HttpRequest request) { SdkHttpFullRequest apiRequest; - String protocol = "https"; try { - URL url = new URL(protocol, host, stage + "/" + resource); + URL url = request.toUrl(); SdkHttpFullRequest.Builder sdkRequestBuilder = SdkHttpFullRequest.builder() - .protocol(protocol) - .host(host) + .protocol(request.getProtocol()) + .host(request.getHost()) .encodedPath(url.getPath()) - .method(method); + .method(request.getMethod()); appendQueryParams(sdkRequestBuilder, url); - putHeaders(sdkRequestBuilder, headers); - if (body != null) { - sdkRequestBuilder.putHeader("Content-Type", "application/json; charset=utf-8"); - sdkRequestBuilder.contentStreamProvider(() -> new StringInputStream(body)); + putHeaders(sdkRequestBuilder, request.getHeaders()); + sdkRequestBuilder.putHeader("Content-Type", "application/json; charset=utf-8"); + if (SdkHttpMethod.GET != request.getMethod() && request.getBody() != null) { + sdkRequestBuilder.contentStreamProvider(() -> new StringInputStream(request.getBody())); } apiRequest = sdkRequestBuilder.build(); - } catch (MalformedURLException mue) { - LOGGER.error("URL parse error {}", mue.getMessage()); - LOGGER.error(Utils.getFullStackTrace(mue)); - throw new RuntimeException(mue); } catch (URISyntaxException use) { LOGGER.error("URI parse error {}", use.getMessage()); LOGGER.error(Utils.getFullStackTrace(use)); @@ -156,29 +249,78 @@ public static SdkHttpFullRequest getApiRequest(String host, String stage, String return apiRequest; } - public static SdkHttpFullRequest signApiRequest(SdkHttpFullRequest apiRequest, String assumedRole, String context) { - Aws4SignerParams sigV4Params = Aws4SignerParams.builder() + protected String getClientCredentialsBearerToken(AppClient appClient) { + // If we've been called within the access token's expiry period, just return the cached copy + Map token = getCachedClientCredentials(appClient.getClientId()); + if (token == null) { + token = executeClientCredentialsGrant(appClient.getTokenEndpointUrl(), appClient.getClientCredentials()); + // Cache this access token until it expires + putCachedClientCredentials(appClient.getClientId(), token); + } + return "Bearer " + token.get("access_token"); + } + + protected Map executeClientCredentialsGrant(URL tokenEndpoint, String clientSecret) { + // POST to the OAuth provider's token endpoint a client_credentials grant + SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder() + .protocol(tokenEndpoint.getProtocol()) + .host(tokenEndpoint.getHost()) + .encodedPath(tokenEndpoint.getPath()) + .method(SdkHttpMethod.POST); + String body = "grant_type=client_credentials"; + requestBuilder.putHeader("Content-Type", "application/x-www-form-urlencoded"); + requestBuilder.putHeader("Authorization", "Basic " + clientSecret); + requestBuilder.contentStreamProvider(() -> new StringInputStream(body)); + + SdkHttpFullRequest clientCredentialsRequest = requestBuilder.build(); + Map clientCredentialsGrant = Utils.fromJson( + executeApiRequest(clientCredentialsRequest), LinkedHashMap.class); + return clientCredentialsGrant; + } + + protected Map getCachedClientCredentials(String key) { + LOGGER.debug(Utils.toJson(CLIENT_CREDENTIALS_CACHE)); + Map cached = CLIENT_CREDENTIALS_CACHE.get(key); + if (cached != null) { + Duration buffer = Duration.ofSeconds(2); + if (Instant.now().plus(buffer).isBefore((Instant) cached.get("expiry"))) { + LOGGER.debug("Client credentials cache hit {}", key); + return (Map) cached.get("token"); + } else { + LOGGER.debug("Cached credentials are expiring < 2s {}", key); + } + } else { + LOGGER.debug("Client credentials cache miss {}", key); + } + return null; + } + + protected void putCachedClientCredentials(String key, Map token) { + LOGGER.debug("Caching client credentials for {} seconds {}", token.get("expires_in"), key); + CLIENT_CREDENTIALS_CACHE.put(key, Map.of( + "expiry", Instant.now().plusSeconds(((Integer) token.get("expires_in")).longValue()), + "token", token) + ); + } + + protected SdkHttpFullRequest signApiRequest(SdkHttpFullRequest apiRequest, String assumedRole, String context) { + return SIG_V4.sign(apiRequest, Aws4SignerParams.builder() .signingName("execute-api") .signingRegion(Region.of(AWS_REGION)) - .awsCredentials(getTemporaryCredentials(assumedRole, context)) - .build(); - SdkHttpFullRequest signedApiRequest = SIG_V4.sign(apiRequest, sigV4Params); - return signedApiRequest; + .awsCredentials(getAwsCredentials(assumedRole, context)) + .build()); } - protected static AwsCredentials getTemporaryCredentials(final String assumedRole, final String context) { - AwsCredentials systemCredentials = null; - - //LOGGER.info("Calling AssumeRole for {}", assumedRole); - // Calling STS here instead of in the constructor so we can name the - // temporary session with the request context for CloudTrail logging + protected AwsCredentials getAwsCredentials(final String assumedRole, final String context) { + // Calling STS here so we can name the temporary session with + // the request context for CloudTrail logging + AwsCredentials awsCredentials; try { AssumeRoleResponse response = sts.assumeRole(request -> request .roleArn(assumedRole) .durationSeconds(900) - .roleSessionName((Utils.isNotBlank(context)) ? context : SAAS_BOOST_ENV) + .roleSessionName(context) ); - //AssumedRoleUser assumedUser = response.assumedRoleUser(); //LOGGER.info("Assumed IAM User {}", assumedUser.arn()); //LOGGER.info("Assumed IAM Role {}", assumedUser.assumedRoleId()); @@ -186,7 +328,7 @@ protected static AwsCredentials getTemporaryCredentials(final String assumedRole // Could use STSAssumeRoleSessionCredentialsProvider here, but this // lambda will timeout before we need to refresh the temporary creds Credentials temporaryCredentials = response.credentials(); - systemCredentials = AwsSessionCredentials.create( + awsCredentials = AwsSessionCredentials.create( temporaryCredentials.accessKeyId(), temporaryCredentials.secretAccessKey(), temporaryCredentials.sessionToken()); @@ -195,11 +337,12 @@ protected static AwsCredentials getTemporaryCredentials(final String assumedRole LOGGER.error(Utils.getFullStackTrace(stsError)); throw stsError; } - return systemCredentials; + return awsCredentials; } - protected static void appendQueryParams(SdkHttpFullRequest.Builder sdkRequestBuilder, URL url) throws URISyntaxException { - List queryParams = URLEncodedUtils.parse(url.toURI(), StandardCharsets.UTF_8); + protected static void appendQueryParams(SdkHttpFullRequest.Builder sdkRequestBuilder, URL url) + throws URISyntaxException { + List queryParams = new URIBuilder(url.toURI()).getQueryParams(); if (queryParams != null) { for (NameValuePair queryParam : queryParams) { sdkRequestBuilder.appendRawQueryParameter(queryParam.getName(), queryParam.getValue()); @@ -214,4 +357,129 @@ protected static void putHeaders(SdkHttpFullRequest.Builder sdkRequestBuilder, M } } } + + private static final class HttpRequest { + private final String protocol; + private final String host; + private final String stage; + private final String resource; + private final SdkHttpMethod method; + private final String body; + private final Map headers; + + private HttpRequest(HttpRequest.Builder builder) { + this.protocol = Utils.isNotBlank(builder.protocol) ? builder.protocol : "https"; + this.host = builder.host; + this.stage = builder.stage; + this.resource = builder.resource; + this.method = builder.method; + this.body = builder.body; + if (builder.headers != null) { + this.headers = Collections.unmodifiableMap(builder.headers); + } else { + this.headers = Collections.unmodifiableMap(Collections.emptyMap()); + } + } + + public static HttpRequest.Builder builder() { + return new HttpRequest.Builder(); + } + + public String getProtocol() { + return protocol; + } + + public String getHost() { + return host; + } + + public String getStage() { + return stage; + } + + public String getResource() { + return resource; + } + + public SdkHttpMethod getMethod() { + return method; + } + + public String getBody() { + return body; + } + + public Map getHeaders() { + return Collections.unmodifiableMap(headers); + } + + public URL toUrl() { + try { + return new URL(protocol, host, "/" + stage + "/" + resource); + } catch (MalformedURLException mue) { + LOGGER.error("URL parse error {}", mue.getMessage()); + LOGGER.error(Utils.getFullStackTrace(mue)); + throw new RuntimeException(mue); + } + } + + public static final class Builder { + + private String protocol; + private String host; + private String stage; + private String resource; + private SdkHttpMethod method; + private String body; + private Map headers; + + private Builder() { + } + + public HttpRequest.Builder protocol(String protocol) { + this.protocol = protocol; + return this; + } + + public HttpRequest.Builder host(String host) { + this.host = host; + return this; + } + + public HttpRequest.Builder stage(String stage) { + this.stage = stage; + return this; + } + + public HttpRequest.Builder resource(String resource) { + if (resource != null && resource.startsWith("/")) { + this.resource = resource.substring(1); + } else { + this.resource = resource; + } + return this; + } + + public HttpRequest.Builder method(String method) { + this.method = SdkHttpMethod.fromValue(method); + return this; + } + + public HttpRequest.Builder body(String body) { + this.body = body; + return this; + } + + public HttpRequest.Builder headers(final Map headers) { + if (headers != null) { + this.headers = Collections.unmodifiableMap(headers); + } + return this; + } + + public HttpRequest build() { + return new HttpRequest(this); + } + } + } } diff --git a/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiRequest.java b/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiRequest.java deleted file mode 100644 index 1ac72c38..00000000 --- a/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiRequest.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; -import software.amazon.awssdk.http.SdkHttpMethod; - -import java.util.Collections; -import java.util.Map; - -@JsonDeserialize(builder = ApiRequest.Builder.class) -public class ApiRequest { - - private String resource; - private final SdkHttpMethod method; - private String body; - private String callback; - private final Map headers; - - private ApiRequest(Builder builder) { - this.resource = builder.resource; - this.method = builder.method; - this.body = builder.body; - this.callback = builder.callback; - if (builder.headers != null) { - this.headers = Collections.unmodifiableMap(builder.headers); - } else { - this.headers = Collections.unmodifiableMap(Collections.EMPTY_MAP); - } - } - - public static Builder builder() { - return new Builder(); - } - - public String getResource() { - return resource; - } - - public SdkHttpMethod getMethod() { - return method; - } - - public String getBody() { - return body; - } - - public String getCallback() { - return callback; - } - - public Map getHeaders() { - return Collections.unmodifiableMap(headers); - } - - @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property] - public static final class Builder { - - private String resource; - private SdkHttpMethod method; - private String body; - private String callback; - private Map headers; - - private Builder() { - } - - public Builder resource(String resource) { - if (resource != null && resource.startsWith("/")) { - this.resource = resource.substring(1); - } else { - this.resource = resource; - } - return this; - } - - public Builder method(String method) { - this.method = SdkHttpMethod.fromValue(method); - return this; - } - - public Builder body(String body) { - this.body = body; - return this; - } - - public Builder callback(String callback) { - this.callback = callback; - return this; - } - - public Builder headers(final Map headers) { - if (headers != null) { - this.headers = Collections.unmodifiableMap(headers); - } - return this; - } - - public ApiRequest build() { - return new ApiRequest(this); - } - } -} diff --git a/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppClient.java b/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppClient.java new file mode 100644 index 00000000..8b56dd25 --- /dev/null +++ b/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppClient.java @@ -0,0 +1,123 @@ +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +@JsonDeserialize(builder = AppClient.Builder.class) +public class AppClient { + + private static final Logger LOGGER = LoggerFactory.getLogger(AppClient.class); + private final String clientId; + private final String clientName; + private final String clientSecret; + private final String tokenEndpoint; + private final String apiEndpoint; + + private AppClient(Builder builder) { + this.clientId = builder.clientId; + this.clientName = builder.clientName; + this.clientSecret = builder.clientSecret; + this.tokenEndpoint = builder.tokenEndpoint; + this.apiEndpoint = builder.apiEndpoint; + } + + public String getClientCredentials() { + // Generate a Base64 secret for HTTP Basic authorization + return new String(Base64.getEncoder().encode((clientId + ":" + clientSecret) + .getBytes(StandardCharsets.UTF_8) + ), StandardCharsets.UTF_8); + } + + public String getClientId() { + return clientId; + } + + public String getClientName() { + return clientName; + } + + public String getClientSecret() { + return clientSecret; + } + + public String getTokenEndpoint() { + return tokenEndpoint; + } + + public URL getTokenEndpointUrl() { + try { + return new URL(getTokenEndpoint()); + } catch (MalformedURLException mue) { + LOGGER.error("URL parse error {}", mue.getMessage()); + LOGGER.error(Utils.getFullStackTrace(mue)); + throw new RuntimeException(mue); + } + } + + public String getApiEndpoint() { + return apiEndpoint; + } + + public URL getApiEndpointUrl() { + try { + return new URL(getApiEndpoint()); + } catch (MalformedURLException mue) { + LOGGER.error("URL parse error {}", mue.getMessage()); + LOGGER.error(Utils.getFullStackTrace(mue)); + throw new RuntimeException(mue); + } + } + + public static Builder builder() { + return new Builder(); + } + + @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property] + public static final class Builder { + + private String clientId; + private String clientName; + private String clientSecret; + private String tokenEndpoint; + private String apiEndpoint; + + private Builder() { + } + + public Builder clientId(String clientId) { + this.clientId = clientId; + return this; + } + + public Builder clientName(String clientName) { + this.clientName = clientName; + return this; + } + + public Builder clientSecret(String clientSecret) { + this.clientSecret = clientSecret; + return this; + } + + public Builder tokenEndpoint(String tokenEndpoint) { + this.tokenEndpoint = tokenEndpoint; + return this; + } + + public Builder apiEndpoint(String apiEndpoint) { + this.apiEndpoint = apiEndpoint; + return this; + } + + public AppClient build() { + return new AppClient(this); + } + } +} diff --git a/layers/apigw-helper/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelperTest.java b/layers/apigw-helper/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelperTest.java index 708eb92e..ab573195 100644 --- a/layers/apigw-helper/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelperTest.java +++ b/layers/apigw-helper/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelperTest.java @@ -16,43 +16,62 @@ package com.amazon.aws.partners.saasfactory.saasboost; -import org.junit.Test; +import org.junit.jupiter.api.Test; import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; import java.net.URL; +import java.time.Instant; import java.util.List; import java.util.Map; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class ApiGatewayHelperTest { @Test public void testAppendQueryParams() throws Exception { - ApiRequest request = ApiRequest.builder() - .resource("settings?setting=SAAS_BOOST_STACK&setting=DOMAIN_NAME") - .method("GET") - .build(); - String protocol = "https"; String host = "xxxxxxxxxx.execute-api.us-east-2.amazonaws.com"; String stage = "v1"; + String method = "GET"; + String resource = "settings?setting=FOO&setting=BAR"; - URL url = new URL(protocol, host, stage + "/" + request.getResource()); + URL url = new URL(protocol, host, stage + "/" + resource); SdkHttpFullRequest.Builder sdkRequestBuilder = SdkHttpFullRequest.builder() - .protocol(protocol) + .protocol("https") .host(host) .encodedPath(url.getPath()) - .method(request.getMethod()); + .method(SdkHttpMethod.fromValue(method)); ApiGatewayHelper.appendQueryParams(sdkRequestBuilder, url); Map> actual = sdkRequestBuilder.rawQueryParameters(); - assertEquals("2 query params with same name", 1, actual.size()); - assertEquals("2 query params with same name", 2, actual.get("setting").size()); - assertTrue("query parameter is named", actual.containsKey("setting")); - assertTrue("multivalue param", actual.get("setting").contains("SAAS_BOOST_STACK")); - assertTrue("multivalue param", actual.get("setting").contains("DOMAIN_NAME")); + assertEquals(1, actual.size(), "2 query params with same name"); + assertEquals(2, actual.get("setting").size(), "2 query params with same name"); + assertTrue(actual.containsKey("setting"), "query parameter is named"); + assertTrue(actual.get("setting").contains("FOO"), "multivalue param"); + assertTrue(actual.get("setting").contains("BAR"), "multivalue param"); + } + + @Test + public void testCachedClientCredentials() { +// Integer expireSeconds = 300; +// Integer buffer = 2; +// Map clientCredentials = Map.of( +// "access_token", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + +// ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ" + +// ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", +// "expires_in", expireSeconds, +// "token_type", "Bearer" +// ); +// ApiGatewayHelper api = ApiGatewayHelper.builder().host("").stage("").build(); +// Instant now = Instant.now(); +// Instant expires = now.plusSeconds(expireSeconds); +// api.putCachedClientCredentials("foo", clientCredentials); +// Map cached = api.getCachedClientCredentials("foo"); +// assertNotNull(cached); +// assertEquals(cached.get("access_token"), clientCredentials.get("access_token")); } } diff --git a/layers/cloudformation-utils/pom.xml b/layers/cloudformation-utils/pom.xml index ed3c950c..cdc11add 100644 --- a/layers/cloudformation-utils/pom.xml +++ b/layers/cloudformation-utils/pom.xml @@ -33,6 +33,7 @@ limitations under the License. + ${project.basedir}/../.. 0 diff --git a/layers/cloudformation-utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CloudFormationResponse.java b/layers/cloudformation-utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CloudFormationResponse.java index a6116875..4b1db65f 100644 --- a/layers/cloudformation-utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CloudFormationResponse.java +++ b/layers/cloudformation-utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CloudFormationResponse.java @@ -79,6 +79,7 @@ protected static void send(String responseUrl, String responseBody) { LOGGER.info("curl -H 'Content-Type: \"\"' -X PUT -d '" + responseBody + "' \"" + responseUrl + "\""); HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) .uri(URI.create(responseUrl)) .setHeader("Content-Type", "") .PUT(HttpRequest.BodyPublishers.ofString(responseBody, StandardCharsets.UTF_8)) diff --git a/layers/cloudformation-utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CloudFormationEventDeserializerTest.java b/layers/cloudformation-utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CloudFormationEventDeserializerTest.java index f420e845..08367c2e 100644 --- a/layers/cloudformation-utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CloudFormationEventDeserializerTest.java +++ b/layers/cloudformation-utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CloudFormationEventDeserializerTest.java @@ -16,13 +16,13 @@ package com.amazon.aws.partners.saasfactory.saasboost; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class CloudFormationEventDeserializerTest { diff --git a/functions/workload-deploy/pom.xml b/layers/keycloak-helper/pom.xml similarity index 65% rename from functions/workload-deploy/pom.xml rename to layers/keycloak-helper/pom.xml index e7e71b2a..a5f2bbc7 100644 --- a/functions/workload-deploy/pom.xml +++ b/layers/keycloak-helper/pom.xml @@ -19,10 +19,10 @@ limitations under the License. 4.0.0 com.amazon.aws.partners.saasfactory.saasboost - saasboost-functions + saasboost-layers 1.0.0 - WorkloadDeploy + KeycloakHelper 1.0.0 jar @@ -33,6 +33,7 @@ limitations under the License. + ${project.basedir}/../.. 0 @@ -46,15 +47,17 @@ limitations under the License. org.apache.maven.plugins maven-surefire-plugin + + + us-east-1 + test + + org.apache.maven.plugins maven-assembly-plugin - - io.github.git-commit-id - git-commit-id-maven-plugin - @@ -67,42 +70,33 @@ limitations under the License. provided - com.amazon.aws.partners.saasfactory.saasboost - ApiGatewayHelper - 1.0.0 - - provided - - - software.amazon.awssdk - s3 - ${aws.java.sdk.version} + org.keycloak + keycloak-admin-client + 19.0.3 + - software.amazon.awssdk - netty-nio-client + org.jboss.resteasy + resteasy-client - software.amazon.awssdk - apache-client + org.jboss.resteasy + resteasy-multipart-provider - - - - software.amazon.awssdk - codepipeline - ${aws.java.sdk.version} - - software.amazon.awssdk - netty-nio-client + org.jboss.resteasy + resteasy-jackson2-provider - software.amazon.awssdk - apache-client + org.jboss.resteasy + resteasy-jaxb-provider + + org.jboss.logging + jboss-logging + 3.3.2.Final +
    - diff --git a/layers/keycloak-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakAdminApi.java b/layers/keycloak-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakAdminApi.java new file mode 100644 index 00000000..c86d6422 --- /dev/null +++ b/layers/keycloak-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakAdminApi.java @@ -0,0 +1,894 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost.keycloak; + +import com.amazon.aws.partners.saasfactory.saasboost.Utils; +import org.keycloak.representations.idm.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.*; + +public class KeycloakAdminApi { + + private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAdminApi.class); + private final HttpClient httpClient = HttpClient.newBuilder().build(); + private final String keycloakHost; + private Map passwordGrant; + private Instant tokenExpiry; + private String accessToken; + + public KeycloakAdminApi(String keycloakHost, String username, String password) { + this.keycloakHost = keycloakHost; + setPasswordGrant(adminPasswordGrant(username, password)); + } + + public KeycloakAdminApi(String keycloakHost, String bearerToken) { + this.keycloakHost = keycloakHost; + this.accessToken = bearerToken.replaceAll("^[B|b]earer ", ""); + } + + public ClientRepresentation getClient(RealmRepresentation realm, String clientId) { + try { + URI endpoint = new URI(keycloakHost + "/admin/realms/" + + realm.getRealm() + "/clients" + + "?search=true&clientId=" + URLEncoder.encode(clientId, StandardCharsets.UTF_8)); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .GET() + .build(); + LOGGER.debug("Invoking Keycloak realm clients endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (HttpURLConnection.HTTP_OK == response.statusCode()) { + ClientRepresentation[] clients = Utils.fromJson(response.body(), ClientRepresentation[].class); + if (clients != null && clients.length == 1) { + return clients[0]; + } else { + LOGGER.error("Can't find client {}", clientId); + LOGGER.error(response.body()); + throw new RuntimeException("Can't find client " + clientId); + } + } else { + LOGGER.error("Received HTTP status " + response.statusCode()); + LOGGER.error(response.body()); + throw new RuntimeException("Keycloak realm clients failed with HTTP " + + response.statusCode()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public List getClients(RealmRepresentation realm) { + try { + URI endpoint = new URI(keycloakHost + "/admin/realms/" + + realm.getRealm() + "/clients"); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .GET() + .build(); + LOGGER.debug("Invoking Keycloak realm clients endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (HttpURLConnection.HTTP_OK == response.statusCode()) { + ClientRepresentation[] clients = Utils.fromJson(response.body(), ClientRepresentation[].class); + if (clients != null) { + return new ArrayList<>(Arrays.asList(clients)); + } else { + LOGGER.error("Can't parse realm clients response {}", response.body()); + throw new RuntimeException("Invalid response from " + request.uri()); + } + } else { + LOGGER.error("Received HTTP status " + response.statusCode()); + LOGGER.error(response.body()); + throw new RuntimeException("Keycloak realm clients failed with HTTP " + + response.statusCode()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public ClientRepresentation createClient(RealmRepresentation realm, ClientRepresentation client) { + try { + URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm() + "/clients"); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(Utils.toJson(client))) + .build(); + LOGGER.debug("Invoking Keycloak client create endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() == HttpURLConnection.HTTP_CREATED) { + LOGGER.debug("Created client {} {}", client.getName(), client.getClientId()); + return getClient(realm, client.getClientId()); + } else { + LOGGER.error("Expected HTTP_CREATED ({}) from client create, but got {}", + HttpURLConnection.HTTP_CREATED, response.statusCode()); + throw new RuntimeException("Unexpected error while creating client: " + client.getClientId()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public ClientRepresentation updateClient(RealmRepresentation realm, ClientRepresentation client) { + try { + URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm() + + "/clients/" + client.getId() + ); + String body = Utils.toJson(client); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .PUT(HttpRequest.BodyPublishers.ofString(body)) + .build(); + LOGGER.debug("Invoking Keycloak update client endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() == HttpURLConnection.HTTP_NO_CONTENT) { + LOGGER.info("Updated client {} {}", client.getName(), client.getClientId()); + return getClient(realm, client.getClientId()); + } else { + LOGGER.error("Expected HTTP_NO_CONTENT ({}) from update client, but got {}", + HttpURLConnection.HTTP_NO_CONTENT, response.statusCode()); + throw new RuntimeException("Unexpected error while updating client: " + response.body()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public ClientScopeRepresentation getClientScope(RealmRepresentation realm, String scopeName) { + return getClientScopes(realm).stream() + .filter(scope -> scopeName.equals(scope.getName())) + .findFirst() + .orElseThrow(() -> new RuntimeException("Can't find client scope " + scopeName)); + } + + public List getClientScopes(RealmRepresentation realm) { + try { + URI endpoint = new URI(keycloakHost + "/admin" + + "/realms/" + realm.getRealm() + "/client-scopes/" + ); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .GET() + .build(); + LOGGER.debug("Invoking Keycloak realm client scopes endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (HttpURLConnection.HTTP_OK == response.statusCode()) { + ClientScopeRepresentation[] scopes = Utils.fromJson(response.body(), + ClientScopeRepresentation[].class); + if (scopes != null) { + return new ArrayList<>(Arrays.asList(scopes)); + } else { + LOGGER.error("Can't parse realm client scopes response {}", response.body()); + throw new RuntimeException("Invalid response from " + request.uri()); + } + } else { + LOGGER.error("Received HTTP status " + response.statusCode()); + LOGGER.error(response.body()); + throw new RuntimeException("Keycloak realm client scopes failed with HTTP " + + response.statusCode()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public ProtocolMapperRepresentation getClientScopeProtocolMapper(RealmRepresentation realm, + ClientScopeRepresentation clientScope, + String protocolMapper) { + return getClientScopeProtocolMappers(realm, clientScope).stream() + .filter(mapper -> protocolMapper.equals(mapper.getName())) + .findFirst() + .orElseThrow(() -> new RuntimeException("Can't find protocol mapper " + protocolMapper)); + } + + public List getClientScopeProtocolMappers(RealmRepresentation realm, + ClientScopeRepresentation clientScope) { + try { + URI endpoint = new URI(keycloakHost + "/admin" + + "/realms/" + realm.getRealm() + + "/client-scopes/" + clientScope.getId() + + "/protocol-mappers/models" + ); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .GET() + .build(); + LOGGER.debug("Invoking Keycloak realm client scope protocol mappers endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (HttpURLConnection.HTTP_OK == response.statusCode()) { + ProtocolMapperRepresentation[] protocolMappers = Utils.fromJson(response.body(), + ProtocolMapperRepresentation[].class); + if (protocolMappers != null) { + return new ArrayList<>(Arrays.asList(protocolMappers)); + } else { + LOGGER.error("Can't parse protocol mappers response {}", response.body()); + throw new RuntimeException("Invalid response from " + request.uri()); + } + } else { + LOGGER.error("Received HTTP status " + response.statusCode()); + LOGGER.error(response.body()); + throw new RuntimeException("Keycloak realm client scope protocol mappers failed with HTTP " + + response.statusCode()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public ClientRepresentation addClientProtocolMapperModel(RealmRepresentation realm, ClientRepresentation client, + ProtocolMapperRepresentation model) { + try { + URI endpoint = new URI(keycloakHost + "/admin" + + "/realms/" + realm.getRealm() + + "/clients/" + client.getId() + + "/protocol-mappers" + + "/add-models" + ); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(Utils.toJson(List.of(model)))) + .build(); + LOGGER.debug("Invoking Keycloak client protocol mapper model {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() == HttpURLConnection.HTTP_NO_CONTENT) { + // let's return the client we just updated + LOGGER.debug("Created protocol mapper model {} {}", client.getName(), model.getName()); + return getClient(realm, client.getClientId()); + } else { + LOGGER.error("Expected HTTP_CREATED ({}) from protocol mapper model create, but got {}", + HttpURLConnection.HTTP_CREATED, response.statusCode()); + throw new RuntimeException("Unexpected error while creating protocol mapper model: " + model.getName()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public RoleRepresentation getRole(RealmRepresentation realm, String roleName) { + try { + URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm() + "/roles/" + + URLEncoder.encode(roleName, StandardCharsets.UTF_8)); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .GET() + .build(); + LOGGER.debug("Invoking Keycloak roles endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() == HttpURLConnection.HTTP_OK) { + LOGGER.debug("Found role: {}", response.body()); + return Utils.fromJson(response.body(), RoleRepresentation.class); + } else { + LOGGER.error("Expected HTTP_OK ({}) from get role by name, but got {}", + HttpURLConnection.HTTP_OK, response.statusCode()); + throw new RuntimeException("Unexpected error while getting role by name: " + response.body()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public RoleRepresentation createRole(RealmRepresentation realm, RoleRepresentation role) { + try { + URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm() + "/roles"); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(Utils.toJson(role))) + .build(); + LOGGER.debug("Invoking Keycloak role create endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() == HttpURLConnection.HTTP_CREATED) { + // let's return the role we just created + LOGGER.debug("Created role {}", role.getName()); + return getRole(realm, role.getName()); + } else { + LOGGER.error("Expected HTTP_CREATED ({}) from role create, but got {}", + HttpURLConnection.HTTP_CREATED, response.statusCode()); + throw new RuntimeException("Unexpected error while creating role: " + role.getName()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public GroupRepresentation createGroup(RealmRepresentation realm, GroupRepresentation group) { + try { + URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm() + "/groups"); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(Utils.toJson(group))) + .build(); + LOGGER.debug("Invoking Keycloak group create endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() == HttpURLConnection.HTTP_CREATED) { + // let's return the group we just created + LOGGER.debug("Created group {}", group.getName()); + return getGroup(realm, group.getName()); + } else { + LOGGER.error("Expected HTTP_CREATED ({}) from group create, but got {}", + HttpURLConnection.HTTP_CREATED, response.statusCode()); + throw new RuntimeException("Unexpected error while creating group: " + group.getName()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public GroupRepresentation getGroup(RealmRepresentation realm, String groupName) { + return getGroups(realm).stream() + .filter(group -> groupName.equals(group.getName())) + .findFirst() + .orElseThrow(() -> new RuntimeException("Can't find group " + groupName)); + } + + public List getGroups(RealmRepresentation realm) { + try { + URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm() + "/groups"); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .GET() + .build(); + LOGGER.debug("Invoking Keycloak list groups endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() == HttpURLConnection.HTTP_OK) { + LOGGER.debug("Found groups: {}", response.body()); + GroupRepresentation[] groups = Utils.fromJson(response.body(), GroupRepresentation[].class); + if (groups != null) { + return new ArrayList<>(Arrays.asList(groups)); + } else { + LOGGER.error("Can't parse groups response {}", response.body()); + throw new RuntimeException("Invalid response from " + request.uri()); + } + } else { + LOGGER.error("Expected HTTP_OK ({}) from list groups, but got {}", + HttpURLConnection.HTTP_OK, response.statusCode()); + throw new RuntimeException("Unexpected error while listing groups: " + response.body()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public RoleRepresentation getClientRole(RealmRepresentation realm, ClientRepresentation client, String roleName) { + return getClientRoles(realm, client).stream() + .filter(role -> roleName.equals(role.getName())) + .findFirst() + .orElseThrow(() -> new RuntimeException("Can't find client role " + roleName + + " for client " + client.getClientId())); + } + + public List getClientRoles(RealmRepresentation realm, ClientRepresentation client) { + try { + URI endpoint = new URI(keycloakHost + "/admin" + + "/realms/" + realm.getRealm() + + "/clients/" + client.getId() + + "/roles" + ); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .GET() + .build(); + LOGGER.debug("Invoking Keycloak realm client roles endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (HttpURLConnection.HTTP_OK == response.statusCode()) { + RoleRepresentation[] roles = Utils.fromJson(response.body(), RoleRepresentation[].class); + if (roles != null) { + return new ArrayList<>(Arrays.asList(roles)); + } else { + LOGGER.error("Can't parse realm client roles response {}", response.body()); + throw new RuntimeException("Invalid response from " + request.uri()); + } + } else { + LOGGER.error("Received HTTP status " + response.statusCode()); + LOGGER.error(response.body()); + throw new RuntimeException("Keycloak realm client roles failed with HTTP " + + response.statusCode()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public List getUsers(RealmRepresentation realm) { + try { + URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm() + "/users"); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .GET() + .build(); + LOGGER.debug("Invoking Keycloak list groups endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() == HttpURLConnection.HTTP_OK) { + LOGGER.debug("Found users: {}", response.body()); + UserRepresentation[] users = Utils.fromJson(response.body(), UserRepresentation[].class); + if (users != null) { + return new ArrayList<>(Arrays.asList(users)); + } else { + LOGGER.error("Can't parse users response {}", response.body()); + throw new RuntimeException("Invalid response from " + request.uri()); + } + } else { + LOGGER.error("Expected HTTP_OK ({}) from list users, but got {}", + HttpURLConnection.HTTP_OK, response.statusCode()); + throw new RuntimeException("Unexpected error while listing users: " + response.body()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public UserRepresentation getUser(RealmRepresentation realm, String username) { + try { + URI endpoint = new URI(keycloakHost + "/admin/realms/" + + realm.getRealm() + "/users?exact=true&username=" + + URLEncoder.encode(username, StandardCharsets.UTF_8)); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .GET() + .build(); + LOGGER.debug("Invoking Keycloak realm users endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (HttpURLConnection.HTTP_OK == response.statusCode()) { + UserRepresentation[] users = Utils.fromJson(response.body(), UserRepresentation[].class); + if (users != null) { + if (users.length == 1) { + return users[0]; + } else { + LOGGER.error("Can't find user {}", username); + LOGGER.error(response.body()); + throw new RuntimeException("Can't find user " + username); + } + } else { + LOGGER.error("Can't parse realm users response {}", response.body()); + throw new RuntimeException("Invalid response from " + request.uri()); + } + } else { + LOGGER.error("Received HTTP status " + response.statusCode()); + LOGGER.error(response.body()); + throw new RuntimeException("Keycloak realm users failed with HTTP " + + response.statusCode()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public UserRepresentation createUser(RealmRepresentation realm, UserRepresentation user) { + try { + URI endpoint = new URI(keycloakHost + "/admin" + + "/realms/" + realm.getRealm() + + "/users" + ); + String body = Utils.toJson(user); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + LOGGER.debug("Invoking Keycloak realm users endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (HttpURLConnection.HTTP_CREATED == response.statusCode()) { + LOGGER.info("Created user " + user.getUsername()); + return getUser(realm, user.getUsername()); + } else { + LOGGER.error("Expected HTTP_CREATED ({}) from create user, but got {}", + HttpURLConnection.HTTP_CREATED, response.statusCode()); + throw new RuntimeException("Keycloak users " + user.getUsername() + + " failed with HTTP " + response.statusCode()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public UserRepresentation updateUser(RealmRepresentation realm, UserRepresentation user) { + try { + URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm() + + "/users/" + user.getId() + ); + String body = Utils.toJson(user); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .PUT(HttpRequest.BodyPublishers.ofString(body)) + .build(); + LOGGER.debug("Invoking Keycloak update user scope endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() == HttpURLConnection.HTTP_NO_CONTENT) { + LOGGER.info("Updated user {}", user.getUsername()); + return getUser(realm, user.getUsername()); + } else { + LOGGER.error("Expected HTTP_NO_CONTENT ({}) from update user, but got {}", + HttpURLConnection.HTTP_NO_CONTENT, response.statusCode()); + throw new RuntimeException("Unexpected error while updating user: " + response.body()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public void deleteUser(RealmRepresentation realm, UserRepresentation user) { + try { + URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm() + + "/users/" + user.getId() + ); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .DELETE() + .build(); + LOGGER.debug("Invoking Keycloak delete user scope endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() == HttpURLConnection.HTTP_NO_CONTENT) { + LOGGER.info("Deleted user {}", user.getUsername()); + } else { + LOGGER.error("Expected HTTP_NO_CONTENT ({}) from delete user, but got {}", + HttpURLConnection.HTTP_NO_CONTENT, response.statusCode()); + throw new RuntimeException("Unexpected error while deleting user: " + response.body()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public ClientScopeRepresentation createClientScope(RealmRepresentation realm, ClientScopeRepresentation scope) { + try { + URI endpoint = new URI(keycloakHost + "/admin" + + "/realms/" + realm.getRealm() + + "/client-scopes" + ); + String body = Utils.toJson(scope); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + LOGGER.debug("Invoking Keycloak realm client scopes endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (HttpURLConnection.HTTP_CREATED == response.statusCode()) { + LOGGER.info("Created client scope {}", scope.getName()); + return getClientScope(realm, scope.getName()); + } else { + LOGGER.error("Expected HTTP_CREATED ({}) from create client scope, but got {}", + HttpURLConnection.HTTP_CREATED, response.statusCode()); + throw new RuntimeException("Keycloak client scope " + scope.getName() + + " failed with HTTP " + response.statusCode()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public RealmRepresentation getRealm(RealmRepresentation realm) { + try { + URI endpoint = new URI(keycloakHost + "/admin/realms/" + + URLEncoder.encode(realm.getRealm(), StandardCharsets.UTF_8)); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .GET() + .build(); + LOGGER.debug("Invoking Keycloak realms endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() == HttpURLConnection.HTTP_OK) { + LOGGER.debug("Found realm: {}", response.body()); + return Utils.fromJson(response.body(), RealmRepresentation.class); + } else { + LOGGER.error("Expected HTTP_OK ({}) from get realm by name, but got {}", + HttpURLConnection.HTTP_OK, response.statusCode()); + throw new RuntimeException("Unexpected error while getting realm by name: " + response.body()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public RealmRepresentation createRealm(RealmRepresentation realm) { + try { + URI endpoint = new URI(keycloakHost + "/admin/realms"); + String requestBody = Utils.toJson(realm); + //String requestBody = JsonSerialization.writeValueAsString(JsonSerialization.createObjectNode(realm)); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + + LOGGER.debug("Invoking Keycloak realm import endpoint {}", request.uri()); + LOGGER.debug(requestBody); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (HttpURLConnection.HTTP_CREATED == response.statusCode()) { + LOGGER.info("Created realm {}", realm.getRealm()); + return getRealm(realm); + } else { + LOGGER.error("Expected HTTP_CREATED ({}) from create realm, but got {}", + HttpURLConnection.HTTP_CREATED, response.statusCode()); + LOGGER.error(response.body()); + throw new RuntimeException("Keycloak create realm " + realm.getRealm() + + " failed with HTTP " + response.statusCode()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public void createGroupClientRoleMapping(RealmRepresentation realm, GroupRepresentation group, + ClientRepresentation client, RoleRepresentation role) { + try { + URI endpoint = new URI(keycloakHost + "/admin" + + "/realms/" + realm.getRealm() + + "/groups/" + group.getId() + + "/role-mappings" + + "/clients/" + client.getId() + ); + String body = Utils.toJson(List.of(role)); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + LOGGER.debug("Invoking Keycloak group client role mapping endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (HttpURLConnection.HTTP_NO_CONTENT == response.statusCode()) { + LOGGER.info("Mapped role {} to group {} for client {}", + role.getName(), group.getName(), client.getName()); + } else { + throw new RuntimeException("Keycloak clientrole mapping for group failed with HTTP " + + response.statusCode()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public void createGroupRoleMapping(RealmRepresentation realm, GroupRepresentation group, RoleRepresentation role) { + try { + URI endpoint = new URI(keycloakHost + "/admin" + + "/realms/" + realm.getRealm() + + "/groups/" + group.getId() + + "/role-mappings" + + "/realm" + ); + // Just in case + role.setContainerId(realm.getId()); + String body = Utils.toJson(List.of(role)); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + LOGGER.debug("Invoking Keycloak group realm role mapping endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + // The POST to map realm roles to a group returns a 204 instead of a 201 created... + if (HttpURLConnection.HTTP_NO_CONTENT == response.statusCode()) { + LOGGER.info("Mapped role {} to group {}", role.getName(), group.getName()); + } else { + throw new RuntimeException("Keycloak admin user group attachment failed with HTTP " + + response.statusCode()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public void addUserToGroup(RealmRepresentation realm, UserRepresentation user, GroupRepresentation group) { + try { + URI endpoint = new URI(keycloakHost + "/admin" + + "/realms/" + realm.getRealm() + + "/users/" + user.getId() + + "/groups/" + group.getId() + ); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding + .uri(endpoint) + .setHeader("Authorization", "Bearer " + getBearerToken()) + .setHeader("Content-Type", "application/json") + .PUT(HttpRequest.BodyPublishers.noBody()) + .build(); + + LOGGER.debug("Invoking Keycloak user group attach endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + // The POST to map client roles to user returns a 204 instead of a 201 created... + if (HttpURLConnection.HTTP_NO_CONTENT == response.statusCode()) { + LOGGER.info("Added user {} to group {}", user.getUsername(), group.getName()); + } else { + throw new RuntimeException("Keycloak admin user group attachment failed with HTTP " + + response.statusCode()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + private String getBearerToken() { + // Are we using the Keycloak super user password grant and the refreshing + // access token from the admin-cli client, or are we using the access token + // passed through from a user sign in via the admin web app client? + if (passwordGrant != null && passwordGrant.containsKey("access_token")) { + //LOGGER.debug("Using admin-cli client access token"); + if (Instant.now().plus(Duration.ofSeconds(1)).isAfter(tokenExpiry)) { + refreshBearerToken(); + } + return (String) passwordGrant.get("access_token"); + } else if (Utils.isNotBlank(accessToken)) { + //LOGGER.debug("Using provided access token"); + LOGGER.debug(accessToken); + return accessToken; + } else { + throw new IllegalStateException("No bearer token set"); + } + } + + private void refreshBearerToken() { + try { + URI endpoint = new URI(keycloakHost + "/realms/master/protocol/openid-connect/token"); + String body = "grant_type=refresh_token" + + "&client_id=admin-cli" + + "&refresh_token=" + passwordGrant.get("refresh_token"); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding + .uri(endpoint) + .setHeader("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + LOGGER.debug("Invoking Keycloak refresh token endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (HttpURLConnection.HTTP_OK == response.statusCode()) { + setPasswordGrant(Utils.fromJson(response.body(), LinkedHashMap.class)); + } else { + LOGGER.error("Received HTTP status " + response.statusCode()); + LOGGER.error(response.body()); + throw new RuntimeException("Keycloak admin password grant failed HTTP " + response.statusCode()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + + } + + private void setPasswordGrant(Map passwordGrant) { + if (passwordGrant == null || passwordGrant.isEmpty()) { + throw new IllegalArgumentException("passwordGrant required"); + } + this.passwordGrant = passwordGrant; + this.tokenExpiry = Instant.now().plusSeconds(((Integer) passwordGrant.get("expires_in")).longValue()); + } + + private Map adminPasswordGrant(String username, String password) { + try { + URI endpoint = new URI(keycloakHost + "/realms/master/protocol/openid-connect/token"); + String body = "grant_type=password" + + "&client_id=admin-cli" + + "&username=" + URLEncoder.encode(username, StandardCharsets.UTF_8) + + "&password=" + URLEncoder.encode(password, StandardCharsets.UTF_8); + HttpRequest request = HttpRequest.newBuilder() + .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding + .uri(endpoint) + .setHeader("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + LOGGER.debug("Invoking Keycloak password grant endpoint {}", request.uri()); + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (HttpURLConnection.HTTP_OK == response.statusCode()) { + return Utils.fromJson(response.body(), LinkedHashMap.class); + } else { + LOGGER.error("Received HTTP status " + response.statusCode()); + LOGGER.error(response.body()); + throw new RuntimeException("Keycloak admin password grant failed HTTP " + response.statusCode()); + } + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/layers/keycloak-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakUtils.java b/layers/keycloak-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakUtils.java new file mode 100644 index 00000000..60740dc4 --- /dev/null +++ b/layers/keycloak-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakUtils.java @@ -0,0 +1,125 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost.keycloak; + +import com.amazon.aws.partners.saasfactory.saasboost.Utils; +import org.keycloak.representations.idm.*; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class KeycloakUtils { + + private KeycloakUtils() { + } + + public static RealmRepresentation asRealm(String realmName) { + RealmRepresentation realm = new RealmRepresentation(); + realm.setRealm(realmName); + realm.setEnabled(Boolean.TRUE); + return realm; + } + + public static RoleRepresentation asRole(String roleName) { + RoleRepresentation role = new RoleRepresentation(); + role.setName(roleName); + return role; + } + + public static GroupRepresentation asGroup(String groupName) { + GroupRepresentation group = new GroupRepresentation(); + group.setName(groupName); + return group; + } + + public static UserRepresentation asUser(String username, String email, String password) { + UserRepresentation user = new UserRepresentation(); + user.setEnabled(true); + user.setCreatedTimestamp(LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli()); + user.setUsername(username); + user.setEmail(email); + user.setEmailVerified(true); + CredentialRepresentation credentials = new CredentialRepresentation(); + credentials.setType("password"); + credentials.setTemporary(true); + credentials.setValue(password); + user.setCredentials(List.of(credentials)); + user.setRequiredActions(List.of("UPDATE_PASSWORD")); + return user; + } + + public static ClientRepresentation asConfidentialClient(String clientName, String clientId, + String description, List scopes) { + ClientRepresentation client = new ClientRepresentation(); + client.setEnabled(true); + client.setProtocol("openid-connect"); + client.setName(clientName); + client.setClientId(clientId); + client.setDescription(description); + client.setStandardFlowEnabled(false); + client.setDirectAccessGrantsEnabled(false); + client.setImplicitFlowEnabled(false); + client.setServiceAccountsEnabled(true); + client.setPublicClient(false); + client.setAttributes(Map.of("use.refresh.tokens", Boolean.FALSE.toString())); + client.setDefaultClientScopes(scopes); + client.setProtocolMappers(new ArrayList<>()); + return client; + } + + public static ClientRepresentation asPublicClient(String clientName, String clientId, String description, + String redirects) { + ClientRepresentation client = new ClientRepresentation(); + client.setEnabled(true); + client.setProtocol("openid-connect"); + client.setName(clientName); + client.setClientId(clientId); + client.setDescription(description); + client.setStandardFlowEnabled(true); + client.setDirectAccessGrantsEnabled(false); + client.setImplicitFlowEnabled(false); + client.setServiceAccountsEnabled(false); + client.setPublicClient(true); + client.setAttributes(Map.of("pkce.code.challenge.method", "S256")); + client.setRedirectUris(List.of(redirects)); + client.setProtocolMappers(new ArrayList<>()); + return client; + } + + public static ClientScopeRepresentation asClientScope(String scope, String description) { + ClientScopeRepresentation clientScope = new ClientScopeRepresentation(); + clientScope.setName(scope); + clientScope.setDescription(description); + clientScope.setProtocol("openid-connect"); + clientScope.setAttributes(Map.of( + "include.in.token.scope", Boolean.TRUE.toString(), + "display.on.consent.screen", Boolean.FALSE.toString()) + ); + return clientScope; + } + + public static CredentialRepresentation temporaryPassword() { + CredentialRepresentation tempPassword = new CredentialRepresentation(); + tempPassword.setType("password"); + tempPassword.setTemporary(Boolean.TRUE); + tempPassword.setValue(Utils.randomString(12)); + return tempPassword; + } +} diff --git a/functions/system-rest-api-client/src/main/resources/lambda-assembly.xml b/layers/keycloak-helper/src/main/resources/lambda-layer-assembly.xml similarity index 69% rename from functions/system-rest-api-client/src/main/resources/lambda-assembly.xml rename to layers/keycloak-helper/src/main/resources/lambda-layer-assembly.xml index 26364854..2daf79c4 100644 --- a/functions/system-rest-api-client/src/main/resources/lambda-assembly.xml +++ b/layers/keycloak-helper/src/main/resources/lambda-layer-assembly.xml @@ -21,22 +21,11 @@ limitations under the License. zip false - - - - ${project.build.outputDirectory} - - com/amazon/aws/partners/saasfactory/** - log4j2.xml - git.properties - - - - false + java/lib + true true - lib - \ No newline at end of file + diff --git a/layers/keycloak-helper/update.sh b/layers/keycloak-helper/update.sh new file mode 100755 index 00000000..3e04b6ef --- /dev/null +++ b/layers/keycloak-helper/update.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ -z $1 ]; then + echo "Usage: $0 [Lambda Folder]" + exit 2 +fi + +MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') +echo "AWS Region = $MY_AWS_REGION" + +ENVIRONMENT=$1 +LAMBDA_STAGE_FOLDER=$2 +if [ -z $LAMBDA_STAGE_FOLDER ]; then + LAMBDA_STAGE_FOLDER="lambdas" +fi +LAMBDA_CODE=KeycloakHelper-lambda.zip +LAYER_NAME="sb-${ENVIRONMENT}-keycloak-helper" + +#set this for V2 AWS CLI to disable paging +export AWS_PAGER="" + +SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) +echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" +if [ -z $SAAS_BOOST_BUCKET ]; then + echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" + exit 1 +fi + +# Do a fresh build of the project +mvn +if [ $? -ne 0 ]; then + echo "Error building project" + exit 1 +fi + +# And copy it up to S3 +aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ + +# Publish a new version of the layer +PUBLISHED_LAYER=$(aws --region $MY_AWS_REGION lambda publish-layer-version --layer-name "${LAYER_NAME}" --compatible-runtimes java11 --content S3Bucket="${SAAS_BOOST_BUCKET}",S3Key="${LAMBDA_STAGE_FOLDER}/${LAMBDA_CODE}") + +# Use eval to deal with the backticks in the filter expression +eval LAYER_VERSION_ARN=\$\("aws lambda list-layers --query 'Layers[?LayerName==\`${LAYER_NAME}\`].LatestMatchingVersion.LayerVersionArn' --output text"\) +echo "Published new layer = $LAYER_VERSION_ARN" + +# Find all the functions for this SaaS Boost environment that have layers +eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-\`)] | [?Layers != null] | [].FunctionName' --output text"\) +#echo "Updating ${#FUNCTIONS[@]} functions with new layer version" + +for FX in ${FUNCTIONS[@]}; do + # The order of the function's layers must be maintained. Iterate through this function's layers + # and update this layer's ARN with the newly published version. + FOUND=0 + LAYERS="" + for LAYER_ARN in $(aws --region $MY_AWS_REGION lambda get-function --function-name $FX --query 'Configuration.Layers[].Arn' --output text); do + if [[ $LAYER_ARN == *"${LAYER_NAME}"* ]]; then + LAYER_ARN=$LAYER_VERSION_ARN + FOUND=1 + fi + if [ ${#LAYERS} -gt 0 ]; then + LAYERS="${LAYERS} " + fi + LAYERS="${LAYERS}${LAYER_ARN}" + done + if (( $FOUND )); then + eval "aws --region $MY_AWS_REGION lambda update-function-configuration --function-name $FX --layers $LAYERS" + fi +done \ No newline at end of file diff --git a/layers/pom.xml b/layers/pom.xml index ea4ac1b9..fdfeb898 100644 --- a/layers/pom.xml +++ b/layers/pom.xml @@ -14,10 +14,15 @@ pom https://github.com/awslabs/aws-saas-boost - apigw-helper utils - cloudformation-utils + apigw-helper + cloudformation-utils + keycloak-helper + saas-boost-api-client-helper/java + + ${project.basedir}/.. + Apache-2.0 @@ -48,8 +53,12 @@ slf4j-api
    - junit - junit + org.junit.jupiter + junit-jupiter-engine + + + org.junit.jupiter + junit-jupiter-api org.slf4j diff --git a/layers/saas-boost-api-client-helper/.gitignore b/layers/saas-boost-api-client-helper/.gitignore new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/layers/saas-boost-api-client-helper/.gitignore @@ -0,0 +1 @@ +build diff --git a/layers/saas-boost-api-client-helper/build.sh b/layers/saas-boost-api-client-helper/build.sh new file mode 100755 index 00000000..64a8ee9a --- /dev/null +++ b/layers/saas-boost-api-client-helper/build.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +artifact="SaaSBoostApiClientHelper" +package=$artifact-lambda.zip + +if [ -d build ] +then + echo "cleaning existing build dir" + rm -rf build +fi + +mkdir build +cp -a python build + +mvn -q -f java/pom.xml clean package +unzip -q java/target/$package -d build + +cd build +if [ -f $package ] +then + rm -f $package +fi +zip -r $package . +cd .. diff --git a/layers/saas-boost-api-client-helper/java/pom.xml b/layers/saas-boost-api-client-helper/java/pom.xml new file mode 100644 index 00000000..16373267 --- /dev/null +++ b/layers/saas-boost-api-client-helper/java/pom.xml @@ -0,0 +1,255 @@ + + + + 4.0.0 + com.amazon.aws.partners.saasfactory.saasboost + SaaSBoostApiClientHelper + 1.0.0 + jar + + + + Apache-2.0 + http://www.apache.org/licenses/LICENSE-2.0 + + + + + ${project.basedir}/../../.. + UTF-8 + 0 + 3.1.1 + 3.3.0 + 4.7.3.5 + 3.8.1 + 11 + 3.0.0 + 5.9.3 + 1.7.32 + 2.20.120 + 2.13.3 + + + + ${project.artifactId} + clean install + + + org.apache.maven.plugins + maven-compiler-plugin + ${compiler.version} + + ${compiler.java.version} + ${compiler.java.version} + UTF-8 + true + + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire.version} + + + + org.apache.logging.log4j:log4j-slf4j-impl + + ${env.JAVA_HOME}/bin/java + + us-east-1 + test + + + + + org.apache.maven.plugins + maven-assembly-plugin + ${assembly.version} + + false + + src/main/resources/lambda-layer-assembly.xml + + + + + package + + single + + + + + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs.version} + + false + + + + spot-bugs + verify + + check + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle.version} + + UTF-8 + src/main/resources/checkstyle/checkstyle.xml + true + true + true + warning + javadoc + true + false + + + + checkstyle + validate + + check + + + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 2.9 + + + io.github.git-commit-id + git-commit-id-maven-plugin + 5.0.0 + + + get-the-git-infos + + revision + + initialize + + + + true + ${project.build.outputDirectory}/git.properties + + ^git.commit.id.abbrev + ^git.commit.id.describe + ^git.commit.id.describe-short + ^git.closest.tag.name + + full + ${project.basedir}/../../../.git + false + + + true + + + + + + + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-nop + ${slf4j.version} + + + org.apache.httpcomponents.core5 + httpcore5 + 5.2.2 + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + 2.13.4.1 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-joda + ${jackson.version} + + + software.amazon.awssdk + url-connection-client + ${aws.java.sdk.version} + + + software.amazon.awssdk + secretsmanager + ${aws.java.sdk.version} + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + apache-client + + + + + + \ No newline at end of file diff --git a/layers/saas-boost-api-client-helper/java/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppClient.java b/layers/saas-boost-api-client-helper/java/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppClient.java new file mode 100644 index 00000000..1f62372e --- /dev/null +++ b/layers/saas-boost-api-client-helper/java/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppClient.java @@ -0,0 +1,133 @@ +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +@JsonDeserialize(builder = AppClient.Builder.class) +public class AppClient { + + private static final Logger LOGGER = LoggerFactory.getLogger(AppClient.class); + private final String clientId; + private final String clientName; + private final String clientSecret; + private final String tokenEndpoint; + private final String apiEndpoint; + + private AppClient(Builder builder) { + this.clientId = builder.clientId; + this.clientName = builder.clientName; + this.clientSecret = builder.clientSecret; + this.tokenEndpoint = builder.tokenEndpoint; + this.apiEndpoint = builder.apiEndpoint; + } + + public String getClientCredentials() { + // Generate a Base64 secret for HTTP Basic authorization + return new String(Base64.getEncoder().encode((clientId + ":" + clientSecret) + .getBytes(StandardCharsets.UTF_8) + ), StandardCharsets.UTF_8); + } + + public String getApiEndpointProtocol() { + return getApiEndpointUrl().getProtocol(); + } + + public String getApiEndpointHost() { + return getApiEndpointUrl().getHost(); + } + + public String getApiEndpointStage() { + return getApiEndpointUrl().getPath().substring(1); + } + + public String getClientId() { + return clientId; + } + + public String getClientName() { + return clientName; + } + + public String getClientSecret() { + return clientSecret; + } + + public String getTokenEndpoint() { + return tokenEndpoint; + } + + public URL getTokenEndpointUrl() { + try { + return new URL(getTokenEndpoint()); + } catch (MalformedURLException mue) { + LOGGER.error("URL parse error {}", mue.getMessage()); + throw new RuntimeException(mue); + } + } + + public String getApiEndpoint() { + return apiEndpoint; + } + + public URL getApiEndpointUrl() { + try { + return new URL(getApiEndpoint()); + } catch (MalformedURLException mue) { + LOGGER.error("URL parse error {}", mue.getMessage()); + throw new RuntimeException(mue); + } + } + + public static Builder builder() { + return new Builder(); + } + + @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property] + public static final class Builder { + + private String clientId; + private String clientName; + private String clientSecret; + private String tokenEndpoint; + private String apiEndpoint; + + private Builder() { + } + + public Builder clientId(String clientId) { + this.clientId = clientId; + return this; + } + + public Builder clientName(String clientName) { + this.clientName = clientName; + return this; + } + + public Builder clientSecret(String clientSecret) { + this.clientSecret = clientSecret; + return this; + } + + public Builder tokenEndpoint(String tokenEndpoint) { + this.tokenEndpoint = tokenEndpoint; + return this; + } + + public Builder apiEndpoint(String apiEndpoint) { + this.apiEndpoint = apiEndpoint; + return this; + } + + public AppClient build() { + return new AppClient(this); + } + } +} diff --git a/layers/saas-boost-api-client-helper/java/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostApiHelper.java b/layers/saas-boost-api-client-helper/java/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostApiHelper.java new file mode 100644 index 00000000..d0661b5d --- /dev/null +++ b/layers/saas-boost-api-client-helper/java/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostApiHelper.java @@ -0,0 +1,459 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.net.URIBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.exception.SdkServiceException; +import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.core.retry.backoff.BackoffStrategy; +import software.amazon.awssdk.core.retry.conditions.RetryCondition; +import software.amazon.awssdk.http.*; +import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; +import software.amazon.awssdk.utils.StringInputStream; + +import java.io.*; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + +public class SaaSBoostApiHelper { + + private static final Logger LOGGER = LoggerFactory.getLogger(SaaSBoostApiHelper.class); + private static final String AWS_REGION = System.getenv("AWS_REGION"); + private static final DateFormat JAVASCRIPT_ISO8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX'Z'"); + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final SdkHttpClient HTTP_CLIENT = UrlConnectionHttpClient.create(); + + static { + JAVASCRIPT_ISO8601.setTimeZone(TimeZone.getTimeZone("UTC")); + MAPPER.findAndRegisterModules(); + MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + MAPPER.setDateFormat(JAVASCRIPT_ISO8601); + MAPPER.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); + MAPPER.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); + MAPPER.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING); + } + + private final Map> cache = new HashMap<>(); + private final SecretsManagerClient secrets; + private AppClient appClient; + + public SaaSBoostApiHelper(String appClientSecretArn) { + this(new DefaultDependencyFactory(), appClientSecretArn); + } + + // Facilitates testing by being able to mock out the Secrets Manager dependency + public SaaSBoostApiHelper(SaaSBoostApiHelperDependencyFactory init, String appClientSecretArn) { + if (AWS_REGION == null || AWS_REGION.isBlank()) { + throw new IllegalStateException("Missing environment variable AWS_REGION"); + } + this.secrets = init.secrets(); + // Fetch the app client details from SecretsManager + try { + GetSecretValueResponse response = secrets.getSecretValue(request -> request + .secretId(appClientSecretArn) + ); + Map clientDetails = fromJson(response.secretString(), LinkedHashMap.class); + appClient = AppClient.builder() + .clientName(clientDetails.get("client_name")) + .clientId(clientDetails.get("client_id")) + .clientSecret(clientDetails.get("client_secret")) + .tokenEndpoint(clientDetails.get("token_endpoint")) + .apiEndpoint(clientDetails.get("api_endpoint")) + .build(); + } catch (SdkServiceException secretsManagerError) { + LOGGER.error(getFullStackTrace(secretsManagerError)); + throw secretsManagerError; + } + } + + public String authorizedRequest(String method, String resource) { + return authorizedRequest(method, resource, null); + } + + public String authorizedRequest(String method, String resource, String body) { + return executeApiRequest( + toSdkHttpFullRequest(HttpRequest.builder() + .protocol(appClient.getApiEndpointProtocol()) + .host(appClient.getApiEndpointHost()) + .stage(appClient.getApiEndpointStage()) + .headers(Map.of("Authorization", getClientCredentialsBearerToken(appClient))) + .method(method) + .resource(resource) + .body(body) + .build() + ) + ); + } + + public String anonymousRequest(String method, String resource) { + return anonymousRequest(method, resource, null); + } + + public String anonymousRequest(String method, String resource, String body) { + return executeApiRequest( + toSdkHttpFullRequest(HttpRequest.builder() + .protocol(appClient.getApiEndpointProtocol()) + .host(appClient.getApiEndpointHost()) + .stage(appClient.getApiEndpointStage()) + .method(method) + .resource(resource) + .body(body) + .build() + ) + ); + } + + protected String executeApiRequest(SdkHttpFullRequest apiRequest) { + HttpExecuteRequest.Builder requestBuilder = HttpExecuteRequest.builder() + .request(apiRequest); + apiRequest.contentStreamProvider().ifPresent(requestBuilder::contentStreamProvider); + HttpExecuteRequest apiExecuteRequest = requestBuilder.build(); + BufferedReader responseReader = null; + String responseBody; + try { + LOGGER.debug("Executing API Request {} {}", apiExecuteRequest.httpRequest().method(), + apiExecuteRequest.httpRequest().getUri().toString()); + HttpExecuteResponse apiResponse = HTTP_CLIENT.prepareRequest(apiExecuteRequest).call(); + responseReader = new BufferedReader(new InputStreamReader(apiResponse.responseBody().get(), + StandardCharsets.UTF_8)); + responseBody = responseReader.lines().collect(Collectors.joining()); + //LOGGER.debug(responseBody); + if (!apiResponse.httpResponse().isSuccessful()) { + throw new RuntimeException("{\"statusCode\":" + apiResponse.httpResponse().statusCode() + + ", \"message\":\"" + apiResponse.httpResponse().statusText().orElse("") + "\"}"); + } + } catch (IOException ioe) { + LOGGER.error("HTTP Client error {}", ioe.getMessage()); + LOGGER.error(getFullStackTrace(ioe)); + throw new RuntimeException(ioe); + } finally { + if (responseReader != null) { + try { + responseReader.close(); + } catch (IOException ioe) { + // swallow + } + } + } + return responseBody; + } + + protected SdkHttpFullRequest toSdkHttpFullRequest(HttpRequest request) { + SdkHttpFullRequest apiRequest; + try { + URL url = request.toUrl(); + SdkHttpFullRequest.Builder sdkRequestBuilder = SdkHttpFullRequest.builder() + .protocol(request.getProtocol()) + .host(request.getHost()) + .encodedPath(url.getPath()) + .method(request.getMethod()); + appendQueryParams(sdkRequestBuilder, url); + putHeaders(sdkRequestBuilder, request.getHeaders()); + sdkRequestBuilder.putHeader("Content-Type", "application/json; charset=utf-8"); + if (SdkHttpMethod.GET != request.getMethod() && request.getBody() != null) { + sdkRequestBuilder.contentStreamProvider(() -> new StringInputStream(request.getBody())); + } + apiRequest = sdkRequestBuilder.build(); + } catch (URISyntaxException use) { + LOGGER.error("URI parse error {}", use.getMessage()); + LOGGER.error(getFullStackTrace(use)); + throw new RuntimeException(use); + } + return apiRequest; + } + + protected String getClientCredentialsBearerToken(AppClient appClient) { + // If we've been called within the access token's expiry period, just return the cached copy + Map token = getCachedClientCredentials(appClient.getClientId()); + if (token == null) { + token = executeClientCredentialsGrant(appClient.getTokenEndpointUrl(), appClient.getClientCredentials()); + // Cache this access token until it expires + putCachedClientCredentials(appClient.getClientId(), token); + } + return "Bearer " + token.get("access_token"); + } + + protected Map executeClientCredentialsGrant(URL tokenEndpoint, String clientSecret) { + // POST to the OAuth provider's token endpoint a client_credentials grant + SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder() + .protocol(tokenEndpoint.getProtocol()) + .host(tokenEndpoint.getHost()) + .encodedPath(tokenEndpoint.getPath()) + .method(SdkHttpMethod.POST); + String body = "grant_type=client_credentials"; + requestBuilder.putHeader("Content-Type", "application/x-www-form-urlencoded"); + requestBuilder.putHeader("Authorization", "Basic " + clientSecret); + requestBuilder.contentStreamProvider(() -> new StringInputStream(body)); + + SdkHttpFullRequest clientCredentialsRequest = requestBuilder.build(); + Map clientCredentialsGrant = fromJson( + executeApiRequest(clientCredentialsRequest), LinkedHashMap.class); + return clientCredentialsGrant; + } + + protected Map getCachedClientCredentials(String key) { + LOGGER.debug(toJson(cache)); + Map cached = cache.get(key); + if (cached != null) { + Duration buffer = Duration.ofSeconds(2); + if (Instant.now().plus(buffer).isBefore((Instant) cached.get("expiry"))) { + LOGGER.debug("Client credentials cache hit {}", key); + return (Map) cached.get("token"); + } else { + LOGGER.debug("Cached credentials are expiring < 2s {}", key); + } + } else { + LOGGER.debug("Client credentials cache miss {}", key); + } + return null; + } + + protected void putCachedClientCredentials(String key, Map token) { + LOGGER.debug("Caching client credentials for {} seconds {}", token.get("expires_in"), key); + cache.put(key, Map.of( + "expiry", Instant.now().plusSeconds(((Integer) token.get("expires_in")).longValue()), + "token", token) + ); + } + + protected void appendQueryParams(SdkHttpFullRequest.Builder sdkRequestBuilder, URL url) + throws URISyntaxException { + List queryParams = new URIBuilder(url.toURI()).getQueryParams(); + for (NameValuePair queryParam : queryParams) { + sdkRequestBuilder.appendRawQueryParameter(queryParam.getName(), queryParam.getValue()); + } + } + + protected void putHeaders(SdkHttpFullRequest.Builder sdkRequestBuilder, Map headers) { + if (sdkRequestBuilder != null && headers != null) { + for (Map.Entry header : headers.entrySet()) { + sdkRequestBuilder.putHeader(header.getKey(), header.getValue()); + } + } + } + + private String toJson(Object obj) { + String json = null; + try { + json = MAPPER.writeValueAsString(obj); + } catch (Exception e) { + LOGGER.error(getFullStackTrace(e)); + } + return json; + } + + private T fromJson(String json, Class serializeTo) { + T object = null; + try { + object = MAPPER.readValue(json, serializeTo); + } catch (Exception e) { + LOGGER.error(getFullStackTrace(e)); + } + return object; + } + + private String getFullStackTrace(Exception e) { + final StringWriter sw = new StringWriter(); + final PrintWriter pw = new PrintWriter(sw, true); + e.printStackTrace(pw); + return sw.getBuffer().toString(); + } + + interface SaaSBoostApiHelperDependencyFactory { + SecretsManagerClient secrets(); + } + + private static final class DefaultDependencyFactory implements SaaSBoostApiHelperDependencyFactory { + + @Override + public SecretsManagerClient secrets() { + Region region = Region.of(AWS_REGION); + String endpoint = "https://" + SecretsManagerClient.SERVICE_NAME + "." + region.id() + + "." + region.metadata().partition().dnsSuffix(); + + return SecretsManagerClient.builder() + .httpClientBuilder(UrlConnectionHttpClient.builder()) + .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) + .region(region) + .endpointOverride(URI.create(endpoint)) + .overrideConfiguration(ClientOverrideConfiguration.builder() + .retryPolicy(RetryPolicy.builder() + .backoffStrategy(BackoffStrategy.defaultStrategy()) + .throttlingBackoffStrategy(BackoffStrategy.defaultThrottlingStrategy()) + .numRetries(SdkDefaultRetrySetting.defaultMaxAttempts()) + .retryCondition(RetryCondition.defaultRetryCondition()) + .build() + ) + .build() + ) + .build(); + } + } + + private static final class HttpRequest { + private final String protocol; + private final String host; + private final String stage; + private final String resource; + private final SdkHttpMethod method; + private final String body; + private final Map headers; + + private HttpRequest(HttpRequest.Builder builder) { + this.protocol = builder.protocol; + this.host = builder.host; + this.stage = builder.stage; + this.resource = builder.resource; + this.method = builder.method; + this.body = builder.body; + if (builder.headers != null) { + this.headers = Collections.unmodifiableMap(builder.headers); + } else { + this.headers = Collections.emptyMap(); + } + } + + public static HttpRequest.Builder builder() { + return new HttpRequest.Builder(); + } + + public String getProtocol() { + return protocol; + } + + public String getHost() { + return host; + } + + public String getStage() { + return stage; + } + + public String getResource() { + return resource; + } + + public SdkHttpMethod getMethod() { + return method; + } + + public String getBody() { + return body; + } + + public Map getHeaders() { + return headers; + } + + public URL toUrl() { + try { + return new URL(protocol, host, "/" + stage + "/" + resource); + } catch (MalformedURLException mue) { + LOGGER.error("URL parse error {}", mue.getMessage()); + throw new RuntimeException(mue); + } + } + + public static final class Builder { + + private String protocol = "https"; + private String host; + private String stage; + private String resource; + private SdkHttpMethod method; + private String body; + private Map headers; + + private Builder() { + } + + public HttpRequest.Builder protocol(String protocol) { + if (protocol == null || protocol.isBlank()) { + throw new IllegalArgumentException("protocol can't be blank"); + } + this.protocol = protocol; + return this; + } + + public HttpRequest.Builder host(String host) { + this.host = host; + return this; + } + + public HttpRequest.Builder stage(String stage) { + this.stage = stage; + return this; + } + + public HttpRequest.Builder resource(String resource) { + if (resource != null && resource.startsWith("/")) { + this.resource = resource.substring(1); + } else { + this.resource = resource; + } + return this; + } + + public HttpRequest.Builder method(String method) { + this.method = SdkHttpMethod.fromValue(method); + return this; + } + + public HttpRequest.Builder body(String body) { + this.body = body; + return this; + } + + public HttpRequest.Builder headers(final Map headers) { + if (headers != null) { + this.headers = Collections.unmodifiableMap(headers); + } + return this; + } + + public HttpRequest build() { + return new HttpRequest(this); + } + } + } +} \ No newline at end of file diff --git a/layers/saas-boost-api-client-helper/java/src/main/resources/checkstyle/checkstyle.xml b/layers/saas-boost-api-client-helper/java/src/main/resources/checkstyle/checkstyle.xml new file mode 100644 index 00000000..197b52a3 --- /dev/null +++ b/layers/saas-boost-api-client-helper/java/src/main/resources/checkstyle/checkstyle.xml @@ -0,0 +1,319 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/functions/workload-deploy/src/main/resources/lambda-assembly.xml b/layers/saas-boost-api-client-helper/java/src/main/resources/lambda-layer-assembly.xml similarity index 69% rename from functions/workload-deploy/src/main/resources/lambda-assembly.xml rename to layers/saas-boost-api-client-helper/java/src/main/resources/lambda-layer-assembly.xml index 26364854..2daf79c4 100644 --- a/functions/workload-deploy/src/main/resources/lambda-assembly.xml +++ b/layers/saas-boost-api-client-helper/java/src/main/resources/lambda-layer-assembly.xml @@ -21,22 +21,11 @@ limitations under the License. zip false - - - - ${project.build.outputDirectory} - - com/amazon/aws/partners/saasfactory/** - log4j2.xml - git.properties - - - - false + java/lib + true true - lib - \ No newline at end of file + diff --git a/layers/saas-boost-api-client-helper/python/SaaSBoostApiHelper.py b/layers/saas-boost-api-client-helper/python/SaaSBoostApiHelper.py new file mode 100644 index 00000000..5ae43843 --- /dev/null +++ b/layers/saas-boost-api-client-helper/python/SaaSBoostApiHelper.py @@ -0,0 +1,107 @@ +import boto3 +import base64 +import json +import logging +from botocore.exceptions import ClientError +from datetime import datetime, timedelta +from urllib.parse import urlencode +from urllib.request import Request, urlopen +#from urllib.request import HTTPHandler, HTTPSHandler, build_opener, install_opener + +logger = logging.getLogger() + +class SaaSBoostApiHelper: + + __credentials_cache = {} + + def __init__(self, app_client_secret_arn): + # Get the OAuth application client details from the SaaS Boost control + # plane Secrets Manager entry + self.secrets = boto3.client('secretsmanager') + try: + api_secret = self.secrets.get_secret_value(SecretId=app_client_secret_arn) + client_details = json.loads(api_secret['SecretString']) + self.client_name = client_details['client_name'] + self.client_id = client_details['client_id'] + self.client_secret = client_details['client_secret'] + self.token_endpoint = client_details['token_endpoint'] + self.api_endpoint = client_details['api_endpoint'] + logger.debug("Fetched API client details from Secrets Manager") + except ClientError as secrets_manager_error: + logger.error("Error fetching API client secret from SaaS Boost control plane") + logger.error(str(secrets_manager_error)) + raise + + def authorized_request(self, method, resource, body=None): + if not resource.startswith('/'): + resource = '/' + resource + api_request = Request( + url=self.api_endpoint + resource, + method=method, + data=body.encode() if body else None + ) + api_request.add_header('Authorization', self.__bearer_token()) + api_request.add_header('Content-Type', 'application/json') + + #http_handler = HTTPHandler(debuglevel=1) + #https_handler = HTTPSHandler(debuglevel=1) + #opener = build_opener(http_handler, https_handler) + #install_opener(opener) + + with urlopen(api_request) as api_response: + response_data = api_response.read() + if response_data: + return json.loads(response_data.decode()) + else: + return + + def __get_cached_credentials(self): + cached = self.__credentials_cache.get(self.client_id) + if cached: + exp = datetime.fromtimestamp(cached['expiry']) + time_buffer = 2 + if datetime.now() + timedelta(seconds=time_buffer) < exp: + logger.debug(f"Client credentials cache hit {self.client_id}") + return cached + else: + logger.debug(f"Cached credentials expiring in < {time_buffer}s {self.client_id}") + else: + logger.debug(f"Client credentials cache miss {self.client_id}") + return None + + def __put_cached_credentials(self, grant): + self.__credentials_cache[self.client_id] = { + 'expiry': (datetime.now() + timedelta(seconds=grant['expires_in'])).timestamp(), + 'access_token': grant['access_token'] + } + + def __bearer_token(self): + token = self.__get_cached_credentials() + if not token: + token = self.__client_credentials_grant() + self.__put_cached_credentials(token) + return f"Bearer {token['access_token']}" + + def __client_credentials(self): + # Generate a Base64 encoded string of the client credential + return base64.b64encode(f"{self.client_id}:{self.client_secret}".encode()).decode() + + def __client_credentials_grant(self): + # Generate the encoded client secret string + client_secret = self.__client_credentials() + + # POST to the token endpoint a client_credentials grant + token_request = Request( + url=self.token_endpoint, + data=urlencode({"grant_type": "client_credentials"}).encode(), + method='POST' + ) + token_request.add_header('Authorization', 'Basic ' + client_secret) + token_request.add_header('Content-Type', 'application/x-www-form-urlencoded') + + with urlopen(token_request) as token_response: + # {'expires_in': seconds, 'token_type': 'Bearer', 'access_token': jwt} + grant = json.loads(token_response.read().decode()) + logger.debug(grant) + return grant + diff --git a/layers/saas-boost-api-client-helper/update.sh b/layers/saas-boost-api-client-helper/update.sh new file mode 100755 index 00000000..dedc7384 --- /dev/null +++ b/layers/saas-boost-api-client-helper/update.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ -z $1 ]; then + echo "Usage: $0 [Lambda Folder]" + exit 2 +fi + +MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') +echo "AWS Region = $MY_AWS_REGION" + +ENVIRONMENT=$1 +LAMBDA_STAGE_FOLDER=$2 +if [ -z $LAMBDA_STAGE_FOLDER ]; then + LAMBDA_STAGE_FOLDER="lambdas" +fi +LAMBDA_CODE=SaaSBoostApiClientHelper-lambda.zip +LAYER_NAME="sb-${ENVIRONMENT}-api-client-helper" + +#set this for V2 AWS CLI to disable paging +export AWS_PAGER="" + +SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) +echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" +if [ -z $SAAS_BOOST_BUCKET ]; then + echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" + exit 1 +fi + +# Do a fresh build of the project +sh build.sh +if [ $? -ne 0 ]; then + echo "Error building project" + exit 1 +fi + +# And copy it up to S3 +aws s3 cp build/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ + +# Publish a new version of the layer +PUBLISHED_LAYER=$(aws --region $MY_AWS_REGION lambda publish-layer-version --layer-name "${LAYER_NAME}" --compatible-runtimes java21 java17 java11 python3.12 python3.11 python3.10 python3.9 python3.8 --content S3Bucket="${SAAS_BOOST_BUCKET}",S3Key="${LAMBDA_STAGE_FOLDER}/${LAMBDA_CODE}") + +# Use eval to deal with the backticks in the filter expression +eval LAYER_VERSION_ARN=\$\("aws lambda list-layers --query 'Layers[?LayerName==\`${LAYER_NAME}\`].LatestMatchingVersion.LayerVersionArn' --output text"\) +echo "Published new layer = $LAYER_VERSION_ARN" + +# Find all the functions for this SaaS Boost environment that have layers +eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-\`)] | [?Layers != null] | [].FunctionName' --output text"\) +FUNCTIONS=($FUNCTIONS) +#echo "Updating ${#FUNCTIONS[@]} functions with new layer version" + +for FX in ${FUNCTIONS[@]}; do + # The order of the function's layers must be maintained. Iterate through this function's layers + # and update this layer's ARN with the newly published version. + FOUND=0 + LAYERS="" + for LAYER_ARN in $(aws --region $MY_AWS_REGION lambda get-function --function-name $FX --query 'Configuration.Layers[].Arn' --output text); do + if [[ $LAYER_ARN == *"${LAYER_NAME}"* ]]; then + LAYER_ARN=$LAYER_VERSION_ARN + FOUND=1 + fi + if [ ${#LAYERS} -gt 0 ]; then + LAYERS="${LAYERS} " + fi + LAYERS="${LAYERS}${LAYER_ARN}" + done + if (( $FOUND )); then + eval "aws --region $MY_AWS_REGION lambda update-function-configuration --function-name $FX --layers $LAYERS" + fi +done diff --git a/layers/utils/pom.xml b/layers/utils/pom.xml index b7bd4189..275862b9 100644 --- a/layers/utils/pom.xml +++ b/layers/utils/pom.xml @@ -33,7 +33,9 @@ limitations under the License. + ${project.basedir}/../.. 4 + 2.14.2 @@ -58,27 +60,27 @@ limitations under the License. com.fasterxml.jackson.core jackson-core - 2.13.3 + ${jackson.version} com.fasterxml.jackson.core jackson-annotations - 2.13.3 + ${jackson.version} com.fasterxml.jackson.core jackson-databind - 2.13.4.1 + ${jackson.version} com.fasterxml.jackson.datatype jackson-datatype-jsr310 - 2.13.3 + ${jackson.version} com.fasterxml.jackson.datatype jackson-datatype-joda - 2.13.3 + ${jackson.version} software.amazon.awssdk @@ -96,5 +98,15 @@ limitations under the License. ${aws.java.sdk.version} provided + + com.amazonaws + aws-lambda-java-core + provided + + + com.amazonaws + aws-lambda-java-events + provided +
    diff --git a/layers/utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Utils.java b/layers/utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Utils.java index 3b09053e..e2da6d5a 100644 --- a/layers/utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Utils.java +++ b/layers/utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Utils.java @@ -16,15 +16,20 @@ package com.amazon.aws.partners.saasfactory.saasboost; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.core.io.JsonStringEncoder; import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder; import software.amazon.awssdk.awscore.client.builder.AwsSyncClientBuilder; @@ -34,6 +39,7 @@ import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.core.retry.backoff.BackoffStrategy; import software.amazon.awssdk.core.retry.conditions.RetryCondition; +import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.partitionmetadata.AwsCnPartitionMetadata; @@ -68,12 +74,15 @@ public class Utils { MAPPER.setDateFormat(JAVASCRIPT_ISO8601); MAPPER.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); MAPPER.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); MAPPER.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING); } - static final char[] LOWERCASE_LETTERS = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; - static final char[] UPPERCASE_LETTERS = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; + static final char[] LOWERCASE_LETTERS = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; + static final char[] UPPERCASE_LETTERS = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; static final char[] NUMBERS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; static final char[] SYMBOLS = {'!', '#', '$', '%', '&', '*', '+', '-', '.', ':', '=', '?', '^', '_'}; @@ -95,7 +104,8 @@ public static String unescapeJson(String quotedJson) { char escapedCharacter = quotedJson.charAt(index); index++; - if (escapedCharacter == '"' || escapedCharacter == '\\' || escapedCharacter == '/' || escapedCharacter == '\'') { + if (escapedCharacter == '"' || escapedCharacter == '\\' || escapedCharacter == '/' + || escapedCharacter == '\'') { // If the character after the backslash is another slash or a quote // then add it to the JSON string we're building. Normal use case is // that the next character should be a double quote mark. @@ -173,6 +183,10 @@ public static T fromJson(InputStream json, Class serializeTo) { return object; } + public static Map asMap(Object obj) { + return fromJson(toJson(obj), LinkedHashMap.class); + } + public static boolean isChinaRegion(String region) { return isChinaRegion(Region.of(region)); } @@ -197,7 +211,19 @@ public static String endpointSuffix(Region region) { return region.metadata().partition().dnsSuffix(); } - public static & AwsClientBuilder, C> C sdkClient(AwsSyncClientBuilder builder, String service) { + public static & AwsClientBuilder, C> C sdkClient( + AwsSyncClientBuilder builder, String service) { + return sdkClient(builder, service, UrlConnectionHttpClient.builder()); + } + + public static & AwsClientBuilder, C> C sdkClient( + AwsSyncClientBuilder builder, String service, SdkHttpClient.Builder httpClientBuilder) { + return sdkClient(builder, service, httpClientBuilder, EnvironmentVariableCredentialsProvider.create()); + } + + public static & AwsClientBuilder, C> C sdkClient( + AwsSyncClientBuilder builder, String service, SdkHttpClient.Builder httpClientBuilder, + AwsCredentialsProvider credentialsProvider) { if (Utils.isBlank(System.getenv("AWS_REGION"))) { throw new IllegalStateException("Missing required environment variable AWS_REGION"); } @@ -231,12 +257,11 @@ public static & AwsClientBuilder, C> region = Region.AWS_GLOBAL; endpoint = "https://iam.amazonaws.com"; } - } C client = builder - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) + .httpClientBuilder(httpClientBuilder) + .credentialsProvider(credentialsProvider) .region(region) .endpointOverride(URI.create(endpoint)) .overrideConfiguration(ClientOverrideConfiguration.builder() @@ -398,7 +423,37 @@ public static String randomString(int length, String allowedCharactersRegex) { return String.valueOf(randomCharacters); } - public static String getFullStackTrace(Exception e) { + public static String generatePassword(int passwordLength) { + if (passwordLength < 8) { + throw new IllegalArgumentException("Invalid password length. Minimum of 8 characters is required."); + } + + // Split the classes of characters into separate buckets so we can be sure to use + // the correct amount of each type + final char[][] chars = {UPPERCASE_LETTERS, LOWERCASE_LETTERS, NUMBERS, SYMBOLS}; + Random random = new Random(); + StringBuilder password = new StringBuilder(passwordLength); + + // Randomly select one character from each of the required character types + ArrayList reqCharBucket = new ArrayList<>(3); + reqCharBucket.add(0, 0); + reqCharBucket.add(1, 1); + reqCharBucket.add(2, 2); + reqCharBucket.add(3, 3); + while (!reqCharBucket.isEmpty()) { + Integer ranReqCharBucket = reqCharBucket.remove(random.nextInt(reqCharBucket.size())); + password.append(chars[ranReqCharBucket][random.nextInt(chars[ranReqCharBucket].length)]); + } + + // Fill out the rest of the password with randomly selected characters + for (int i = 0; i < passwordLength - reqCharBucket.size(); i++) { + int charBucket = random.nextInt(chars.length); + password.append(chars[charBucket][random.nextInt(chars[charBucket].length)]); + } + return password.toString(); + } + + public static String getFullStackTrace(Throwable e) { final StringWriter sw = new StringWriter(); final PrintWriter pw = new PrintWriter(sw, true); e.printStackTrace(pw); @@ -409,24 +464,64 @@ public static void logRequestEvent(Map event) { LOGGER.info(toJson(event)); } + public static void logRequestEvent(APIGatewayProxyRequestEvent event) { + LOGGER.info(toJson(event)); + } + + public static boolean warmup(APIGatewayProxyRequestEvent event) { + Map queryParams = event.getQueryStringParameters(); + // Before parsing the request body, look to see if the request was + // ?source=warmup + if (queryParams != null && "warmup".equals(queryParams.get("source"))) { + return true; + } + if (isNotEmpty(event.getBody())) { + try { + // Don't know if request body is an array or an object + // Ignore arrays. We're looking for {"source": "warmup"} + JsonNode json = MAPPER.readTree(event.getBody()); + if (json.isObject()) { + Map body = MAPPER.treeToValue(json, LinkedHashMap.class); + if (body.containsKey("source") && "warmup".equals(body.get("source"))) { + return true; + } + } + } catch (JsonProcessingException jpe) { + // swallow + } + } + return false; + } + public static boolean warmup(Map event) { - boolean warmup = false; + if ("warmup".equals(event.get("source"))) { + // Lambda invocation _not_ through API Gateway + return true; + } if (event.containsKey("queryStringParameters")) { + // Before parsing the request body, look to see if the request was + // ?source=warmup Map queryParams = (Map) event.get("queryStringParameters"); if (queryParams != null && "warmup".equals(queryParams.get("source"))) { - warmup = true; + return true; } - } else if (event.containsKey("body")) { - Map body = Utils.fromJson((String) event.get("body"), HashMap.class); - if (body != null && body.containsKey("source") && "warmup".equals(body.get("source"))) { - warmup = true; - } - } else { - if ("warmup".equals(event.get("source"))) { - warmup = true; + } + if (event.containsKey("body") && isNotEmpty((String) event.get("body"))) { + try { + // Don't know if request body is an array or an object + // Ignore arrays. We're looking for {"source": "warmup"} + JsonNode json = MAPPER.readTree((String) event.get("body")); + if (json.isObject()) { + Map body = MAPPER.treeToValue(json, LinkedHashMap.class); + if (body.containsKey("source") && "warmup".equals(body.get("source"))) { + return true; + } + } + } catch (JsonProcessingException jpe) { + // swallow } } - return warmup; + return false; } public static String version(Class clazz) { diff --git a/layers/utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/GitVersionInfoTest.java b/layers/utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/GitVersionInfoTest.java index 46bb2a34..6a08f6be 100644 --- a/layers/utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/GitVersionInfoTest.java +++ b/layers/utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/GitVersionInfoTest.java @@ -16,13 +16,12 @@ package com.amazon.aws.partners.saasfactory.saasboost; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.Properties; -import org.junit.Before; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.*; public final class GitVersionInfoTest { @@ -37,7 +36,7 @@ public final class GitVersionInfoTest { private Properties properties; - @Before + @BeforeEach public void setup() { properties = new Properties(); properties.setProperty(GitVersionInfo.TAG_NAME_PROPERTY, VALID_TAG); @@ -45,15 +44,19 @@ public void setup() { properties.setProperty(GitVersionInfo.DESCRIPTION_PROPERTY, VALID_DESC); } - @Test(expected = IllegalArgumentException.class) + @Test public void testFromProperties_empty() { properties.clear(); - GitVersionInfo.fromProperties(properties); + assertThrows(IllegalArgumentException.class, () -> { + GitVersionInfo.fromProperties(properties); + }); } - @Test(expected = NullPointerException.class) + @Test public void testFromProperties_null() { - GitVersionInfo.fromProperties(null); + assertThrows(NullPointerException.class, () -> { + GitVersionInfo.fromProperties(null); + }); } @Test diff --git a/layers/utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/UtilsTest.java b/layers/utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/UtilsTest.java index 649e2acc..559a349e 100644 --- a/layers/utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/UtilsTest.java +++ b/layers/utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/UtilsTest.java @@ -16,18 +16,23 @@ package com.amazon.aws.partners.saasfactory.saasboost; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; public class UtilsTest { @Test public void testIsChinaRegion() { - assertFalse("N. Virginia is not in China", Utils.isChinaRegion("us-east-1")); - assertFalse("US Gov Cloud is not in China", Utils.isChinaRegion("us-gov-west-1")); - assertTrue("Beijing is in China", Utils.isChinaRegion("cn-north-1")); - assertTrue("Ningxia is in China", Utils.isChinaRegion("cn-northwest-1")); + assertFalse(Utils.isChinaRegion("us-east-1"), "N. Virginia is not in China"); + assertFalse(Utils.isChinaRegion("us-gov-west-1"), "US Gov Cloud is not in China"); + assertTrue(Utils.isChinaRegion("cn-north-1"), "Beijing is in China"); + assertTrue(Utils.isChinaRegion("cn-northwest-1"), "Ningxia is in China"); } @Test @@ -41,8 +46,9 @@ public void testRandomString() { String randomString = Utils.randomString(1000); for (int ch = 0; ch < randomString.length(); ch++) { String character = String.valueOf(randomString.charAt(ch)); - assertEquals("Character " + character + " is illegal", -1, illegalCharacters.indexOf(character)); - assertTrue("Character " + character + " is legal", legalCharacters.contains(character)); + assertEquals(-1, illegalCharacters.indexOf(character), + "Character " + character + " is illegal"); + assertTrue(legalCharacters.contains(character), "Character " + character + " is legal"); } } @@ -62,4 +68,35 @@ public void testToSnakeCase() { assertEquals("foo_bar_baz", Utils.toSnakeCase("fooBarBaz")); assertEquals("foo_bar_baz", Utils.toSnakeCase("fooBarBAZ")); } + + @Test + public void testCollectionFromJson() { + String json = "[{\"foo\": \"Santa\", \"bar\": \"Claus\"}]"; + MyPojo[] pojoArray = Utils.fromJson(json, MyPojo[].class); + List pojoList = Arrays.asList(pojoArray); + } + + public static class MyPojo { + private String foo; + private String bar; + + public MyPojo() { + } + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public String getBar() { + return bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + } } diff --git a/layers/utils/update.sh b/layers/utils/update.sh index f60cceeb..b54e7eab 100755 --- a/layers/utils/update.sh +++ b/layers/utils/update.sh @@ -62,7 +62,7 @@ eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'F # In case we have multiple environments in the same account/region, this could potentially override the Utils implementation # when one environment is updated from underneath another. This shouldn't be an issue unless the Utils upgrade includes a # change to the isBlank, isEmpty, logRequestEvent, or Utils.toJson functions. -FUNCTIONS=($FUNCTIONS "saas-boost-app-services-macro") +#FUNCTIONS=($FUNCTIONS "saas-boost-app-services-macro") #echo "Updating ${#FUNCTIONS[@]} functions with new layer version" for FX in ${FUNCTIONS[@]}; do @@ -83,4 +83,4 @@ for FX in ${FUNCTIONS[@]}; do if (( $FOUND )); then eval "aws --region $MY_AWS_REGION lambda update-function-configuration --function-name $FX --layers $LAYERS" fi -done \ No newline at end of file +done diff --git a/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/aggregation/StripeBillingPublish.java b/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/aggregation/StripeBillingPublish.java index 90ac2ab6..db21c090 100644 --- a/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/aggregation/StripeBillingPublish.java +++ b/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/aggregation/StripeBillingPublish.java @@ -15,10 +15,10 @@ */ package com.amazon.aws.partners.saasfactory.metering.aggregation; - import com.amazon.aws.partners.saasfactory.metering.common.AggregationEntry; import com.amazon.aws.partners.saasfactory.metering.common.BillingUtils; import com.amazon.aws.partners.saasfactory.metering.common.TenantConfiguration; +import com.amazon.aws.partners.saasfactory.saasboost.ApiGatewayHelper; import com.amazon.aws.partners.saasfactory.saasboost.Utils; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; @@ -36,10 +36,7 @@ import java.io.OutputStream; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import static com.amazon.aws.partners.saasfactory.metering.common.Constants.*; @@ -47,32 +44,22 @@ public class StripeBillingPublish implements RequestStreamHandler { private static final Logger LOGGER = LoggerFactory.getLogger(StripeBillingPublish.class); private final static String TABLE_NAME = System.getenv(TABLE_ENV_VARIABLE); - private static final String API_GATEWAY_HOST = System.getenv("API_GATEWAY_HOST"); - private static final String API_GATEWAY_STAGE = System.getenv("API_GATEWAY_STAGE"); - private static final String API_TRUST_ROLE = System.getenv("API_TRUST_ROLE"); + private static final String API_APP_CLIENT = System.getenv("API_APP_CLIENT"); private final DynamoDbClient ddb; public StripeBillingPublish() { - long startTimeMillis = System.currentTimeMillis(); if (Utils.isBlank(TABLE_NAME)) { throw new IllegalStateException("Missing required environment variable " + TABLE_ENV_VARIABLE); } - if (Utils.isBlank(API_GATEWAY_HOST)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_HOST"); - } - if (Utils.isBlank(API_GATEWAY_STAGE)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_STAGE"); - } - if (Utils.isBlank(API_TRUST_ROLE)) { - throw new IllegalStateException("Missing required environment variable API_TRUST_ROLE"); + if (Utils.isBlank(API_APP_CLIENT)) { + throw new IllegalStateException("Missing required environment variable API_APP_CLIENT"); } // Used by TenantConfiguration if (Utils.isBlank(System.getenv("DYNAMODB_CONFIG_INDEX_NAME"))) { throw new IllegalStateException("Missing required environment variable DYNAMODB_CONFIG_INDEX_NAME"); } LOGGER.info("Version Info: " + Utils.version(this.getClass())); - ddb = Utils.sdkClient(DynamoDbClient.builder(), DynamoDbClient.SERVICE_NAME); - LOGGER.info("Constructor init: {}", System.currentTimeMillis() - startTimeMillis); + this.ddb = Utils.sdkClient(DynamoDbClient.builder(), DynamoDbClient.SERVICE_NAME); } private List getAggregationEntries(String tenantID) { @@ -224,7 +211,8 @@ private void markAggregationRecordAsSubmitted(AggregationEntry aggregationEntry) @Override public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) { - Stripe.apiKey = BillingUtils.getBillingApiKey(API_GATEWAY_HOST, API_GATEWAY_STAGE, API_TRUST_ROLE); + ApiGatewayHelper api = ApiGatewayHelper.clientCredentialsHelper(API_APP_CLIENT); + Stripe.apiKey = BillingUtils.getBillingApiKey(api); LOGGER.info("Fetching tenant IDs in table {}", TABLE_NAME); List tenantConfigurations = TenantConfiguration.getTenantConfigurations(TABLE_NAME, ddb, LOGGER); if (tenantConfigurations == null || tenantConfigurations.isEmpty()) { diff --git a/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/common/BillingUtils.java b/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/common/BillingUtils.java index ad515a16..86a8149c 100644 --- a/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/common/BillingUtils.java +++ b/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/common/BillingUtils.java @@ -17,11 +17,9 @@ package com.amazon.aws.partners.saasfactory.metering.common; import com.amazon.aws.partners.saasfactory.saasboost.ApiGatewayHelper; -import com.amazon.aws.partners.saasfactory.saasboost.ApiRequest; import com.amazon.aws.partners.saasfactory.saasboost.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import software.amazon.awssdk.http.SdkHttpFullRequest; import java.util.HashMap; import java.util.Map; @@ -31,18 +29,11 @@ public final class BillingUtils { private static final Logger LOGGER = LoggerFactory.getLogger(BillingUtils.class); - public static String getBillingApiKey(String apiGatewayHost, String apiGatewayStage, String apiGatewayRole) { + public static String getBillingApiKey(ApiGatewayHelper api) { //invoke SaaS Boost private API to get API Key for Billing String apiKey = null; - ApiRequest billingApiKeySecret = ApiRequest.builder() - .resource("settings/BILLING_API_KEY/secret") - .method("GET") - .build(); - SdkHttpFullRequest apiRequest = ApiGatewayHelper.getApiRequest( - apiGatewayHost, apiGatewayStage, billingApiKeySecret); try { - String responseBody = ApiGatewayHelper.signAndExecuteApiRequest( - apiRequest, apiGatewayRole, "BillingIntegration"); + String responseBody = api.authorizedRequest("GET", "settings/BILLING_API_KEY/secret"); Map setting = Utils.fromJson(responseBody, HashMap.class); if (null == setting) { throw new RuntimeException("responseBody is invalid"); diff --git a/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/onboarding/BillingIntegration.java b/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/onboarding/BillingIntegration.java index fd0dcb4a..13de8768 100644 --- a/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/onboarding/BillingIntegration.java +++ b/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/onboarding/BillingIntegration.java @@ -19,6 +19,7 @@ import com.amazon.aws.partners.saasfactory.metering.common.EventBridgeEvent; import com.amazon.aws.partners.saasfactory.metering.common.MeteredProduct; import com.amazon.aws.partners.saasfactory.metering.common.SubscriptionPlan; +import com.amazon.aws.partners.saasfactory.saasboost.ApiGatewayHelper; import com.amazon.aws.partners.saasfactory.saasboost.Utils; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; @@ -39,31 +40,19 @@ public class BillingIntegration implements RequestHandler event, Context co private void provisionTenantInStripe(String tenantId, String planId) throws StripeException { LOGGER.info("provisionTenantInStripe: Starting..."); - Stripe.apiKey = BillingUtils.getBillingApiKey(API_GATEWAY_HOST, API_GATEWAY_STAGE, API_TRUST_ROLE); + ApiGatewayHelper api = ApiGatewayHelper.clientCredentialsHelper(API_APP_CLIENT); + Stripe.apiKey = BillingUtils.getBillingApiKey(api); if (Utils.isBlank(tenantId)) { throw new RuntimeException("provisionTenantInStripe: No TenantID found in the event detail"); @@ -329,7 +320,8 @@ private void cancelSubscriptionInStripe(String tenantId) throws StripeException LOGGER.info("cancelSubscriptionInStripe: Starting..."); try { - Stripe.apiKey = BillingUtils.getBillingApiKey(API_GATEWAY_HOST, API_GATEWAY_STAGE, API_TRUST_ROLE); + ApiGatewayHelper api = ApiGatewayHelper.clientCredentialsHelper(API_APP_CLIENT); + Stripe.apiKey = BillingUtils.getBillingApiKey(api); } catch (Exception e) { LOGGER.error("No api key found so skipping subscription cancellation"); return; @@ -505,5 +497,6 @@ private void putTenantProductOnboardEvent(String eventBridgeDetail) { public Object handleRequest(EventBridgeEvent eventBridgeEvent, Context context) { return null; } + } diff --git a/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/onboarding/SubscriptionService.java b/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/onboarding/SubscriptionService.java index 8e137305..5f357691 100644 --- a/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/onboarding/SubscriptionService.java +++ b/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/onboarding/SubscriptionService.java @@ -17,6 +17,7 @@ package com.amazon.aws.partners.saasfactory.metering.onboarding; import com.amazon.aws.partners.saasfactory.metering.common.BillingUtils; +import com.amazon.aws.partners.saasfactory.saasboost.ApiGatewayHelper; import com.amazon.aws.partners.saasfactory.saasboost.Utils; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; @@ -37,20 +38,12 @@ public class SubscriptionService { private static final Map CORS = Map.of("Access-Control-Allow-Origin", "*"); private static final Logger LOGGER = LoggerFactory.getLogger(SubscriptionService.class); - private static final String API_GATEWAY_HOST = System.getenv("API_GATEWAY_HOST"); - private static final String API_GATEWAY_STAGE = System.getenv("API_GATEWAY_STAGE"); - private static final String API_TRUST_ROLE = System.getenv("API_TRUST_ROLE"); + private static final String API_APP_CLIENT = System.getenv("API_APP_CLIENT"); public SubscriptionService() { LOGGER.info("Version Info: " + Utils.version(this.getClass())); - if (Utils.isBlank(API_GATEWAY_HOST)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_HOST"); - } - if (Utils.isBlank(API_GATEWAY_STAGE)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_STAGE"); - } - if (Utils.isBlank(API_TRUST_ROLE)) { - throw new IllegalStateException("Missing required environment variable API_TRUST_ROLE"); + if (Utils.isBlank(API_APP_CLIENT)) { + throw new IllegalStateException("Missing required environment variable API_APP_CLIENT"); } } @@ -63,7 +56,8 @@ public APIGatewayProxyResponseEvent getPlans(Map event, Context Utils.logRequestEvent(event); APIGatewayProxyResponseEvent response; - Stripe.apiKey = BillingUtils.getBillingApiKey(API_GATEWAY_HOST, API_GATEWAY_STAGE, API_TRUST_ROLE); + ApiGatewayHelper api = ApiGatewayHelper.clientCredentialsHelper(API_APP_CLIENT); + Stripe.apiKey = BillingUtils.getBillingApiKey(api); if (Stripe.apiKey != null) { try { ArrayNode plans = JsonNodeFactory.instance.arrayNode(); @@ -108,4 +102,5 @@ public APIGatewayProxyResponseEvent getPlans(Map event, Context return response; } + } diff --git a/pom.xml b/pom.xml index 248786ca..8fa6267f 100644 --- a/pom.xml +++ b/pom.xml @@ -15,44 +15,55 @@ https://github.com/awslabs/aws-saas-boost + layers + installer resources/custom-resources functions - installer - layers - metering-billing - metrics-analytics services + ${project.basedir} UTF-8 3.1.1 - 3.1.2 + 3.3.0 3.8.1 - 11 - 4.3.0 - - 2.22.2 + 17 + 4.7.3.5 + 3.0.0 1.5.1 - 1.2.1 - 3.11.0 - 2.17.132 + 1.2.2 + 3.11.2 + 2.20.120 2.17.1 - 4.13.2 + 5.9.3 3.12.4 + 0.8.11 1.7.32 - junit - junit + org.junit.jupiter + junit-jupiter-engine ${junit.version} test + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + com.amazonaws + aws-lambda-java-tests + 1.1.1 + test + org.mockito mockito-core @@ -128,6 +139,11 @@ org.apache.logging.log4j:log4j-slf4j-impl + ${env.JAVA_HOME}/bin/java + + us-east-1 + test + @@ -148,6 +164,42 @@ + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs.version} + + false + + + + spot-bugs + verify + + check + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + + prepare-agent + + + + report + prepare-package + + report + + + + @@ -155,27 +207,14 @@ com.github.spotbugs spotbugs-maven-plugin - ${spotbugs.version} - - false - - - - spot-bugs - verify - - check - - - org.apache.maven.plugins maven-checkstyle-plugin ${checkstyle.version} - UTF-8 - resources/checkstyle/checkstyle.xml + UTF-8 + ${saasboost.rootdir}/resources/checkstyle/checkstyle.xml true true true diff --git a/resources/api-docs/app.js b/resources/api-docs/app.js new file mode 100644 index 00000000..c7d40924 --- /dev/null +++ b/resources/api-docs/app.js @@ -0,0 +1,31 @@ +import express from 'express'; +import serverless from 'serverless-http'; +import { serve, setup } from 'swagger-ui-express'; +import { readFile } from 'fs/promises'; + +const swagger = JSON.parse( + await readFile( + new URL(process.env.LAMBDA_TASK_ROOT + '/swagger.json', import.meta.url) + ) +); + +let applicationPath = process.env.SWAGGER_UI_URL_PATH; +if (!applicationPath) { + applicationPath = "/docs"; +} +if (!applicationPath.startsWith('/')) { + applicationPath = '/' + applicationPath; +} + +const app = express(); +app.use( + applicationPath, + serve, + setup(null, { + swaggerOptions: { + spec: swagger + } + }) +); + +export const handler = serverless(app); diff --git a/resources/api-docs/buildspec_no_post_build.yaml b/resources/api-docs/buildspec_no_post_build.yaml new file mode 100644 index 00000000..6b731dfd --- /dev/null +++ b/resources/api-docs/buildspec_no_post_build.yaml @@ -0,0 +1,16 @@ +version: 0.2 +phases: + pre_build: + commands: + - if [ "$AWS_DEFAULT_REGION" = "cn-northwest-1" ] || [ "$AWS_DEFAULT_REGION" = "cn-north-1" ]; then npm config set registry https://registry.npm.taobao.org; fi + - aws s3 cp s3://$SOURCE_BUCKET/${SOURCE_BUCKET_PREFIX}client/api-docs/src.zip src.zip + - unzip src.zip + - aws s3 cp s3://$SOURCE_BUCKET/${SOURCE_BUCKET_PREFIX}client/api-docs/swagger.json ./resources/api-docs/swagger.json + build: + commands: + - cd ./resources/api-docs + - npm install + - cd ../../ +cache: + paths: + - $CODEBUILD_SRC_DIR/resources/api-docs/node_modules/**/* diff --git a/resources/api-docs/package-lock.json b/resources/api-docs/package-lock.json new file mode 100644 index 00000000..548a31e7 --- /dev/null +++ b/resources/api-docs/package-lock.json @@ -0,0 +1,1311 @@ +{ + "name": "api-docs", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "serverless-http": "^2.6.1", + "swagger-ui-express": "^5.0.0" + } + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.126", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.126.tgz", + "integrity": "sha512-5eh4ffLdGYgGYI1Xr6W5L4IVse4RR7L2ns5OVUXA52nW5GFapIcGMcCzHAIMMOdpcQs3aGVxbvFlJNZH6IpgEQ==", + "optional": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "peer": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "peer": true + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "peer": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "peer": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "peer": true + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "peer": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "peer": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "peer": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "peer": true + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "peer": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "peer": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "peer": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "peer": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "peer": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "peer": true + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "peer": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "peer": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "peer": true + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "peer": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "peer": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "peer": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "peer": true + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "peer": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "peer": true + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "peer": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serverless-http": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/serverless-http/-/serverless-http-2.7.0.tgz", + "integrity": "sha512-iWq0z1X2Xkuvz6wL305uCux/SypbojHlYsB5bzmF5TqoLYsdvMNIoCsgtWjwqWoo3AR2cjw3zAmHN2+U6mF99Q==", + "engines": { + "node": ">=8.0" + }, + "optionalDependencies": { + "@types/aws-lambda": "^8.10.56" + } + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "peer": true, + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "peer": true + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.9.3.tgz", + "integrity": "sha512-/OgHfO96RWXF+p/EOjEnvKNEh94qAG/VHukgmVKh5e6foX9kas1WbjvQnDDj0sSTAMr9MHRBqAWytDcQi0VOrg==" + }, + "node_modules/swagger-ui-express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz", + "integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "peer": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "peer": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "peer": true, + "engines": { + "node": ">= 0.8" + } + } + }, + "dependencies": { + "@types/aws-lambda": { + "version": "8.10.126", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.126.tgz", + "integrity": "sha512-5eh4ffLdGYgGYI1Xr6W5L4IVse4RR7L2ns5OVUXA52nW5GFapIcGMcCzHAIMMOdpcQs3aGVxbvFlJNZH6IpgEQ==", + "optional": true + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "peer": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "peer": true + }, + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "peer": true, + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "peer": true + }, + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "peer": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "peer": true, + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "peer": true + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "peer": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "peer": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "peer": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "peer": true, + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "peer": true + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "peer": true + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "peer": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "peer": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "peer": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "peer": true + }, + "express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "peer": true, + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "peer": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "peer": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "peer": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "peer": true + }, + "get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "peer": true, + "requires": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "peer": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "peer": true, + "requires": { + "get-intrinsic": "^1.2.2" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "peer": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "peer": true + }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "peer": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "peer": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "peer": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "peer": true + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "peer": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "peer": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "peer": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "peer": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "peer": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "peer": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "peer": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "peer": true + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "peer": true + }, + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "peer": true + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "peer": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "peer": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "peer": true + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "peer": true, + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "peer": true, + "requires": { + "side-channel": "^1.0.4" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "peer": true + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "peer": true, + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "peer": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "peer": true + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "peer": true, + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "peer": true + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "peer": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "serverless-http": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/serverless-http/-/serverless-http-2.7.0.tgz", + "integrity": "sha512-iWq0z1X2Xkuvz6wL305uCux/SypbojHlYsB5bzmF5TqoLYsdvMNIoCsgtWjwqWoo3AR2cjw3zAmHN2+U6mF99Q==", + "requires": { + "@types/aws-lambda": "^8.10.56" + } + }, + "set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "peer": true, + "requires": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "peer": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "peer": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "peer": true + }, + "swagger-ui-dist": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.9.3.tgz", + "integrity": "sha512-/OgHfO96RWXF+p/EOjEnvKNEh94qAG/VHukgmVKh5e6foX9kas1WbjvQnDDj0sSTAMr9MHRBqAWytDcQi0VOrg==" + }, + "swagger-ui-express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz", + "integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==", + "requires": { + "swagger-ui-dist": ">=5.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "peer": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "peer": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "peer": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "peer": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "peer": true + } + } +} diff --git a/resources/api-docs/package.json b/resources/api-docs/package.json new file mode 100644 index 00000000..87e0c1f8 --- /dev/null +++ b/resources/api-docs/package.json @@ -0,0 +1,7 @@ +{ + "type": "module", + "dependencies": { + "serverless-http": "^2.6.1", + "swagger-ui-express": "^5.0.0" + } +} \ No newline at end of file diff --git a/resources/api-docs/swagger.json b/resources/api-docs/swagger.json new file mode 100644 index 00000000..f50e41da --- /dev/null +++ b/resources/api-docs/swagger.json @@ -0,0 +1,20 @@ +{ + "swagger" : "2.0", + "info" : { + "title" : "sb-env-api" + }, + "basePath" : "/v1", + "schemes" : [ "https" ], + "paths" : { + }, + "securityDefinitions" : { + "sb-workshop-api-authorizer" : { + "type" : "apiKey", + "name" : "Authorization", + "in" : "header", + "x-amazon-apigateway-authtype" : "custom" + } + }, + "definitions" : { + } +} diff --git a/resources/custom-resources/rds-bootstrap/update.sh b/resources/api-docs/update.sh similarity index 55% rename from resources/custom-resources/rds-bootstrap/update.sh rename to resources/api-docs/update.sh index 77674b7e..b54822af 100755 --- a/resources/custom-resources/rds-bootstrap/update.sh +++ b/resources/api-docs/update.sh @@ -17,42 +17,45 @@ if [ -z $1 ]; then echo "Usage: $0 [Lambda Folder]" exit 2 fi - -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -echo "AWS Region = $MY_AWS_REGION" - ENVIRONMENT=$1 -LAMBDA_STAGE_FOLDER=$2 -if [ -z $LAMBDA_STAGE_FOLDER ]; then - LAMBDA_STAGE_FOLDER="lambdas" -fi -LAMBDA_CODE=RdsBootstrap-lambda.zip +FUNCTIONS=($2) +LAMBDA_CODE=ApiDocs-lambda.zip -#set this for V2 AWS CLI to disable paging +# Disable AWS CLI paging export AWS_PAGER="" +MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') +echo "AWS Region = $MY_AWS_REGION" + SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" if [ -z $SAAS_BOOST_BUCKET ]; then echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" exit 1 fi +LAMBDAS_FOLDER=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/LAMBDAS_FOLDER" --query 'Parameter.Value' --output text 2>/dev/null) +if [ -z $LAMBDAS_FOLDER ]; then + LAMBDAS_FOLDER="lambdas/" +fi +if [[ $LAMBDAS_FOLDER != */ ]]; then + LAMBDAS_FOLDER="$LAMBDAS_FOLDER/" +fi +echo "Lambdas folder = $LAMBDAS_FOLDER" +echo "Function code = $LAMBDA_CODE" # Do a fresh build of the project -mvn -if [ $? -ne 0 ]; then - echo "Error building project" - exit 1 -fi +rm -rf build +mkdir build +npm install +zip -r build/$LAMBDA_CODE app.js package.json package-lock.json swagger.json node_modules # And copy it up to S3 -aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ - -# Find all the functions for this microservice -# We must list in the rds-bootstrap case since functions are created with a tenant ID suffix -eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-rds-bootstrap-\`)] | [].FunctionName' --output text"\) -FUNCTIONS=($FUNCTIONS) -for FX in "${FUNCTIONS[@]}"; do - printf "Updating function code for %s\n" $FX - aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "$FX" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE -done +aws s3 cp build/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDAS_FOLDER + +# Make sure the function exists before trying to update it +FUNCTION="sb-${ENVIRONMENT}-api-docs" +aws lambda --region "$MY_AWS_REGION" get-function --function-name "$FUNCTION" > /dev/null 2>&1 +if [ $? -eq 0 ]; then + printf "Updating function code for ${FUNCTION}\n" + aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "$FUNCTION" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key "${LAMBDAS_FOLDER}${LAMBDA_CODE}" +fi diff --git a/resources/checkstyle/audit.py b/resources/checkstyle/audit.py new file mode 100644 index 00000000..337ee351 --- /dev/null +++ b/resources/checkstyle/audit.py @@ -0,0 +1,105 @@ +#!/bin/env python3 + +import logging +import operator +import os +import re +import subprocess +import sys +import xml.dom.minidom as minidom + +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format="%(levelname)s - %(message)s") + +audit_start_pattern = re.compile(r"^\[INFO\] Ignored \d+ errors, \d+ violations remaining\.$") +audit_end_pattern = re.compile(r"^\[INFO\] You have \d+ Checkstyle violations\. The maximum number of allowed violations is \d+\.$") +#audit_line_pattern = re.compile(r"^\[WARN\] (.*): (.*)\. \[(.*)\]$") +audit_line_pattern = re.compile(r"^\[WARNING\] .*:\[\d+(?:,\d+)?] \(.*\) (.*?): .*$") +max_allowed_violations = -1 + +def checkstyle_stats(): + maven_project_dir = sys.argv[1] + maven_project_dir_path = os.path.abspath(maven_project_dir) + pom_file = os.path.join(maven_project_dir_path, "pom.xml") + if not os.path.isfile(pom_file): + logging.error("Can't find pom.xml file in %s" % maven_project_dir_path) + sys.exit(1) + + logging.info("Processing pom.xml file in %s" % maven_project_dir_path) + os.chdir(maven_project_dir_path) + + pom = minidom.parse(open(pom_file)) + pom_properties = pom.getElementsByTagName("properties") + if pom_properties is not None: + for property_tag in pom_properties: + checkstyle_tag = property_tag.getElementsByTagName("checkstyle.maxAllowedViolations") + if checkstyle_tag is not None and checkstyle_tag.length == 1: + global max_allowed_violations + max_allowed_violations = int(checkstyle_tag[0].firstChild.data) + break + else: + logging.error("Can't find checkstyle.maxAllowedViolations") + else: + logging.error("Can't find properties tag in pom.xml file") + logging.debug("max allowed violations = %d" % max_allowed_violations) + + try: + maven = subprocess.check_output(["mvn", "checkstyle:check"], universal_newlines=True) + checkstyle_audit = maven.split("\n") + logging.debug("mvn checkstyle:check finished with %d lines of output" % len(checkstyle_audit)) + except subprocess.CalledProcessError as e: + logging.error("command mvn checkstyle:check failed") + sys.exit(1) + + audit = {} + start = audit_start(checkstyle_audit) + end = audit_end(checkstyle_audit) + + for line in checkstyle_audit[start:end]: + regex = audit_line_pattern.match(line) + if regex: + category = regex.group(1) + if category in audit: + audit[category] += 1 + else: + audit[category] = 1 + else: + logging.warning("Line did not match audit pattern:\n" + line) + + print_results(audit) + +def audit_start(lines): + start = 0 + for line_no, line in enumerate(lines): + #if "[INFO] Starting audit..." == line: + if audit_start_pattern.match(line): + logging.debug("Found start of audit on line %d %s" % (line_no, line)) + start = line_no + 1 + break + return start + +def audit_end(lines): + end = 0 + for line_no, line in enumerate(lines): + #if "Audit done." == line: + if audit_end_pattern.match(line): + logging.debug("Found end of audit on line %d %s" % (line_no, line)) + end = line_no + break + return end + +def print_results(results): + total = sum(results.values()) + print("Total checkstyle warnings %d. Maximum allowed violations %d." % (total, max_allowed_violations)) + if total > max_allowed_violations: + print("PR sanity check will fail until you reduce CheckStyle violations") + elif max_allowed_violations > total: + print("Update pom.xml and set checkstyle.maxAllowedViolations to %d" % total) + data = sorted(results.items(), key=operator.itemgetter(1), reverse=True) + for category, count in data: + print("% 5.1f%% %4d %s" % (((count / total) * 100), count, category)) + +if __name__ == '__main__': + if len(sys.argv) < 2: + print("Usage: %s dir_with_pom_file" % sys.argv[0]) + sys.exit(1) + checkstyle_stats() diff --git a/resources/checkstyle/checkstyle.xml b/resources/checkstyle/checkstyle.xml index 587308e7..197b52a3 100644 --- a/resources/checkstyle/checkstyle.xml +++ b/resources/checkstyle/checkstyle.xml @@ -283,7 +283,7 @@ value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/> - + diff --git a/resources/custom-resources/app-services-macro/pom.xml b/resources/custom-resources/app-services-macro/pom.xml deleted file mode 100644 index 8203756e..00000000 --- a/resources/custom-resources/app-services-macro/pom.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - 4.0.0 - - com.amazon.aws.partners.saasfactory.saasboost - saasboost-custom-resources - 1.0.0 - - ApplicationServicesMacro - 1.0.0 - jar - - - Apache-2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - - 0 - - - - ${project.artifactId} - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - org.apache.maven.plugins - maven-assembly-plugin - - - io.github.git-commit-id - git-commit-id-maven-plugin - - - - - - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - - - - diff --git a/resources/custom-resources/app-services-macro/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApplicationServicesMacro.java b/resources/custom-resources/app-services-macro/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApplicationServicesMacro.java deleted file mode 100644 index 5cdd36f6..00000000 --- a/resources/custom-resources/app-services-macro/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApplicationServicesMacro.java +++ /dev/null @@ -1,348 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.*; - -public class ApplicationServicesMacro implements RequestHandler, Map> { - - private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationServicesMacro.class); - private static final String FRAGMENT = "fragment"; - private static final String REQUEST_ID = "requestId"; - private static final String TEMPLATE_PARAMETERS = "templateParameterValues"; - private static final String STATUS = "status"; - private static final String SUCCESS = "SUCCESS"; - private static final String FAILURE = "FAILURE"; - private static final String ERROR_MSG = "errorMessage"; - - /** - * CloudFormation macro to create resources based on SaaS Boost appConfig objects: - * 1/ ECR repository resources for each application service passed as a template parameter. - * Returns failure if the "ApplicationServices" template parameter is missing. - * 2/ S3 bucket resource for all application services if enabled via a template parameter. - * Assumes a missing `AppExtension` parameter means S3 support is not enabled. - * - * @param event Lambda event containing the CloudFormation request id, fragment, and template parameters - * @param context Lambda execution context - * @return CloudFormation macro response of success or failure and the modified template fragment - */ - @Override - @SuppressWarnings("unchecked") - public Map handleRequest(Map event, Context context) { - Utils.logRequestEvent(event); - - Map response = new HashMap<>(); - response.put(REQUEST_ID, event.get(REQUEST_ID)); - response.put(STATUS, FAILURE); - - Map templateParameters = (Map) event.get(TEMPLATE_PARAMETERS); - Map template = (Map) event.get(FRAGMENT); - - String ecrError = updateTemplateForEcr(templateParameters, template); - if (ecrError != null) { - LOGGER.error("Encountered error updating template for ECR repositories: {}"); - response.put(ERROR_MSG, ecrError); - return response; - } - LOGGER.info("Successfully altered template for ECR repositories"); - - String extensionsError = updateTemplateForPooledExtensions(templateParameters, template); - if (extensionsError != null) { - LOGGER.error("Encountered error updating template for pooled extensions: {}"); - response.put(ERROR_MSG, extensionsError); - return response; - } - LOGGER.info("Successfully altered template for extensions"); - - response.put(FRAGMENT, template); - response.put(STATUS, SUCCESS); - return response; - } - - protected static String updateTemplateForPooledExtensions( - final Map templateParameters, - Map template) { - Set processedExtensions = new HashSet(); - if (templateParameters.containsKey("AppExtensions") && template.containsKey("Resources")) { - String applicationExtensions = (String) templateParameters.get("AppExtensions"); - if (Utils.isNotEmpty(applicationExtensions)) { - String[] extensions = applicationExtensions.split(","); - for (String extension : extensions) { - if (processedExtensions.contains(extension)) { - LOGGER.warn("Skipping duplicate extension {}", extension); - continue; - } - switch (extension) { - case "s3": { - String s3Error = updateTemplateForS3(templateParameters, template); - if (s3Error != null) { - LOGGER.error("Processing S3 extension failed: {}", s3Error); - return s3Error; - } - LOGGER.info("Successfully processed s3 extension"); - break; - } - default: { - LOGGER.warn("Skipping unknown extension {}", extension); - } - } - processedExtensions.add(extension); - } - } else { - LOGGER.debug("Empty AppExtensions parameter, skipping updating template for extensions"); - } - } else { - LOGGER.error("Invalid template, missing AppExtensions parameter or missing Resources"); - return "Invalid template, missing AppExtensions parameter or missing Resources"; - } - return null; - } - - protected static String updateTemplateForS3(final Map templateParameters, - Map template) { - if (templateParameters.containsKey("Environment") - && templateParameters.containsKey("LoggingBucket")) { - String environmentName = (String) templateParameters.get("Environment"); - // Bucket Resource - Map s3Resource = s3Resource(environmentName, - (String) templateParameters.get("LoggingBucket")); - ((Map) template.get("Resources")).put("TenantStorage", s3Resource); - - // Custom Resource to clear the bucket before we delete it - Map clearBucketResource = Map.of( - "Type", "Custom::CustomResource", - "Properties", Map.of( - "ServiceToken", "{{resolve:ssm:/saas-boost/" + environmentName + "/CLEAR_BUCKET_ARN}}", - "Bucket", Map.of("Ref", "TenantStorage") - ) - ); - ((Map) template.get("Resources")).put("ClearTenantStorageBucket", clearBucketResource); - } else { - return "Invalid template, missing parameter Environment or LoggingBucket"; - } - return null; - } - - protected static Map s3Resource(String environment, String loggingBucket) { - Map resourceProperties = new LinkedHashMap<>(); - - // tags - resourceProperties.put("Tags", List.of(Map.of( - "Key", "SaaS Boost", - "Value", environment - ))); - - // encryptionConfiguration - resourceProperties.put("BucketEncryption", Map.of( - "ServerSideEncryptionConfiguration", List.of(Map.of( - "BucketKeyEnabled", true, - "ServerSideEncryptionByDefault", Map.of("SSEAlgorithm", "AES256") - )) - )); - - // loggingConfiguration - resourceProperties.put("LoggingConfiguration", Map.of( - "DestinationBucketName", loggingBucket, - "LogFilePrefix", "s3extension-logs" - )); - - // ownershipControls - resourceProperties.put("OwnershipControls", Map.of( - "Rules", List.of(Map.of("ObjectOwnership", "BucketOwnerEnforced")) - )); - - // publicAccessBlockConfiguration - resourceProperties.put("PublicAccessBlockConfiguration", Map.of( - "BlockPublicAcls", true, - "BlockPublicPolicy", true, - "IgnorePublicAcls", true, - "RestrictPublicBuckets", true - )); - - Map resource = new LinkedHashMap<>(); - resource.put("Type", "AWS::S3::Bucket"); - resource.put("Properties", resourceProperties); - - return resource; - } - - protected static String updateTemplateForEcr( - final Map templateParameters, - Map template) { - if (templateParameters.containsKey("ApplicationServices")) { - if (template.containsKey("Resources")) { - String servicesList = (String) templateParameters.get("ApplicationServices"); - if (Utils.isNotEmpty(servicesList)) { - String[] services = servicesList.split(","); - for (String service : services) { - // Each application service needs its own ECR repository - String ecrResourceName = ecrResourceName(service); - ((Map) template.get("Resources")).put(ecrResourceName, ecrResource(service)); - - // Define an EventBridge rule to capture image events on the repo - String eventRuleResourceName = eventRuleResourceName(service); - ((Map) template.get("Resources")).put(eventRuleResourceName, - eventRuleResource(service, ecrResourceName)); - - // And we need a Lambda permission for this rule to invoke the workload deploy function - ((Map) template.get("Resources")).put(eventRulePermissionName(service), - eventRulePermissionResource(eventRuleResourceName)); - } - LOGGER.info(Utils.toJson(template)); - } else { - LOGGER.info("Empty ApplicationServices list. Skipping template modification."); - } - } else { - LOGGER.warn("CloudFormation template fragment does not have Resources"); - } - } else { - LOGGER.error("Invalid template, missing parameter ApplicationServices"); - return "Invalid template, missing parameter ApplicationServices"; - } - // no error message implies success? - return null; - } - - protected static String cloudFormationResourceName(String name) { - return name != null ? name.replaceAll("[^A-Za-z0-9]", "") : null; - } - - protected static String ecrResourceName(String serviceName) { - if (Utils.isBlank(serviceName)) { - throw new IllegalArgumentException("service name cannot be blank"); - } - return cloudFormationResourceName(serviceName); - } - - protected static Map ecrResource(String serviceName) { - if (Utils.isBlank(serviceName)) { - throw new IllegalArgumentException("service name cannot be blank"); - } - - Map resourceProperties = new LinkedHashMap<>(); - resourceProperties.put("EncryptionConfiguration", Collections - .singletonMap("EncryptionType", "AES256") - ); - Map tag = new LinkedHashMap<>(); - tag.put("Key", "Name"); - tag.put("Value", serviceName.trim()); - resourceProperties.put("Tags", Collections.singletonList(tag)); - - Map resource = new LinkedHashMap<>(); - resource.put("Type", "AWS::ECR::Repository"); - resource.put("Properties", resourceProperties); - - return resource; - } - - protected static String eventRuleResourceName(String serviceName) { - if (Utils.isBlank(serviceName)) { - throw new IllegalArgumentException("service name cannot be blank"); - } - return "ImageEventRule" + cloudFormationResourceName(serviceName); - } - - /** - * Generates an AWS::Events::Rule resource to trigger the workload deployment Lambda function when an ECR image - * event occurs for a given repository. - * - *

    Example: "Service A" returns - *

    -     * Type: AWS::Events::Rule
    -     * Properties:
    -     *   Name: !Sub sb-${Environment}-ecr-servicea
    -     *   EventPattern:
    -     *     source:
    -     *       - aws.ecr
    -     *     detail-type:
    -     *       - ECR Image Action
    -     *     detail:
    -     *       repository-name:
    -     *         - !Ref servicea
    -     *   State: ENABLED
    -     *   Targets:
    -     *     - Arn: !GetAtt WorkloadDeployLambda.Arn
    -     *       Id: !Sub sb-${Environment}-deploy-servicea
    -     * 
    - * @param serviceName The name of the application service - * @return an AWS::Events:Rule resource object - */ - protected static Map eventRuleResource(String serviceName, String ecrResource) { - if (Utils.isBlank(serviceName)) { - throw new IllegalArgumentException("service name cannot be blank"); - } - if (Utils.isBlank(ecrResource)) { - throw new IllegalArgumentException("ECR repository resource to reference cannot be blank"); - } - Map resourceProperties = new LinkedHashMap<>(); - resourceProperties.put("Name", Collections.singletonMap("Fn::Sub", "sb-${Environment}-ecr-" - + cloudFormationResourceName(serviceName).toLowerCase()) - ); - Map eventPattern = new LinkedHashMap<>(); - eventPattern.put("source", Collections.singletonList("aws.ecr")); - eventPattern.put("detail-type", Collections.singletonList("ECR Image Action")); - eventPattern.put("detail", Collections.singletonMap("repository-name", Collections.singletonList( - Collections.singletonMap("Ref", ecrResource) - ))); - resourceProperties.put("EventPattern", eventPattern); - resourceProperties.put("State", "ENABLED"); - - Map target = new LinkedHashMap<>(); - target.put("Arn", Collections.singletonMap("Fn::GetAtt", Arrays.asList("WorkloadDeployLambda", "Arn"))); - target.put("Id", Collections.singletonMap("Fn::Sub", "sb-${Environment}-deploy-" - + cloudFormationResourceName(serviceName).toLowerCase()) - ); - resourceProperties.put("Targets", Collections.singletonList(target)); - - Map resource = new LinkedHashMap<>(); - resource.put("Type", "AWS::Events::Rule"); - resource.put("Properties", resourceProperties); - - return resource; - } - - protected static String eventRulePermissionName(String serviceName) { - if (Utils.isBlank(serviceName)) { - throw new IllegalArgumentException("service name cannot be blank"); - } - return "ImageEventPermission" + cloudFormationResourceName(serviceName); - } - - protected static Map eventRulePermissionResource(String eventRuleResource) { - if (Utils.isBlank(eventRuleResource)) { - throw new IllegalArgumentException("EventBridge rule to reference cannot be blank"); - } - Map resourceProperties = new LinkedHashMap<>(); - resourceProperties.put("FunctionName", Collections.singletonMap("Ref", "WorkloadDeployLambda")); - resourceProperties.put("Principal", "events.amazonaws.com"); - resourceProperties.put("Action", "lambda:InvokeFunction"); - resourceProperties.put("SourceArn", Collections.singletonMap("Fn::GetAtt", - Arrays.asList(eventRuleResource, "Arn")) - ); - - Map resource = new LinkedHashMap<>(); - resource.put("Type", "AWS::Lambda::Permission"); - resource.put("Properties", resourceProperties); - - return resource; - } -} diff --git a/resources/custom-resources/app-services-macro/src/main/resources/lambda-assembly.xml b/resources/custom-resources/app-services-macro/src/main/resources/lambda-assembly.xml deleted file mode 100644 index 26364854..00000000 --- a/resources/custom-resources/app-services-macro/src/main/resources/lambda-assembly.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - lambda - - zip - - false - - - - ${project.build.outputDirectory} - - com/amazon/aws/partners/saasfactory/** - log4j2.xml - git.properties - - - - - - false - true - lib - - - \ No newline at end of file diff --git a/resources/custom-resources/app-services-macro/src/main/resources/log4j2.xml b/resources/custom-resources/app-services-macro/src/main/resources/log4j2.xml deleted file mode 100644 index 04128110..00000000 --- a/resources/custom-resources/app-services-macro/src/main/resources/log4j2.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} %X{AWSRequestId} %-5p %C{1} - %m%n - - - - - - - - - - - - diff --git a/resources/custom-resources/app-services-macro/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ApplicationServicesMacroTest.java b/resources/custom-resources/app-services-macro/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ApplicationServicesMacroTest.java deleted file mode 100644 index 99558fde..00000000 --- a/resources/custom-resources/app-services-macro/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ApplicationServicesMacroTest.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import org.junit.Test; - -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; - -import static org.junit.Assert.*; - -public class ApplicationServicesMacroTest { - - @Test(expected = IllegalArgumentException.class) - public void testResourceNameNullServiceName() { - ApplicationServicesMacro.ecrResourceName(null); - } - - @Test(expected = IllegalArgumentException.class) - public void testResourceNameEmptyServiceName() { - ApplicationServicesMacro.ecrResourceName(""); - } - - @Test(expected = IllegalArgumentException.class) - public void testResourceNameBlankServiceName() { - ApplicationServicesMacro.ecrResourceName(" "); - } - - @Test - public void testResourceName() { - String serviceName = "foo"; - String expected = "foo"; - String actual = ApplicationServicesMacro.ecrResourceName(serviceName); - assertEquals(expected, actual); - - serviceName = "Foo"; - expected = "Foo"; - actual = ApplicationServicesMacro.ecrResourceName(serviceName); - assertEquals(expected, actual); - - serviceName = "Foo Bar"; - expected = "FooBar"; - actual = ApplicationServicesMacro.ecrResourceName(serviceName); - assertEquals(expected, actual); - - serviceName = "Foo_Bar"; - expected = "FooBar"; - actual = ApplicationServicesMacro.ecrResourceName(serviceName); - assertEquals(expected, actual); - - serviceName = "Foo-Bar"; - expected = "FooBar"; - actual = ApplicationServicesMacro.ecrResourceName(serviceName); - assertEquals(expected, actual); - } - - @Test - public void testHandleRequest() throws Exception { - try (InputStream json = Files.newInputStream(Path.of(this.getClass().getClassLoader().getResource("template.json").toURI()))) { - LinkedHashMap template = Utils.fromJson(json, LinkedHashMap.class); - - ApplicationServicesMacro macro = new ApplicationServicesMacro(); - - // Blank ApplicationServices parameter should return the same template - Map response = macro.handleRequest(buildEvent(template), null); - assertTrue(response.containsKey("fragment")); - LinkedHashMap modifiedTemplate = (LinkedHashMap) response.get("fragment"); - - assertEquals("Size unequal", template.size(), modifiedTemplate.size()); - for (Map.Entry entry : template.entrySet()) { - assertEquals("Value mismatch for '" + entry.getKey() + "'", template.get(entry.getKey()), modifiedTemplate.get(entry.getKey())); - } - - // No ApplicationServices parameter should return failure - Map applicationServices = (Map) ((LinkedHashMap) template.get("Parameters")).get("ApplicationServices"); - ((LinkedHashMap) template.get("Parameters")).remove("ApplicationServices"); - response = macro.handleRequest(buildEvent(template), null); - assertEquals("No ApplicationServices parameter is an error", "FAILURE", response.get("status")); - - // List of ApplicationServices should return new resources in the fragment - applicationServices.put("Default", "foo, Bar,baz Oole"); - ((LinkedHashMap) template.get("Parameters")).put("ApplicationServices", applicationServices); - response = macro.handleRequest(buildEvent(template), null); - assertTrue(response.containsKey("fragment")); - modifiedTemplate = (LinkedHashMap) response.get("fragment"); - LinkedHashMap resources = (LinkedHashMap) modifiedTemplate.get("Resources"); - assertEquals(9, resources.size()); - assertTrue(resources.containsKey("foo")); - assertTrue(resources.containsKey("ImageEventRulefoo")); - assertTrue(resources.containsKey("ImageEventPermissionfoo")); - assertTrue(resources.containsKey("Bar")); - assertTrue(resources.containsKey("ImageEventRuleBar")); - assertTrue(resources.containsKey("ImageEventPermissionBar")); - assertTrue(resources.containsKey("bazOole")); - assertTrue(resources.containsKey("ImageEventRulebazOole")); - assertTrue(resources.containsKey("ImageEventPermissionbazOole")); - - // There should be a single tag for Name and it should have the non-modified application service name - // for its Value - List> tags = (List>) ((LinkedHashMap) ((LinkedHashMap) resources.get("bazOole")).get("Properties")).get("Tags"); - assertEquals(1, tags.size()); - assertEquals("baz Oole", tags.get(0).get("Value")); - } - } - - static Map buildEvent(LinkedHashMap template) { - Map event = new HashMap<>(); - event.put("requestId", UUID.randomUUID().toString()); - event.put("templateParameterValues", templateParameters(template)); - event.put("fragment", template); - return event; - } - - static LinkedHashMap templateParameters(LinkedHashMap template) { - LinkedHashMap templateParameters = new LinkedHashMap<>(); - LinkedHashMap parameters = (LinkedHashMap) template.get("Parameters"); - for (Map.Entry parameter : parameters.entrySet()) { - templateParameters.put(parameter.getKey(), ((Map) parameter.getValue()).get("Default")); - } - return templateParameters; - } -} \ No newline at end of file diff --git a/resources/custom-resources/app-services-macro/src/test/resources/template.json b/resources/custom-resources/app-services-macro/src/test/resources/template.json deleted file mode 100644 index 6968c5f6..00000000 --- a/resources/custom-resources/app-services-macro/src/test/resources/template.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "AWSTemplateFormatVersion": "2010-09-09", - "Description": "Resource Name Macro Test", - "Parameters": { - "ApplicationServices": { - "Type": "String", - "Default": "" - }, - "AppExtensions": { - "Type": "String", - "Default": "" - }, - "Environment": { - "Type": "String", - "Default": "" - }, - "LoggingBucket": { - "Type": "String", - "Default": "" - } - }, - "Resources": { - } -} \ No newline at end of file diff --git a/resources/custom-resources/app-services-macro/update.sh b/resources/custom-resources/app-services-macro/update.sh deleted file mode 100755 index 52c20e3f..00000000 --- a/resources/custom-resources/app-services-macro/update.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -if [ -z $1 ]; then - echo "Usage: $0 [Lambda Folder]" - exit 2 -fi - -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -echo "AWS Region = $MY_AWS_REGION" - -ENVIRONMENT=$1 -LAMBDA_STAGE_FOLDER=$2 -if [ -z $LAMBDA_STAGE_FOLDER ]; then - LAMBDA_STAGE_FOLDER="lambdas" -fi -LAMBDA_CODE=ApplicationServicesMacro-lambda.zip - -#set this for V2 AWS CLI to disable paging -export AWS_PAGER="" - -SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) -echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" -if [ -z $SAAS_BOOST_BUCKET ]; then - echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" - exit 1 -fi - -# Do a fresh build of the project -mvn -if [ $? -ne 0 ]; then - echo "Error building project" - exit 1 -fi - -# And copy it up to S3 -aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ - -eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`saas-boost-app-services-macro\`)] | [].FunctionName' --output text"\) - -for FUNCTION in ${FUNCTIONS[@]}; do - #echo $FUNCTION - aws lambda --region $MY_AWS_REGION update-function-code --function-name $FUNCTION --s3-bucket $SAAS_BOOST_BUCKET --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE -done \ No newline at end of file diff --git a/resources/custom-resources/attach-ecs-capacity-provider/pom.xml b/resources/custom-resources/attach-ecs-capacity-provider/pom.xml deleted file mode 100644 index d7be7275..00000000 --- a/resources/custom-resources/attach-ecs-capacity-provider/pom.xml +++ /dev/null @@ -1,134 +0,0 @@ - - - - 4.0.0 - - com.amazon.aws.partners.saasfactory.saasboost - saasboost-custom-resources - 1.0.0 - - AttachEcsCapacityProvider - 1.0.0 - jar - - - Apache-2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - - 0 - - - - ${project.artifactId} - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - org.apache.maven.plugins - maven-assembly-plugin - - - pl.project13.maven - git-commit-id-plugin - 4.0.0 - - - get-the-git-infos - - revision - - initialize - - - - true - ${project.build.outputDirectory}/git.properties - - ^git.commit.id.describe - ^git.commit.id.describe-short - ^git.commit.time - ^git.closest.tag.name - - full - ../../.git - false - - - - - - - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - - - com.amazon.aws.partners.saasfactory.saasboost - CloudFormationUtils - 1.0.0 - - provided - - - org.mockito - mockito-core - - - software.amazon.awssdk - ecs - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - software.amazon.awssdk - dynamodb - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - - diff --git a/resources/custom-resources/attach-ecs-capacity-provider/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AttachCapacityProviderRequestHandler.java b/resources/custom-resources/attach-ecs-capacity-provider/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AttachCapacityProviderRequestHandler.java deleted file mode 100644 index c4ab216d..00000000 --- a/resources/custom-resources/attach-ecs-capacity-provider/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AttachCapacityProviderRequestHandler.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.services.ecs.EcsClient; -import software.amazon.awssdk.services.ecs.model.Cluster; -import software.amazon.awssdk.services.ecs.model.ClusterNotFoundException; -import software.amazon.awssdk.services.ecs.model.DescribeClustersRequest; -import software.amazon.awssdk.services.ecs.model.PutClusterCapacityProvidersRequest; -import software.amazon.awssdk.services.ecs.model.UpdateInProgressException; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.function.Function; -import java.util.stream.Collectors; - -public final class AttachCapacityProviderRequestHandler implements Callable { - - private static final Logger LOGGER = LoggerFactory.getLogger(AttachCapacityProviderRequestHandler.class); - - private final RequestContext requestContext; - private final CapacityProviderLock lock; - private final EcsClient ecs; - - public AttachCapacityProviderRequestHandler( - RequestContext requestContext, - CapacityProviderLock lock, - EcsClient ecs) { - this.ecs = ecs; - this.requestContext = requestContext; - this.lock = lock; - } - - @Override - public HandleResult call() { - HandleResult result = new HandleResult(); - LOGGER.info(requestContext.requestType.toUpperCase()); - if ("Create".equalsIgnoreCase(requestContext.requestType) - || "Update".equalsIgnoreCase(requestContext.requestType)) { - LOGGER.info("Attaching capacity provider {} to ecs cluster {} for tenant {}", - requestContext.capacityProvider, requestContext.ecsCluster, requestContext.tenantId); - result = atomicallyUpdateCapacityProviders((capacityProviders) -> { - if (!capacityProviders.contains(requestContext.capacityProvider)) { - List modifiedCapacityProviders = new ArrayList(capacityProviders); - modifiedCapacityProviders.add(requestContext.capacityProvider); - return modifiedCapacityProviders; - } - return capacityProviders; - }); - } else if ("Delete".equalsIgnoreCase(requestContext.requestType)) { - // unclear whether we need this.. commenting it out for testing. - LOGGER.info("Detaching capacity provider {} from ecs cluster {} for tenant {}", - requestContext.capacityProvider, requestContext.ecsCluster, requestContext.tenantId); - result = atomicallyUpdateCapacityProviders((capacityProviders) -> { - return capacityProviders.stream() - .filter((capacityProvider) -> !capacityProvider.equals(requestContext.capacityProvider)) - .collect(Collectors.toList()); - }); - result.setSucceeded(); - } else { - LOGGER.error("FAILED unknown requestType {}", requestContext.requestType); - result.putFailureReason("Unknown RequestType " + requestContext.requestType); - result.setFailed(); - } - - return result; - } - - private HandleResult atomicallyUpdateCapacityProviders( - Function, List> capacityProvidersMutationFunction) { - HandleResult result = new HandleResult(); - // lock ddb - lock.lock(requestContext); - try { - // read capacity providers into list - List existingCapacityProviders = getExistingCapacityProviders(); - - List mutatedCapacityProviders = capacityProvidersMutationFunction.apply(existingCapacityProviders); - LOGGER.debug("existingCapacityProviders {} mutated to {}", - existingCapacityProviders, mutatedCapacityProviders); - - boolean successful = false; - // if the mutate did nothing, no point in slowing us down to make an ECS call - if (existingCapacityProviders.equals(mutatedCapacityProviders)) { - successful = true; - result.setSucceeded(); - } - while (!successful) { - try { - // set capacity providers. response doesn't really give us anything but a - // description of the new cluster. exceptions are thrown on failure - ecs.putClusterCapacityProviders(PutClusterCapacityProvidersRequest.builder() - .cluster(requestContext.ecsCluster) - .capacityProviders(mutatedCapacityProviders) - .build()); - successful = true; - result.setSucceeded(); - } catch (UpdateInProgressException uipe) { - // There's a Amazon ECS container agent update in progress on this container instance. - // ECS errors indicate this can be retried. Wait 10 seconds and try again. - LOGGER.error("Received error calling putClusterCapacityProviders", uipe); - LOGGER.error(Utils.getFullStackTrace(uipe)); - LOGGER.error("Waiting 10 seconds before retrying.."); - Thread.sleep(10 * 1000); // 10 seconds - } - } - } catch (ClusterNotFoundException cnfe) { - LOGGER.error("Could not find ecs cluster: {}", requestContext.ecsCluster); - LOGGER.error(Utils.getFullStackTrace(cnfe)); - result.putFailureReason(cnfe.getMessage()); - result.setFailed(); - } catch (InterruptedException ie) { - LOGGER.error("Error while waiting between putClusterCapacityProvider calls", ie.getMessage()); - LOGGER.error(Utils.getFullStackTrace(ie)); - result.putFailureReason(ie.getMessage()); - result.setFailed(); - } finally { - // unlock ddb - lock.unlock(requestContext); - } - return result; - } - - private List getExistingCapacityProviders() { - List returnedClusters = ecs.describeClusters( - DescribeClustersRequest.builder().clusters(requestContext.ecsCluster).build()).clusters(); - if (returnedClusters.size() != 1) { - // we only passed one cluster ARN but we received 0 or 2 - LOGGER.error("Expected 1 cluster with name {} but found {}", - requestContext.ecsCluster, returnedClusters.size()); - } - List existingCapacityProviders = returnedClusters.get(0).capacityProviders(); - return existingCapacityProviders == null ? List.of() : existingCapacityProviders; - } - -} diff --git a/resources/custom-resources/attach-ecs-capacity-provider/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AttachEcsCapacityProvider.java b/resources/custom-resources/attach-ecs-capacity-provider/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AttachEcsCapacityProvider.java deleted file mode 100644 index 0bf21135..00000000 --- a/resources/custom-resources/attach-ecs-capacity-provider/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AttachEcsCapacityProvider.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; -import software.amazon.awssdk.services.ecs.EcsClient; - -import java.util.*; -import java.util.concurrent.*; - -public class AttachEcsCapacityProvider implements RequestHandler, Object> { - - private static final Logger LOGGER = LoggerFactory.getLogger(AttachEcsCapacityProvider.class); - - private final EcsClient ecs; - private final CapacityProviderLock lock; - - public AttachEcsCapacityProvider() { - LOGGER.info("Version Info: {}", Utils.version(this.getClass())); - ecs = Utils.sdkClient(EcsClient.builder(), EcsClient.SERVICE_NAME); - lock = new CapacityProviderLock(Utils.sdkClient(DynamoDbClient.builder(), DynamoDbClient.SERVICE_NAME)); - } - - @Override - public Object handleRequest(Map event, Context context) { - Utils.logRequestEvent(event); - - Map resourceProperties = (Map) event.get("ResourceProperties"); - RequestContext requestContext = RequestContext.builder() - .requestType((String) event.get("RequestType")) - .capacityProvider((String) resourceProperties.get("CapacityProvider")) - .ecsCluster((String) resourceProperties.get("ECSCluster")) - .onboardingDdbTable((String) resourceProperties.get("OnboardingDdbTable")) - .tenantId((String) resourceProperties.get("TenantId")) - .build(); - HandleResult handleRequestResult = new HandleResult(); - ExecutorService service = Executors.newSingleThreadExecutor(); - try { - Callable c = new AttachCapacityProviderRequestHandler(requestContext, lock, ecs); - Future f = service.submit(c); - handleRequestResult = (HandleResult) f.get(context.getRemainingTimeInMillis() - 1000, - TimeUnit.MILLISECONDS); - } catch (final TimeoutException | InterruptedException | ExecutionException e) { - // Timed out - LOGGER.error("FAILED unexpected error or request timed out " + e.getMessage()); - String stackTrace = Utils.getFullStackTrace(e); - LOGGER.error(stackTrace); - handleRequestResult.setFailed(); - handleRequestResult.putResponseData("Reason", stackTrace); - } finally { - service.shutdown(); - } - CloudFormationResponse.send(event, context, - handleRequestResult.succeeded() ? "SUCCESS" : "FAILED", - handleRequestResult.getResponseData()); - return null; - } -} \ No newline at end of file diff --git a/resources/custom-resources/attach-ecs-capacity-provider/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CapacityProviderLock.java b/resources/custom-resources/attach-ecs-capacity-provider/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CapacityProviderLock.java deleted file mode 100644 index f68bb3b7..00000000 --- a/resources/custom-resources/attach-ecs-capacity-provider/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CapacityProviderLock.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.amazon.aws.partners.saasfactory.saasboost; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; -import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; -import software.amazon.awssdk.services.dynamodb.model.DynamoDbException; -import software.amazon.awssdk.services.dynamodb.model.ScanRequest; -import software.amazon.awssdk.services.dynamodb.model.ScanResponse; -import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; - -import java.util.Map; - -public class CapacityProviderLock { - private static final Logger LOGGER = LoggerFactory.getLogger(CapacityProviderLock.class); - - private final DynamoDbClient ddb; - private AttributeValue onboardingId = null; - - public CapacityProviderLock(DynamoDbClient ddb) { - this.ddb = ddb; - } - - /** - * Locks the distributed lock for reading/writing CapacityProviders. - * - * This function blocks indefinitely until the operation is successful, relying on outside - * timeouts to prevent us from actually blocking forever. - */ - public void lock(RequestContext requestContext) { - boolean locked = false; - while (!locked) { - locked = tryLockUnlock(requestContext, true); - if (!locked) { - // self-throttle so we don't blow up DDB trying to attain the lock - try { - Thread.sleep(5 * 1000); // 5 seconds - } catch (InterruptedException ie) { - // do nothing, keep trying - } - } - } - } - - /** - * Unlocks the distributed lock for reading/writing CapacityProviders. - * - * This function blocks indefinitely until the operation is successful, relying on outside - * timeouts to prevent us from actually blocking forever. We don't allow unlocking an unlocked - * lock, since it is an invalid operation: we should only be unlocking after our own lock. - */ - public void unlock(RequestContext requestContext) { - boolean success = false; - while (!success) { - success = tryLockUnlock(requestContext, false); - if (!success) { - // self-throttle so we don't blow up DDB trying to relinquish the lock - try { - Thread.sleep(5 * 1000); // 5 seconds - } catch (InterruptedException ie) { - // do nothing, keep trying - } - } - } - } - - // VisibleForTesting - protected AttributeValue currentOnboardingId(RequestContext requestContext) { - if (onboardingId == null) { - try { - ScanRequest scanRequest = ScanRequest.builder() - .tableName(requestContext.onboardingDdbTable) - .filterExpression("tenant_id = :tenantid") - .expressionAttributeValues((Map) Map.of( - ":tenantid", AttributeValue.builder().s(requestContext.tenantId).build())) - .build(); - LOGGER.debug("sending scan with ScanRequest {}", scanRequest); - ScanResponse scanResponse = ddb.scan(ScanRequest.builder() - .tableName(requestContext.onboardingDdbTable) - .filterExpression("tenant_id = :tenantid") - .expressionAttributeValues((Map) Map.of( - ":tenantid", AttributeValue.builder().s(requestContext.tenantId).build())) - .build()); - this.onboardingId = scanResponse.items().get(0).get("id"); - } catch (DynamoDbException ddbe) { - LOGGER.error("Error trying to scan for current onboarding id: {}", ddbe.getMessage()); - LOGGER.error(Utils.getFullStackTrace(ddbe)); - throw new RuntimeException(ddbe); - } - } - return this.onboardingId; - } - - // VisibleForTesting - protected boolean tryLockUnlock(RequestContext requestContext, boolean tryLock) { - try { - UpdateItemRequest updateItemRequest = UpdateItemRequest.builder() - .tableName(requestContext.onboardingDdbTable) - .key(Map.of("id", currentOnboardingId(requestContext))) - .conditionExpression("ecs_cluster_locked = :lock_expected") - .updateExpression("SET ecs_cluster_locked = :new_lock") - .expressionAttributeValues(Map.of( - ":lock_expected", AttributeValue.builder().bool(!tryLock).build(), - ":new_lock", AttributeValue.builder().bool(tryLock).build())) - .build(); - LOGGER.debug("trying to {} with updateItemRequest {}", tryLock ? "lock" : "unlock", updateItemRequest); - ddb.updateItem(UpdateItemRequest.builder() - .tableName(requestContext.onboardingDdbTable) - .key(Map.of("id", currentOnboardingId(requestContext))) - .conditionExpression("ecs_cluster_locked = :lock_expected") - .updateExpression("SET ecs_cluster_locked = :new_lock") - .expressionAttributeValues(Map.of( - ":lock_expected", AttributeValue.builder().bool(!tryLock).build(), - ":new_lock", AttributeValue.builder().bool(tryLock).build())) - .build()); - } catch (ConditionalCheckFailedException ccfe) { - LOGGER.error("Could not {} ecs_cluster_locked, conditional check failed: {}", - tryLock ? "lock" : "unlock", ccfe.getMessage()); - return false; - } catch (DynamoDbException ddbe) { - LOGGER.error("Error trying to update lock for current onboarding id: {}", ddbe.getMessage()); - LOGGER.error(Utils.getFullStackTrace(ddbe)); - throw new RuntimeException(ddbe); - } - return true; - } -} diff --git a/resources/custom-resources/attach-ecs-capacity-provider/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/HandleResult.java b/resources/custom-resources/attach-ecs-capacity-provider/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/HandleResult.java deleted file mode 100644 index f2dd9de3..00000000 --- a/resources/custom-resources/attach-ecs-capacity-provider/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/HandleResult.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import java.util.HashMap; -import java.util.Map; - -public class HandleResult { - private boolean success = false; - private Map responseData = new HashMap(); - - public void setSucceeded() { - this.success = true; - } - - public void setFailed() { - this.success = false; - } - - public void setResponseData(Map responseData) { - this.responseData = responseData; - } - - public void putResponseData(String key, Object value) { - this.responseData.put(key, value); - } - - public void putFailureReason(String reason) { - putResponseData("Reason", reason); - } - - public boolean succeeded() { - return this.success; - } - - public Map getResponseData() { - return this.responseData; - } -} \ No newline at end of file diff --git a/resources/custom-resources/attach-ecs-capacity-provider/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/RequestContext.java b/resources/custom-resources/attach-ecs-capacity-provider/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/RequestContext.java deleted file mode 100644 index 5c855109..00000000 --- a/resources/custom-resources/attach-ecs-capacity-provider/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/RequestContext.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -public final class RequestContext { - public final String requestType; - public final String ecsCluster; - public final String onboardingDdbTable; - public final String capacityProvider; - public final String tenantId; - - private RequestContext(Builder b) { - this.requestType = b.requestType; - this.ecsCluster = b.ecsCluster; - this.onboardingDdbTable = b.onboardingDdbTable; - this.capacityProvider = b.capacityProvider; - this.tenantId = b.tenantId; - } - - public static Builder builder() { - return new Builder(); - } - - public static Builder builder(RequestContext requestContext) { - return new Builder() - .requestType(requestContext.requestType) - .ecsCluster(requestContext.ecsCluster) - .onboardingDdbTable(requestContext.onboardingDdbTable) - .capacityProvider(requestContext.capacityProvider) - .tenantId(requestContext.tenantId); - } - - public static class Builder { - private String requestType; - private String ecsCluster; - private String onboardingDdbTable; - private String capacityProvider; - private String tenantId; - - public Builder requestType(String requestType) { - this.requestType = requestType; - return this; - } - - public Builder ecsCluster(String ecsCluster) { - this.ecsCluster = ecsCluster; - return this; - } - - public Builder onboardingDdbTable(String onboardingDdbTable) { - this.onboardingDdbTable = onboardingDdbTable; - return this; - } - - public Builder capacityProvider(String capacityProvider) { - this.capacityProvider = capacityProvider; - return this; - } - - public Builder tenantId(String tenantId) { - this.tenantId = tenantId; - return this; - } - - public RequestContext build() { - return new RequestContext(this); - } - } -} diff --git a/resources/custom-resources/attach-ecs-capacity-provider/src/main/resources/lambda-assembly.xml b/resources/custom-resources/attach-ecs-capacity-provider/src/main/resources/lambda-assembly.xml deleted file mode 100644 index 26364854..00000000 --- a/resources/custom-resources/attach-ecs-capacity-provider/src/main/resources/lambda-assembly.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - lambda - - zip - - false - - - - ${project.build.outputDirectory} - - com/amazon/aws/partners/saasfactory/** - log4j2.xml - git.properties - - - - - - false - true - lib - - - \ No newline at end of file diff --git a/resources/custom-resources/attach-ecs-capacity-provider/src/main/resources/log4j2.xml b/resources/custom-resources/attach-ecs-capacity-provider/src/main/resources/log4j2.xml deleted file mode 100644 index 04128110..00000000 --- a/resources/custom-resources/attach-ecs-capacity-provider/src/main/resources/log4j2.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} %X{AWSRequestId} %-5p %C{1} - %m%n - - - - - - - - - - - - diff --git a/resources/custom-resources/attach-ecs-capacity-provider/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AttachEcsCapacityProviderRequestHandlerTest.java b/resources/custom-resources/attach-ecs-capacity-provider/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AttachEcsCapacityProviderRequestHandlerTest.java deleted file mode 100644 index 8e32ca6a..00000000 --- a/resources/custom-resources/attach-ecs-capacity-provider/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AttachEcsCapacityProviderRequestHandlerTest.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import software.amazon.awssdk.services.ecs.EcsClient; -import software.amazon.awssdk.services.ecs.model.Cluster; -import software.amazon.awssdk.services.ecs.model.DescribeClustersRequest; -import software.amazon.awssdk.services.ecs.model.DescribeClustersResponse; -import software.amazon.awssdk.services.ecs.model.PutClusterCapacityProvidersRequest; - -import java.util.ArrayList; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -public class AttachEcsCapacityProviderRequestHandlerTest { - - private static final String CAPACITY_PROVIDER_1 = "capacityProvider1"; - private static final String CAPACITY_PROVIDER_2 = "capacityProvider2"; - private static final String NEW_CAPACITY_PROVIDER = "capacityProvider3"; - private static final List EXISTING_PROVIDERS = List.of(CAPACITY_PROVIDER_1, CAPACITY_PROVIDER_2); - private static final RequestContext BASE_REQUEST_CONTEXT = RequestContext.builder() - .requestType("Create") - .capacityProvider(NEW_CAPACITY_PROVIDER) - .ecsCluster("ecsCluster") - .onboardingDdbTable("onboardingDdbTable") - .tenantId("tenant-123-456") - .build(); - private static final ArgumentCaptor putRequestCaptor = - ArgumentCaptor.forClass(PutClusterCapacityProvidersRequest.class); - - CapacityProviderLock mockLock = mock(CapacityProviderLock.class); // already no-op? - EcsClient mockEcs = mock(EcsClient.class); - - @Before - public void setup() { - // when you describe clusters to look for capacity providers in ECS you find EXISTING_PROVIDERS - doReturn(DescribeClustersResponse.builder() - .clusters(Cluster.builder().capacityProviders(EXISTING_PROVIDERS).build()) - .build()).when(mockEcs).describeClusters(any(DescribeClustersRequest.class)); - // right now response is ignored - doReturn(null).when(mockEcs).putClusterCapacityProviders(putRequestCaptor.capture()); - } - - @Test - public void testCall_basicCreate() { - AttachCapacityProviderRequestHandler testHandler = new AttachCapacityProviderRequestHandler( - BASE_REQUEST_CONTEXT, mockLock, mockEcs); - List expectedProviders = new ArrayList<>(EXISTING_PROVIDERS); - expectedProviders.add(NEW_CAPACITY_PROVIDER); - testCall(testHandler, true, expectedProviders); - } - - @Test - public void testCall_basicUpdate() { - AttachCapacityProviderRequestHandler testHandler = new AttachCapacityProviderRequestHandler( - RequestContext.builder(BASE_REQUEST_CONTEXT).requestType("Update").build(), mockLock, mockEcs); - List expectedProviders = new ArrayList<>(EXISTING_PROVIDERS); - expectedProviders.add(NEW_CAPACITY_PROVIDER); - testCall(testHandler, true, expectedProviders); - } - - @Test - public void testCall_basicDelete() { - AttachCapacityProviderRequestHandler testHandler = new AttachCapacityProviderRequestHandler( - RequestContext.builder(BASE_REQUEST_CONTEXT) - .requestType("Delete") - .capacityProvider(CAPACITY_PROVIDER_1) - .build(), mockLock, mockEcs); - List expectedProviders = new ArrayList<>(EXISTING_PROVIDERS); - expectedProviders.remove(CAPACITY_PROVIDER_1); - testCall(testHandler, true, expectedProviders); - } - - @Test - public void testCall_addExistingCapacityProvider() { - AttachCapacityProviderRequestHandler testHandler = new AttachCapacityProviderRequestHandler( - RequestContext.builder(BASE_REQUEST_CONTEXT) - .requestType("Create") - .capacityProvider(CAPACITY_PROVIDER_1) - .build(), mockLock, mockEcs); - // pass null for expected capacity providers to indicate we shouldn't make a call to ECS - testCall(testHandler, true, null); - } - - @Test - public void testCall_removeNonExistingCapacityProvider() { - AttachCapacityProviderRequestHandler testHandler = new AttachCapacityProviderRequestHandler( - RequestContext.builder(BASE_REQUEST_CONTEXT) - .requestType("Delete") - .capacityProvider(NEW_CAPACITY_PROVIDER) - .build(), mockLock, mockEcs); - List expectedProviders = new ArrayList<>(EXISTING_PROVIDERS); - expectedProviders.remove(CAPACITY_PROVIDER_1); - testCall(testHandler, true, null); - } - - @Test - public void testCall_unknownRequestType() { - AttachCapacityProviderRequestHandler testHandler = new AttachCapacityProviderRequestHandler( - RequestContext.builder(BASE_REQUEST_CONTEXT) - .requestType("UNKNOWN") - .capacityProvider(CAPACITY_PROVIDER_1) - .build(), mockLock, mockEcs); - testCall(testHandler, false, null); - } - - private void testCall(AttachCapacityProviderRequestHandler handler, - boolean expectSuccess, List expectedPassedCapacityProviders) { - // start start - HandleResult result = handler.call(); - assertEquals(expectSuccess, result.succeeded()); - if (expectSuccess) { - verify(mockLock, times(1)).lock(any(RequestContext.class)); - verify(mockLock, times(1)).unlock(any(RequestContext.class)); - if (expectedPassedCapacityProviders != null) { - assertEquals(expectedPassedCapacityProviders, putRequestCaptor.getValue().capacityProviders()); - } else { - verify(mockEcs, times(0)).putClusterCapacityProviders(any(PutClusterCapacityProvidersRequest.class)); - } - } - } -} \ No newline at end of file diff --git a/resources/custom-resources/attach-ecs-capacity-provider/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CapacityProviderLockTest.java b/resources/custom-resources/attach-ecs-capacity-provider/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CapacityProviderLockTest.java deleted file mode 100644 index 9dadc5a2..00000000 --- a/resources/custom-resources/attach-ecs-capacity-provider/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CapacityProviderLockTest.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.amazon.aws.partners.saasfactory.saasboost; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import java.util.List; -import java.util.Map; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; -import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; -import software.amazon.awssdk.services.dynamodb.model.InternalServerErrorException; -import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; -import software.amazon.awssdk.services.dynamodb.model.ScanRequest; -import software.amazon.awssdk.services.dynamodb.model.ScanResponse; -import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; -import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse; - -public final class CapacityProviderLockTest { - private static final String ONBOARDING_DDB_TABLE = "onboarding"; - private static final String TENANT_ID = "123-456"; - private static final String ONBOARDING_ID = "onb-123-456"; - private static final RequestContext TEST_CONTEXT = RequestContext.builder() - .requestType("Create") - .ecsCluster("ecsCluster") - .onboardingDdbTable(ONBOARDING_DDB_TABLE) - .capacityProvider("capacityProvider") - .tenantId(TENANT_ID) - .build(); - - private DynamoDbClient mockDdb; - private CapacityProviderLock testLock; - - @Before - public void setup() { - mockDdb = mock(DynamoDbClient.class); - testLock = new CapacityProviderLock(mockDdb); - } - - /** - * trylock something already locked - * tryunlock something not locked - * trylock happy case - * tryunlock happy case - * verify each test does a scan - * verify each test does an update - * verify a conditional update fail means false - */ - - @Test - public void getOnboardingId_basic() { - final ArgumentCaptor scanCaptor = ArgumentCaptor.forClass(ScanRequest.class); - final AttributeValue onboardingId = AttributeValue.builder().s("onb-123-456").build(); - final AttributeValue tenantIdAttributeValue = AttributeValue.builder().s(TENANT_ID).build(); - doReturn(ScanResponse.builder().items(List.of(Map.of("id", onboardingId))).build()) - .when(mockDdb).scan(scanCaptor.capture()); - AttributeValue foundOnboardingId = testLock.currentOnboardingId(TEST_CONTEXT); - assertEquals(onboardingId, foundOnboardingId); - assertTrue("scan for onboarding ID should include the tenant id passed in request context", - scanCaptor.getValue().expressionAttributeValues().values().contains(tenantIdAttributeValue)); - - doReturn(ScanResponse.builder().build()).when(mockDdb).scan(any(ScanRequest.class)); - // assert that we cache onboardingId, since it should not change for the lifetime of the lambda - assertEquals(onboardingId, testLock.currentOnboardingId(TEST_CONTEXT)); - verify(mockDdb, times(1)).scan(any(ScanRequest.class)); - } - - @Test(expected = RuntimeException.class) - public void getOnboardingId_scanFailure() { - doThrow(ResourceNotFoundException.builder().build()).when(mockDdb).scan(any(ScanRequest.class)); - testLock.currentOnboardingId(TEST_CONTEXT); - } - - @Test - public void tryLockUnlock_basic() { - final AttributeValue onboardingId = AttributeValue.builder().s(ONBOARDING_ID).build(); - doReturn(ScanResponse.builder().items(List.of(Map.of("id", onboardingId))).build()) - .when(mockDdb).scan(any(ScanRequest.class)); - - final ArgumentCaptor updateCaptor = ArgumentCaptor.forClass(UpdateItemRequest.class); - doReturn(UpdateItemResponse.builder().build()).when(mockDdb).updateItem(updateCaptor.capture()); - - boolean success = testLock.tryLockUnlock(TEST_CONTEXT, true); - UpdateItemRequest actualRequest = updateCaptor.getValue(); - assertEquals(onboardingId, actualRequest.key().get("id")); - assertEquals("ecs_cluster_locked = :lock_expected", actualRequest.conditionExpression()); - assertEquals("SET ecs_cluster_locked = :new_lock", actualRequest.updateExpression()); - assertEquals(AttributeValue.builder().bool(false).build(), actualRequest.expressionAttributeValues().get(":lock_expected")); - assertEquals(AttributeValue.builder().bool(true).build(), actualRequest.expressionAttributeValues().get(":new_lock")); - assertTrue(success); - - success = testLock.tryLockUnlock(TEST_CONTEXT, false); - actualRequest = updateCaptor.getValue(); - assertEquals(onboardingId, actualRequest.key().get("id")); - assertEquals("ecs_cluster_locked = :lock_expected", actualRequest.conditionExpression()); - assertEquals("SET ecs_cluster_locked = :new_lock", actualRequest.updateExpression()); - assertEquals(AttributeValue.builder().bool(true).build(), actualRequest.expressionAttributeValues().get(":lock_expected")); - assertEquals(AttributeValue.builder().bool(false).build(), actualRequest.expressionAttributeValues().get(":new_lock")); - assertTrue(success); - } - - @Test - public void tryLockUnlock_conditionNotMet() { - final AttributeValue onboardingId = AttributeValue.builder().s(ONBOARDING_ID).build(); - doReturn(ScanResponse.builder().items(List.of(Map.of("id", onboardingId))).build()) - .when(mockDdb).scan(any(ScanRequest.class)); - - doThrow(ConditionalCheckFailedException.builder().build()).when(mockDdb).updateItem(any(UpdateItemRequest.class)); - - boolean success = testLock.tryLockUnlock(TEST_CONTEXT, true); - assertFalse(success); - } - - @Test(expected = RuntimeException.class) - public void tryLockUnlock_unexpectedException() { - final AttributeValue onboardingId = AttributeValue.builder().s(ONBOARDING_ID).build(); - doReturn(ScanResponse.builder().items(List.of(Map.of("id", onboardingId))).build()) - .when(mockDdb).scan(any(ScanRequest.class)); - - doThrow(InternalServerErrorException.builder().build()).when(mockDdb).updateItem(any(UpdateItemRequest.class)); - testLock.tryLockUnlock(TEST_CONTEXT, true); - } -} diff --git a/resources/custom-resources/attach-ecs-capacity-provider/src/test/resources/template.json b/resources/custom-resources/attach-ecs-capacity-provider/src/test/resources/template.json deleted file mode 100644 index 7d020875..00000000 --- a/resources/custom-resources/attach-ecs-capacity-provider/src/test/resources/template.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "AWSTemplateFormatVersion": "2010-09-09", - "Description": "Resource Name Macro Test", - "Parameters": { - "ApplicationServices": { - "Type": "String", - "Default": "" - } - }, - "Resources": { - } -} \ No newline at end of file diff --git a/resources/custom-resources/cidr-dynamodb/pom.xml b/resources/custom-resources/cidr-dynamodb/pom.xml deleted file mode 100644 index 39bf8a10..00000000 --- a/resources/custom-resources/cidr-dynamodb/pom.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - - 4.0.0 - - com.amazon.aws.partners.saasfactory.saasboost - saasboost-custom-resources - 1.0.0 - - CidrDynamoDB - 1.0.0 - jar - - - Apache-2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - - 0 - - - - ${project.artifactId} - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - org.apache.maven.plugins - maven-assembly-plugin - - - io.github.git-commit-id - git-commit-id-maven-plugin - - - - - - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - - - com.amazon.aws.partners.saasfactory.saasboost - CloudFormationUtils - 1.0.0 - - provided - - - software.amazon.awssdk - dynamodb - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - - diff --git a/resources/custom-resources/cidr-dynamodb/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CidrDynamoDB.java b/resources/custom-resources/cidr-dynamodb/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CidrDynamoDB.java deleted file mode 100644 index 32368216..00000000 --- a/resources/custom-resources/cidr-dynamodb/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CidrDynamoDB.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; -import software.amazon.awssdk.services.dynamodb.model.*; - -import java.util.*; -import java.util.concurrent.*; - -public class CidrDynamoDB implements RequestHandler, Object> { - - private static final Logger LOGGER = LoggerFactory.getLogger(CidrDynamoDB.class); - private final DynamoDbClient ddb; - - public CidrDynamoDB() { - LOGGER.info("Version Info: {}", Utils.version(this.getClass())); - this.ddb = Utils.sdkClient(DynamoDbClient.builder(), DynamoDbClient.SERVICE_NAME); - } - - @Override - public Object handleRequest(Map event, Context context) { - Utils.logRequestEvent(event); - - final String requestType = (String) event.get("RequestType"); - Map resourceProperties = (Map) event.get("ResourceProperties"); - final String table = (String) resourceProperties.get("Table"); - - ExecutorService service = Executors.newSingleThreadExecutor(); - Map responseData = new HashMap<>(); - try { - Runnable r = () -> { - if ("Create".equalsIgnoreCase(requestType) || "Update".equalsIgnoreCase(requestType)) { - LOGGER.info("CREATE or UPDATE"); - try { - ScanResponse scan = ddb.scan(request -> request.tableName(table)); - // ScanResponse::hasItems will return true even with an empty list - if (scan.hasItems() && !scan.items().isEmpty()) { - LOGGER.info("CIDR table {} is already populated with {} items", table, scan.count()); - } else { - LOGGER.info("Populating CIDR table"); - List> batches = generateBatches(); - for (List batch : batches) { - try { - ddb.batchWriteItem(request -> request.requestItems(Map.of(table, batch))); - } catch (DynamoDbException e) { - LOGGER.error(Utils.getFullStackTrace(e)); - responseData.put("Reason", e.awsErrorDetails().errorMessage()); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } - } - } - CloudFormationResponse.send(event, context, "SUCCESS", responseData); - } catch (DynamoDbException e) { - LOGGER.error(Utils.getFullStackTrace(e)); - responseData.put("Reason", e.awsErrorDetails().errorMessage()); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } - } else if ("Delete".equalsIgnoreCase(requestType)) { - LOGGER.info("DELETE"); - CloudFormationResponse.send(event, context, "SUCCESS", responseData); - } else { - LOGGER.error("FAILED unknown requestType " + requestType); - responseData.put("Reason", "Unknown RequestType " + requestType); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } - }; - Future f = service.submit(r); - f.get(context.getRemainingTimeInMillis() - 1000, TimeUnit.MILLISECONDS); - } catch (final TimeoutException | InterruptedException | ExecutionException e) { - // Timed out - LOGGER.error("FAILED unexpected error or request timed out", e); - String stackTrace = Utils.getFullStackTrace(e); - LOGGER.error(stackTrace); - responseData.put("Reason", stackTrace); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } finally { - service.shutdown(); - } - return null; - } - - protected static List> generateBatches() { - final int batchWriteItemLimit = 25; - final int maxOctet = 255; - int octet = -1; - List> batches = new ArrayList<>(); - List batch = new ArrayList<>(); - while (octet <= maxOctet) { - octet++; - if (batch.size() == batchWriteItemLimit || octet > maxOctet) { - batches.add(new ArrayList<>(batch)); // shallow copy is ok here - batch.clear(); // clear out our working batch so we can fill it up again to the limit - } - String cidr = String.format("10.%d.0.0", octet); - WriteRequest putRequest = WriteRequest.builder() - .putRequest(PutRequest.builder() - .item(Map.of("cidr_block", AttributeValue.builder().s(cidr).build())) - .build()) - .build(); - batch.add(putRequest); - } - return batches; - } -} \ No newline at end of file diff --git a/resources/custom-resources/cidr-dynamodb/src/main/resources/lambda-assembly.xml b/resources/custom-resources/cidr-dynamodb/src/main/resources/lambda-assembly.xml deleted file mode 100644 index 26364854..00000000 --- a/resources/custom-resources/cidr-dynamodb/src/main/resources/lambda-assembly.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - lambda - - zip - - false - - - - ${project.build.outputDirectory} - - com/amazon/aws/partners/saasfactory/** - log4j2.xml - git.properties - - - - - - false - true - lib - - - \ No newline at end of file diff --git a/resources/custom-resources/cidr-dynamodb/src/main/resources/log4j2.xml b/resources/custom-resources/cidr-dynamodb/src/main/resources/log4j2.xml deleted file mode 100644 index 04128110..00000000 --- a/resources/custom-resources/cidr-dynamodb/src/main/resources/log4j2.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} %X{AWSRequestId} %-5p %C{1} - %m%n - - - - - - - - - - - - diff --git a/resources/custom-resources/cidr-dynamodb/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CidrDynamoDBTest.java b/resources/custom-resources/cidr-dynamodb/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CidrDynamoDBTest.java deleted file mode 100644 index be952cfb..00000000 --- a/resources/custom-resources/cidr-dynamodb/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CidrDynamoDBTest.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Ignore; -import org.junit.Test; -import software.amazon.awssdk.services.dynamodb.model.WriteRequest; - -import java.util.*; - -import static org.junit.Assert.*; - -public class CidrDynamoDBTest { - - @Test - public void testGenerateBatches() { - List> batches = CidrDynamoDB.generateBatches(); - // Max batch write size for DynamoDB is 25 and we're batching up 256 items - // We should have 11 batches total - assertEquals(11, batches.size()); - // The first 10 batches should be filled to the limit - for (int i = 0; i < 10; i++) { - assertEquals(25, batches.get(i).size()); - } - // and one remainder batch of 6 - assertEquals(6, batches.get(10).size()); - } -} \ No newline at end of file diff --git a/resources/custom-resources/cidr-dynamodb/update.sh b/resources/custom-resources/cidr-dynamodb/update.sh deleted file mode 100755 index 64a66a68..00000000 --- a/resources/custom-resources/cidr-dynamodb/update.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -if [ -z $1 ]; then - echo "Usage: $0 [Lambda Folder]" - exit 2 -fi - -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -echo "AWS Region = $MY_AWS_REGION" - -ENVIRONMENT=$1 -LAMBDA_STAGE_FOLDER=$2 -if [ -z $LAMBDA_STAGE_FOLDER ]; then - LAMBDA_STAGE_FOLDER="lambdas" -fi -LAMBDA_CODE=CidrDynamoDB-lambda.zip - -#set this for V2 AWS CLI to disable paging -export AWS_PAGER="" - -SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) -echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" -if [ -z $SAAS_BOOST_BUCKET ]; then - echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" - exit 1 -fi - -# Do a fresh build of the project -mvn -if [ $? -ne 0 ]; then - echo "Error building project" - exit 1 -fi - -# And copy it up to S3 -aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ - -printf "Updating function code for sb-${ENVIRONMENT}-populate-ddb\n" -eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-populate-ddb\`)] | [].FunctionName' --output text"\) - -for FUNCTION in ${FUNCTIONS[@]}; do - #echo $FUNCTION - aws lambda --region $MY_AWS_REGION update-function-code --function-name $FUNCTION --s3-bucket $SAAS_BOOST_BUCKET --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE -done \ No newline at end of file diff --git a/resources/custom-resources/clear-ecr-repo/pom.xml b/resources/custom-resources/clear-ecr-repo/pom.xml index 147643bc..ff567f98 100644 --- a/resources/custom-resources/clear-ecr-repo/pom.xml +++ b/resources/custom-resources/clear-ecr-repo/pom.xml @@ -31,10 +31,12 @@ limitations under the License. http://www.apache.org/licenses/LICENSE-2.0 + + ${project.basedir}/../../.. 0 - + ${project.artifactId} diff --git a/resources/custom-resources/clear-s3-bucket/pom.xml b/resources/custom-resources/clear-s3-bucket/pom.xml index 32bfb82c..b17ba18f 100644 --- a/resources/custom-resources/clear-s3-bucket/pom.xml +++ b/resources/custom-resources/clear-s3-bucket/pom.xml @@ -31,10 +31,12 @@ limitations under the License. http://www.apache.org/licenses/LICENSE-2.0 + + ${project.basedir}/../../.. 0 - + ${project.artifactId} diff --git a/resources/custom-resources/clear-s3-bucket/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ClearS3Bucket.java b/resources/custom-resources/clear-s3-bucket/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ClearS3Bucket.java index e1460780..f066a8be 100644 --- a/resources/custom-resources/clear-s3-bucket/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ClearS3Bucket.java +++ b/resources/custom-resources/clear-s3-bucket/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ClearS3Bucket.java @@ -73,36 +73,34 @@ public Object handleRequest(Map event, Context context) { String keyMarker = null; String versionIdMarker = null; do { - ListObjectVersionsRequest request; - if (Utils.isNotBlank(keyMarker) && Utils.isNotBlank(versionIdMarker)) { - request = ListObjectVersionsRequest.builder() - .bucket(bucket) - .prefix(prefix) - .keyMarker(keyMarker) - .versionIdMarker(versionIdMarker) - .build(); - } else if (Utils.isNotBlank(keyMarker)) { - request = ListObjectVersionsRequest.builder() - .bucket(bucket) - .prefix(prefix) - .keyMarker(keyMarker) - .build(); - } else { - request = ListObjectVersionsRequest.builder() - .bucket(bucket) - .prefix(prefix) - .build(); + ListObjectVersionsRequest.Builder request = ListObjectVersionsRequest.builder() + .bucket(bucket) + .prefix(prefix); + if (Utils.isNotBlank(versionIdMarker)) { + request = request.versionIdMarker(versionIdMarker); + } + if (Utils.isNotBlank(keyMarker)) { + request = request.keyMarker(keyMarker); } - response = s3.listObjectVersions(request); + response = s3.listObjectVersions(request.build()); keyMarker = response.nextKeyMarker(); versionIdMarker = response.nextVersionIdMarker(); response.versions() .stream() .map(version -> - ObjectIdentifier.builder() - .key(version.key()) - .versionId(version.versionId()) - .build() + ObjectIdentifier.builder() + .key(version.key()) + .versionId(version.versionId()) + .build() + ) + .forEachOrdered(toDelete::add); + response.deleteMarkers() + .stream() + .map(marker -> + ObjectIdentifier.builder() + .key(marker.key()) + .versionId(marker.versionId()) + .build() ) .forEachOrdered(toDelete::add); } while (response.isTruncated()); @@ -111,20 +109,14 @@ public Object handleRequest(Map event, Context context) { ListObjectsV2Response response; String token = null; do { - ListObjectsV2Request request; + ListObjectsV2Request.Builder request = ListObjectsV2Request.builder() + .bucket(bucket) + .prefix(prefix); if (Utils.isNotBlank(token)) { - request = ListObjectsV2Request.builder() - .bucket(bucket) - .prefix(prefix) - .continuationToken(token) - .build(); - } else { - request = ListObjectsV2Request.builder() - .bucket(bucket) - .prefix(prefix) - .build(); + request = request.continuationToken(token); + } - response = s3.listObjectsV2(request); + response = s3.listObjectsV2(request.build()); token = response.nextContinuationToken(); response.contents() .stream() diff --git a/resources/custom-resources/cognito-app-client-details/pom.xml b/resources/custom-resources/cognito-app-client-details/pom.xml index 14ea77ab..2570b481 100644 --- a/resources/custom-resources/cognito-app-client-details/pom.xml +++ b/resources/custom-resources/cognito-app-client-details/pom.xml @@ -31,10 +31,12 @@ limitations under the License. http://www.apache.org/licenses/LICENSE-2.0 + + ${project.basedir}/../../.. 0 - + ${project.artifactId} @@ -51,27 +53,13 @@ limitations under the License. maven-assembly-plugin
    - pl.project13.maven - git-commit-id-plugin + io.github.git-commit-id + git-commit-id-maven-plugin - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - - - com.amazon.aws.partners.saasfactory.saasboost - CloudFormationUtils - 1.0.0 - - provided - software.amazon.awssdk cognitoidentityprovider diff --git a/resources/custom-resources/customize-cognito-ui/pom.xml b/resources/custom-resources/customize-cognito-ui/pom.xml index c6248e09..24139a87 100644 --- a/resources/custom-resources/customize-cognito-ui/pom.xml +++ b/resources/custom-resources/customize-cognito-ui/pom.xml @@ -31,10 +31,12 @@ limitations under the License. http://www.apache.org/licenses/LICENSE-2.0 + + ${project.basedir}/../../.. 0 - + ${project.artifactId} @@ -51,8 +53,8 @@ limitations under the License. maven-assembly-plugin - pl.project13.maven - git-commit-id-plugin + io.github.git-commit-id + git-commit-id-maven-plugin diff --git a/resources/custom-resources/customize-cognito-ui/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CustomizeCognitoUi.java b/resources/custom-resources/customize-cognito-ui/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CustomizeCognitoUi.java index cbcfc9fa..ad60c8ff 100644 --- a/resources/custom-resources/customize-cognito-ui/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CustomizeCognitoUi.java +++ b/resources/custom-resources/customize-cognito-ui/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CustomizeCognitoUi.java @@ -67,6 +67,7 @@ public Object handleRequest(Map event, Context context) { final String requestType = (String) event.get("RequestType"); final Map resourceProperties = (Map) event.get("ResourceProperties"); final String sourceBucket = (String) resourceProperties.get("SourceBucket"); + final String sourceBucketPrefix = (String) resourceProperties.get("SourceBucketPrefix"); final String userPoolId = (String) resourceProperties.get("UserPoolId"); final String userPoolDomain = (String) resourceProperties.get("UserPoolDomain"); @@ -78,9 +79,14 @@ public Object handleRequest(Map event, Context context) { LOGGER.info("CREATE or UPDATE"); try { // Fetch the admin web app source from S3 + String bucketPrefix = Objects.toString(sourceBucketPrefix, ""); + if (Utils.isNotEmpty(bucketPrefix) && !bucketPrefix.endsWith("/")) { + bucketPrefix = bucketPrefix + "/"; + } + String sourceKey = bucketPrefix + ADMIN_WEB_SOURCE_KEY; ResponseInputStream responseInputStream = s3.getObject(request -> request .bucket(sourceBucket) - .key(ADMIN_WEB_SOURCE_KEY) + .key(sourceKey) .build()); // Customize the background colors to match the SaaS Boost branding StringBuilder css = new StringBuilder(); diff --git a/resources/custom-resources/fsx-dns-name/pom.xml b/resources/custom-resources/fsx-dns-name/pom.xml deleted file mode 100644 index bb0fef6c..00000000 --- a/resources/custom-resources/fsx-dns-name/pom.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - - 4.0.0 - - com.amazon.aws.partners.saasfactory.saasboost - saasboost-custom-resources - 1.0.0 - - FsxDnsName - 1.0.0 - jar - - - Apache-2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - - 0 - - - - ${project.artifactId} - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - org.apache.maven.plugins - maven-assembly-plugin - - - io.github.git-commit-id - git-commit-id-maven-plugin - - - - - - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - - - com.amazon.aws.partners.saasfactory.saasboost - CloudFormationUtils - 1.0.0 - - provided - - - software.amazon.awssdk - fsx - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - - diff --git a/resources/custom-resources/fsx-dns-name/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/FsxDnsName.java b/resources/custom-resources/fsx-dns-name/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/FsxDnsName.java deleted file mode 100644 index c2e23763..00000000 --- a/resources/custom-resources/fsx-dns-name/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/FsxDnsName.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.services.fsx.FSxClient; -import software.amazon.awssdk.services.fsx.model.DescribeFileSystemsResponse; -import software.amazon.awssdk.services.fsx.model.DescribeStorageVirtualMachinesResponse; -import software.amazon.awssdk.services.fsx.model.FSxException; -import software.amazon.awssdk.services.fsx.model.StorageVirtualMachineFilter; - -import java.util.*; -import java.util.concurrent.*; - -public class FsxDnsName implements RequestHandler, Object> { - - private static final Logger LOGGER = LoggerFactory.getLogger(FsxDnsName.class); - private static final String AWS_REGION = System.getenv("AWS_REGION"); - private final FSxClient fsx; - - public FsxDnsName() { - if (Utils.isBlank(AWS_REGION)) { - throw new IllegalStateException("Missing required environment variable AWS_REGION"); - } - LOGGER.info("Version Info: {}", Utils.version(this.getClass())); - this.fsx = Utils.sdkClient(FSxClient.builder(), FSxClient.SERVICE_NAME); - } - - @Override - public Object handleRequest(Map event, Context context) { - Utils.logRequestEvent(event); - - final String requestType = (String) event.get("RequestType"); - final Map resourceProperties = (Map) event.get("ResourceProperties"); - final String fileSystemId = (String) resourceProperties.get("FsxFileSystemId"); - final String storageVirtualMachineId = (String) resourceProperties.get("StorageVirtualMachineId"); - final String securityStyle = (String) resourceProperties.get("VolumeSecurityStyle"); - - ExecutorService service = Executors.newSingleThreadExecutor(); - Map responseData = new HashMap<>(); - try { - Runnable r = () -> { - if ("Create".equalsIgnoreCase(requestType) || "Update".equalsIgnoreCase(requestType)) { - LOGGER.info("CREATE or UPDATE"); - try { - String fsxDns; - if (Utils.isNotBlank(storageVirtualMachineId)) { - LOGGER.info("Querying for Storage Virtual Machine DNS hostname"); - // FSx for NetApp ONTAP uses Storage Virtual Machines and the hostname the EC2 - // instance needs to mount is an attribute of the SMB endpoints of that SVM - // not the file system itself like it is for FSx for Windows File Server - DescribeStorageVirtualMachinesResponse response = fsx.describeStorageVirtualMachines( - request -> request - .storageVirtualMachineIds(List.of(storageVirtualMachineId)) - .filters(StorageVirtualMachineFilter.builder() - .name("file-system-id") - .values(fileSystemId) - .build() - ) - ); - LOGGER.info("SVM response: " + Objects.toString(response, "null")); - if (Utils.isNotBlank(securityStyle)) { - LOGGER.info("Reading Storage Virtual Machine NFS DNS hostname"); - fsxDns = response.storageVirtualMachines().get(0).endpoints().nfs().dnsName(); - } else { - LOGGER.info("Reading for Storage Virtual Machine SMB DNS hostname"); - fsxDns = response.storageVirtualMachines().get(0).endpoints().smb().dnsName(); - } - } else { - LOGGER.info("Querying for File System DNS hostname"); - DescribeFileSystemsResponse response = fsx.describeFileSystems(request -> request - .fileSystemIds(fileSystemId) - ); - LOGGER.info("File System response: " + Objects.toString(response, "null")); - fsxDns = response.fileSystems().get(0).dnsName(); - } - responseData.put("DnsName", fsxDns); - LOGGER.info("responseDate: " + Utils.toJson(responseData)); - CloudFormationResponse.send(event, context, "SUCCESS", responseData); - } catch (FSxException e) { - LOGGER.error(Utils.isBlank(storageVirtualMachineId) - ? "fsx:DescribeFileSystems" : "fsx:DescribeStorageVirtualMachines", e); - LOGGER.error(Utils.getFullStackTrace(e)); - responseData.put("Reason", e.awsErrorDetails().errorMessage()); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } catch (Exception e) { - LOGGER.error(Utils.isBlank(storageVirtualMachineId) - ? "fsx:DescribeFileSystems" : "fsx:DescribeStorageVirtualMachines", e); - LOGGER.error(Utils.getFullStackTrace(e)); - responseData.put("Reason", Objects.toString(e.getMessage(), e.toString())); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } - } else if ("Delete".equalsIgnoreCase(requestType)) { - LOGGER.info("DELETE"); - CloudFormationResponse.send(event, context, "SUCCESS", responseData); - } else { - LOGGER.error("FAILED unknown requestType " + requestType); - responseData.put("Reason", "Unknown RequestType " + requestType); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } - }; - Future f = service.submit(r); - f.get(context.getRemainingTimeInMillis() - 1000, TimeUnit.MILLISECONDS); - } catch (final TimeoutException | InterruptedException | ExecutionException e) { - // Timed out - LOGGER.error("FAILED unexpected error or request timed out " + e.getMessage()); - LOGGER.error(Utils.getFullStackTrace(e)); - responseData.put("Reason", e.getMessage()); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } finally { - service.shutdown(); - } - return null; - } - -} \ No newline at end of file diff --git a/resources/custom-resources/fsx-dns-name/src/main/resources/lambda-assembly.xml b/resources/custom-resources/fsx-dns-name/src/main/resources/lambda-assembly.xml deleted file mode 100644 index 26364854..00000000 --- a/resources/custom-resources/fsx-dns-name/src/main/resources/lambda-assembly.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - lambda - - zip - - false - - - - ${project.build.outputDirectory} - - com/amazon/aws/partners/saasfactory/** - log4j2.xml - git.properties - - - - - - false - true - lib - - - \ No newline at end of file diff --git a/resources/custom-resources/fsx-dns-name/src/main/resources/log4j2.xml b/resources/custom-resources/fsx-dns-name/src/main/resources/log4j2.xml deleted file mode 100644 index 04128110..00000000 --- a/resources/custom-resources/fsx-dns-name/src/main/resources/log4j2.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} %X{AWSRequestId} %-5p %C{1} - %m%n - - - - - - - - - - - - diff --git a/resources/custom-resources/keycloak-setup/pom.xml b/resources/custom-resources/keycloak-setup/pom.xml index 52a906e6..de994710 100644 --- a/resources/custom-resources/keycloak-setup/pom.xml +++ b/resources/custom-resources/keycloak-setup/pom.xml @@ -31,10 +31,12 @@ limitations under the License. http://www.apache.org/licenses/LICENSE-2.0 + - 0 + ${project.basedir}/../../.. + 10 - + ${project.artifactId} @@ -51,8 +53,8 @@ limitations under the License. maven-assembly-plugin - pl.project13.maven - git-commit-id-plugin + io.github.git-commit-id + git-commit-id-maven-plugin @@ -72,6 +74,13 @@ limitations under the License. provided + + com.amazon.aws.partners.saasfactory.saasboost + KeycloakHelper + 1.0.0 + + provided + software.amazon.awssdk secretsmanager diff --git a/resources/custom-resources/keycloak-setup/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/KeycloakSetup.java b/resources/custom-resources/keycloak-setup/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/KeycloakSetup.java deleted file mode 100644 index c22764a7..00000000 --- a/resources/custom-resources/keycloak-setup/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/KeycloakSetup.java +++ /dev/null @@ -1,596 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.core.exception.SdkServiceException; -import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; -import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.*; -import java.util.concurrent.*; - -public class KeycloakSetup implements RequestHandler, Object> { - - private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakSetup.class); - private static final String RESPONSE_DATA_KEY_KEYCLOAK_REALM = "KeycloakRealm"; - private static final String RESPONSE_DATA_KEY_WEB_APP_CLIENT_ID = "AdminWebAppClientId"; - private static final String RESPONSE_DATA_KEY_WEB_APP_CLIENT_NAME = "AdminWebAppClientName"; - private static final String RESPONSE_DATA_KEY_API_APP_CLIENT_ID = "ApiAppClientId"; - private static final String RESPONSE_DATA_KEY_API_APP_CLIENT_NAME = "ApiAppClientName"; - private static final String RESPONSE_DATA_KEY_API_APP_CLIENT_SECRET = "ApiAppClientSecret"; - private final HttpClient httpClient = HttpClient.newBuilder().build(); - private final SecretsManagerClient secrets; - - public KeycloakSetup() { - LOGGER.info("Version Info: {}", Utils.version(this.getClass())); - secrets = Utils.sdkClient(SecretsManagerClient.builder(), SecretsManagerClient.SERVICE_NAME); - } - - @Override - public Object handleRequest(Map event, Context context) { - Utils.logRequestEvent(event); - - final String requestType = (String) event.get("RequestType"); - final Map resourceProperties = (Map) event.get("ResourceProperties"); - final String keycloakHost = (String) resourceProperties.get("KeycloakHost"); - final String keycloakSecretId = (String) resourceProperties.get("KeycloakCredentials"); - final String realm = (String) resourceProperties.get("Realm"); - final String adminUserSecretId = (String) resourceProperties.get("AdminUserCredentials"); - String adminWebAppUrl = (String) resourceProperties.get("AdminWebAppUrl"); - final String redirectUriPattern = (!adminWebAppUrl.endsWith("/*")) ? adminWebAppUrl + "/*" : adminWebAppUrl; - ExecutorService service = Executors.newSingleThreadExecutor(); - Map responseData = new HashMap<>(); - try { - Runnable r = () -> { - if ("Create".equalsIgnoreCase(requestType)) { - LOGGER.info("CREATE"); - try { - LOGGER.info("Fetching SaaS Boost admin user credentials from Secrets Manager"); - GetSecretValueResponse adminUserSecretValue = secrets.getSecretValue(request -> request - .secretId(adminUserSecretId) - ); - final Map adminUserCredentials = Utils.fromJson( - adminUserSecretValue.secretString(), LinkedHashMap.class); - - LOGGER.info("Fetching Keycloak super user credentials from Secrets Manager"); - GetSecretValueResponse keycloakSecretValue = secrets.getSecretValue(request -> request - .secretId(keycloakSecretId) - ); - final Map keycloakCredentials = Utils.fromJson( - keycloakSecretValue.secretString(), LinkedHashMap.class); - - LOGGER.info("Executing admin password grant endpoint"); - Map passwordGrant = adminPasswordGrant(keycloakHost, - keycloakCredentials.get("username"), - keycloakCredentials.get("password") - ); - String bearerToken = (String) passwordGrant.get("access_token"); - if (Utils.isBlank(bearerToken)) { - throw new RuntimeException("Admin password grant doesn't contain an Access Token"); - } - - // Admin password grant access token expires in 60 seconds! - LOGGER.info("Executing realm import endpoint"); - Map keycloakRealmSetupResults = setupKeycloak(keycloakHost, bearerToken, realm, - adminUserCredentials.get("username"), adminUserCredentials.get("password"), - adminUserCredentials.get("email"), redirectUriPattern); - - if (keycloakRealmSetupResults != null && !keycloakRealmSetupResults.isEmpty()) { - responseData.putAll(keycloakRealmSetupResults); - // We're returning sensitive data, so be sure to use NoEcho = true - // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html - CloudFormationResponse.send(event, context, "SUCCESS", responseData, true); - } else { - responseData.put("Reason", "Keycloak setup did not return app client details"); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } - } catch (SdkServiceException secretsManagerError) { - LOGGER.error("Secrets Manager error {}", secretsManagerError.getMessage()); - LOGGER.error(Utils.getFullStackTrace(secretsManagerError)); - responseData.put("Reason", secretsManagerError.getMessage()); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } catch (Exception e) { - LOGGER.error(Utils.getFullStackTrace(e)); - responseData.put("Reason", e.getMessage()); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } - } else if ("Update".equalsIgnoreCase(requestType)) { - LOGGER.info("UPDATE"); - CloudFormationResponse.send(event, context, "SUCCESS", responseData); - } else if ("Delete".equalsIgnoreCase(requestType)) { - LOGGER.info("DELETE"); - CloudFormationResponse.send(event, context, "SUCCESS", responseData); - } else { - LOGGER.error("FAILED unknown requestType " + requestType); - responseData.put("Reason", "Unknown RequestType " + requestType); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } - }; - Future f = service.submit(r); - f.get(context.getRemainingTimeInMillis() - 1000, TimeUnit.MILLISECONDS); - } catch (final TimeoutException | InterruptedException | ExecutionException e) { - // Timed out - LOGGER.error("FAILED unexpected error or request timed out " + e.getMessage()); - LOGGER.error(Utils.getFullStackTrace(e)); - responseData.put("Reason", e.getMessage()); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } finally { - service.shutdown(); - } - return null; - } - - protected Map setupKeycloak(String keycloakHost, String bearerToken, String realmName, - String username, String password, String email, - String redirectUriPattern) { - // Initial SaaS Boost admin user - Map adminUser = buildKeycloakUser(username, email, password); - - // Public OAuth app client with PKCE for the admin web app - String adminWebAppClientId = Utils.randomString(20); - String adminWebAppClientName = realmName + "-admin-webapp-client"; - String adminWebAppClientDescription = "SaaS Boost Admin Web App Client"; - Map adminWebAppClient = buildPublicAppClient(adminWebAppClientName, - adminWebAppClientId, redirectUriPattern, adminWebAppClientDescription); - - // Private OAuth app client with secret for service-to-service API calls - String apiAppClientId = Utils.randomString(20); - String apiAppClientName = realmName + "-api-client"; - String apiAppClientDescription = "SaaS Boost API App Client"; - Map apiAppClient = buildPrivateAppClient(apiAppClientName, apiAppClientId, - apiAppClientDescription); - - // Keycloak realm for this SaaS Boost environment - Map realm = buildRealm(realmName, List.of(adminWebAppClient, apiAppClient), List.of(adminUser)); - - // Return newly generated app client data back to CloudFormation - final Map setupResults = new HashMap<>(); - setupResults.put(RESPONSE_DATA_KEY_KEYCLOAK_REALM, realmName); - - int importRealmResponse = postRealm(keycloakHost, bearerToken, realm); - if (HttpURLConnection.HTTP_CREATED == importRealmResponse) { - LOGGER.info("Successfully created realm " + realmName); - // If the POST to /admin/realms succeeds we just get back a HTTP 201 with no body - // Now that we have a new Keycloak realm for this SaaS Boost environment, we need - // to setup the proper admin use role mappings and we need to fetch the generated - // app clients (and their secrets) so we save that info as SaaS Boost settings - // and secrets. - - // Do this first because we need the realm clients to wire up the admin user - // permissions anyway - List> clients = getClients(keycloakHost, bearerToken, realmName); - for (Map appClient : clients) { - if (apiAppClientId.equals(appClient.get("clientId"))) { - // Get the Keycloak generated client secret - setupResults.put(RESPONSE_DATA_KEY_API_APP_CLIENT_NAME, (String) appClient.get("name")); - setupResults.put(RESPONSE_DATA_KEY_API_APP_CLIENT_ID, (String) appClient.get("clientId")); - setupResults.put(RESPONSE_DATA_KEY_API_APP_CLIENT_SECRET, (String) appClient.get("secret")); - } else if (adminWebAppClientId.equals(appClient.get("clientId"))) { - // Confirms that the public app client got created properly - setupResults.put(RESPONSE_DATA_KEY_WEB_APP_CLIENT_NAME, (String) appClient.get("name")); - setupResults.put(RESPONSE_DATA_KEY_WEB_APP_CLIENT_ID, (String) appClient.get("clientId")); - } - } - - final String adminGroupName = "admin"; - String adminGroupId = createAdminGroup(keycloakHost, bearerToken, realmName, adminGroupName); - - // Map realm management permissions to admin group - int mapAdminGroupServiceRolesResponse = mapAdminGroupServiceRoles(keycloakHost, bearerToken, realmName, - adminGroupId, clients); - if (HttpURLConnection.HTTP_NO_CONTENT == mapAdminGroupServiceRolesResponse) { - LOGGER.info("Successfully mapped admin service role to group"); - } else { - throw new RuntimeException("Keycloak service role mapping for admin group failed with HTTP " - + mapAdminGroupServiceRolesResponse); - } - - // Finally, attach the admin user to the admin group - String adminUserId = (String) getUser(keycloakHost, bearerToken, realmName, username).get("id"); - int attachUserToGroupResponse = attachUserToGroup(keycloakHost, bearerToken, - realmName, adminUserId, adminGroupId); - - // The POST to map client roles to user returns a 204 instead of a 201 created... - if (HttpURLConnection.HTTP_NO_CONTENT == attachUserToGroupResponse) { - LOGGER.info("Successfully attached user " + username + " to admin group"); - } else { - throw new RuntimeException("Keycloak admin user group attachment failed with HTTP " - + mapAdminGroupServiceRolesResponse); - } - } else { - throw new RuntimeException("Keycloak import realm failed with HTTP " + importRealmResponse); - } - - return setupResults; - } - - protected Map adminPasswordGrant(String keycloakHost, String username, String password) { - Map passwordGrant; - try { - URI endpoint = new URI(keycloakHost - + "/realms/master/protocol/openid-connect/token"); - String body = "grant_type=password" - + "&client_id=admin-cli" - + "&username=" + URLEncoder.encode(username, StandardCharsets.UTF_8) - + "&password=" + URLEncoder.encode(password, StandardCharsets.UTF_8); - HttpRequest request = HttpRequest.newBuilder() - .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding - .uri(endpoint) - .setHeader("Content-Type", "application/x-www-form-urlencoded") - .POST(HttpRequest.BodyPublishers.ofString(body)) - .build(); - - LOGGER.info("Invoking Keycloak password grant endpoint {}", request.uri()); - HttpResponse response = httpClient.send(request, - HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - if (HttpURLConnection.HTTP_OK == response.statusCode()) { - passwordGrant = Utils.fromJson(response.body(), LinkedHashMap.class); - } else { - LOGGER.error("Received HTTP status " + response.statusCode()); - LOGGER.error(response.body()); - throw new RuntimeException("Keycloak admin password grant failed HTTP " + response.statusCode()); - } - } catch (URISyntaxException | IOException | InterruptedException e) { - throw new RuntimeException(e); - } - return passwordGrant; - } - - protected int postRealm(String keycloakHost, String bearerToken, Map realm) { - try { - URI endpoint = new URI(keycloakHost + "/admin/realms"); - HttpRequest request = HttpRequest.newBuilder() - .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding - .uri(endpoint) - .setHeader("Authorization", "Bearer " + bearerToken) - .setHeader("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(Utils.toJson(realm))) - .build(); - - LOGGER.info("Invoking Keycloak realm import endpoint {}", request.uri()); - HttpResponse response = httpClient.send(request, - HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - return response.statusCode(); - } catch (URISyntaxException | IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - protected String createAdminGroup(String keycloakHost, String bearerToken, String realm, String groupName) { - try { - URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm + "/groups"); - HttpRequest request = HttpRequest.newBuilder() - .version(HttpClient.Version.HTTP_1_1) - .uri(endpoint) - .setHeader("Authorization", "Bearer " + bearerToken) - .setHeader("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(Utils.toJson(Map.of("name", groupName)))) - .build(); - LOGGER.info("Invoking Keycloak group create endpoint {}", request.uri()); - HttpResponse response = httpClient.send(request, - HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - if (response.statusCode() == HttpURLConnection.HTTP_CREATED) { - // let's return the group id we just created - Map group = findGroupByName(keycloakHost, bearerToken, realm, groupName); - String id = (String) group.get("id"); - if (id != null) { - return id; - } - throw new RuntimeException("Unexpected error: created group did not have id: " + Utils.toJson(group)); - } else { - LOGGER.error("Expected HTTP_CREATED ({}) from group create, but got {}", - HttpURLConnection.HTTP_CREATED, response.statusCode()); - throw new RuntimeException("Unexpected error while creating group: " + groupName); - } - } catch (URISyntaxException | IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - protected Map findGroupByName(String keycloakHost, String bearerToken, - String realm, String groupName) { - try { - URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm + "/groups"); - HttpRequest request = HttpRequest.newBuilder() - .version(HttpClient.Version.HTTP_1_1) - .uri(endpoint) - .setHeader("Authorization", "Bearer " + bearerToken) - .setHeader("Content-Type", "application/json") - .GET() - .build(); - LOGGER.info("Invoking Keycloak list groups endpoint {}", request.uri()); - HttpResponse response = httpClient.send(request, - HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - if (response.statusCode() == HttpURLConnection.HTTP_OK) { - LOGGER.info("Found groups: {}", response.body()); - List> groups = Utils.fromJson(response.body(), ArrayList.class); - for (Map group : groups) { - String name = (String) group.get("name"); - if (name != null && name.equals(groupName)) { - return group; - } - } - throw new RuntimeException("Could not find group with name " + groupName); - } else { - LOGGER.error("Expected HTTP_CREATED ({}) from group create, but got {}", - HttpURLConnection.HTTP_CREATED, response.statusCode()); - throw new RuntimeException("Unexpected error while creating group: " + groupName); - } - } catch (URISyntaxException | IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - protected int mapAdminGroupServiceRoles(String keycloakHost, String bearerToken, String realmName, String groupId, - List> clients) { - // We need the realm management client that's auto generated with every new realm in Keycloak - Map realmManagementClient = clients.stream() - .filter(m -> "realm-management".equals(m.get("clientId"))) - .findFirst() - .orElseThrow(() -> new RuntimeException("Can't find realm-management client in realm " + realmName)); - String clientId = (String) realmManagementClient.get("id"); - - // We need the realm admin role that's owned by the realm management client - List> roles = getClientRoles(keycloakHost, bearerToken, realmName, clientId); - Map realmAdminRole = roles.stream() - .filter(role -> "realm-admin".equals(role.get("name"))) - .findFirst() - .orElseThrow(() -> new RuntimeException("Can't find realm-admin role in client " + clientId)); - - // Finally we can map the manage realm role to our admin group so that users in that group - // will have permissions to do things like add and edit users - return postGroupRoleMapping(keycloakHost, bearerToken, realmName, groupId, clientId, realmAdminRole); - } - - protected List> getClients(String keycloakHost, String bearerToken, String realmName) { - try { - URI endpoint = new URI(keycloakHost + "/admin/realms/" - + realmName + "/clients"); - HttpRequest request = HttpRequest.newBuilder() - .version(HttpClient.Version.HTTP_1_1) - .uri(endpoint) - .setHeader("Authorization", "Bearer " + bearerToken) - .setHeader("Content-Type", "application/json") - .GET() - .build(); - LOGGER.info("Invoking Keycloak realm clients endpoint {}", request.uri()); - HttpResponse response = httpClient.send(request, - HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - if (HttpURLConnection.HTTP_OK == response.statusCode()) { - List> clients = Utils.fromJson(response.body(), ArrayList.class); - if (clients != null) { - return clients; - } else { - LOGGER.error("Can't parse realm clients response {}", response.body()); - throw new RuntimeException("Invalid response from " + request.uri()); - } - } else { - LOGGER.error("Received HTTP status " + response.statusCode()); - LOGGER.error(response.body()); - throw new RuntimeException("Keycloak realm clients failed with HTTP " - + response.statusCode()); - } - } catch (URISyntaxException | IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - protected List> getClientRoles(String keycloakHost, String bearerToken, String realmName, - String clientId) { - try { - URI endpoint = new URI(keycloakHost + "/admin" - + "/realms/" + realmName - + "/clients/" + clientId - + "/roles" - ); - HttpRequest request = HttpRequest.newBuilder() - .version(HttpClient.Version.HTTP_1_1) - .uri(endpoint) - .setHeader("Authorization", "Bearer " + bearerToken) - .setHeader("Content-Type", "application/json") - .GET() - .build(); - LOGGER.info("Invoking Keycloak realm client roles endpoint {}", request.uri()); - HttpResponse response = httpClient.send(request, - HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - if (HttpURLConnection.HTTP_OK == response.statusCode()) { - List> roles = Utils.fromJson(response.body(), ArrayList.class); - if (roles != null) { - return roles; - } else { - LOGGER.error("Can't parse realm client roles response {}", response.body()); - throw new RuntimeException("Invalid response from " + request.uri()); - } - } else { - LOGGER.error("Received HTTP status " + response.statusCode()); - LOGGER.error(response.body()); - throw new RuntimeException("Keycloak realm client roles failed with HTTP " - + response.statusCode()); - } - } catch (URISyntaxException | IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - protected Map getUser(String keycloakHost, String bearerToken, String realmName, String username) { - try { - URI endpoint = new URI(keycloakHost + "/admin/realms/" - + realmName + "/users?exact=true&username=" - + URLEncoder.encode(username, StandardCharsets.UTF_8)); - HttpRequest request = HttpRequest.newBuilder() - .version(HttpClient.Version.HTTP_1_1) - .uri(endpoint) - .setHeader("Authorization", "Bearer " + bearerToken) - .setHeader("Content-Type", "application/json") - .GET() - .build(); - LOGGER.info("Invoking Keycloak realm users endpoint {}", request.uri()); - HttpResponse response = httpClient.send(request, - HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - if (HttpURLConnection.HTTP_OK == response.statusCode()) { - List> users = Utils.fromJson(response.body(), ArrayList.class); - if (users != null) { - if (users.size() == 1) { - return users.get(0); - } else { - LOGGER.error("Can't find user {}", username); - LOGGER.error(response.body()); - throw new RuntimeException("Can't find user " + username); - } - } else { - LOGGER.error("Can't parse realm users response {}", response.body()); - throw new RuntimeException("Invalid response from " + request.uri()); - } - } else { - LOGGER.error("Received HTTP status " + response.statusCode()); - LOGGER.error(response.body()); - throw new RuntimeException("Keycloak realm users failed with HTTP " - + response.statusCode()); - } - } catch (URISyntaxException | IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - protected int attachUserToGroup(String keycloakHost, String bearerToken, String realmName, - String userId, String groupId) { - try { - URI endpoint = new URI(keycloakHost + "/admin" - + "/realms/" + realmName - + "/users/" + userId - + "/groups/" + groupId - ); - HttpRequest request = HttpRequest.newBuilder() - .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding - .uri(endpoint) - .setHeader("Authorization", "Bearer " + bearerToken) - .setHeader("Content-Type", "application/json") - .PUT(HttpRequest.BodyPublishers.noBody()) - .build(); - - LOGGER.info("Invoking Keycloak user group attach endpoint {}", request.uri()); - HttpResponse response = httpClient.send(request, - HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - return response.statusCode(); - } catch (URISyntaxException | IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - protected int postGroupRoleMapping(String keycloakHost, String bearerToken, String realmName, String groupId, - String clientId, Map role) { - try { - URI endpoint = new URI(keycloakHost + "/admin" - + "/realms/" + realmName - + "/groups/" + groupId - + "/role-mappings" - + "/clients/" + clientId - ); - String body = Utils.toJson(List.of(role)); - HttpRequest request = HttpRequest.newBuilder() - .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding - .uri(endpoint) - .setHeader("Authorization", "Bearer " + bearerToken) - .setHeader("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(body)) - .build(); - - LOGGER.info("Invoking Keycloak group role mapping endpoint {}", request.uri()); - LOGGER.info("POST body"); - LOGGER.info(body); - HttpResponse response = httpClient.send(request, - HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - return response.statusCode(); - } catch (URISyntaxException | IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - protected Map buildKeycloakUser(String username, String email, String password) { - LinkedHashMap user = new LinkedHashMap<>(); - user.put("enabled", true); - user.put("createdTimestamp", LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli()); - user.put("username", username); - user.put("email", email); - user.put("emailVerified", true); - user.put("credentials", List.of( - Map.of("type", "password", "temporary", true, "value", password) - )); - user.put("requiredActions", List.of("UPDATE_PASSWORD")); - return user; - } - - protected Map buildPublicAppClient(String clientName, String clientId, String redirects, - String description) { - LinkedHashMap publicAppClient = new LinkedHashMap<>(); - publicAppClient.put("enabled", true); - publicAppClient.put("protocol", "openid-connect"); - publicAppClient.put("name", clientName); - publicAppClient.put("clientId", clientId); - publicAppClient.put("description", description); - publicAppClient.put("redirectUris", List.of(redirects)); - publicAppClient.put("standardFlowEnabled", true); - publicAppClient.put("directAccessGrantsEnabled", false); - publicAppClient.put("implicitFlowEnabled", false); - publicAppClient.put("publicClient", true); - publicAppClient.put("attributes", Map.of("pkce.code.challenge.method", "S256")); - return publicAppClient; - } - - protected Map buildPrivateAppClient(String clientName, String clientId, String description) { - LinkedHashMap privateAppClient = new LinkedHashMap<>(); - privateAppClient.put("enabled", true); - privateAppClient.put("protocol", "openid-connect"); - privateAppClient.put("name", clientName); - privateAppClient.put("clientId", clientId); - privateAppClient.put("description", description); - privateAppClient.put("standardFlowEnabled", false); - privateAppClient.put("directAccessGrantsEnabled", false); - privateAppClient.put("implicitFlowEnabled", false); - privateAppClient.put("serviceAccountsEnabled", true); - privateAppClient.put("publicClient", false); - privateAppClient.put("attributes", Map.of("use.refresh.tokens", false)); - return privateAppClient; - } - - protected Map buildRealm(String realmName, List> clients, - List> users) { - LinkedHashMap realm = new LinkedHashMap<>(); - realm.put("realm", realmName); - realm.put("enabled", true); - realm.put("loginTheme", "saas-boost-theme"); - realm.put("displayNameHtml", "
    " + realmName + "
    "); - realm.put("users", users); - realm.put("clients", clients); - return realm; - } -} diff --git a/resources/custom-resources/keycloak-setup/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakSetup.java b/resources/custom-resources/keycloak-setup/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakSetup.java new file mode 100644 index 00000000..4b7586c1 --- /dev/null +++ b/resources/custom-resources/keycloak-setup/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakSetup.java @@ -0,0 +1,248 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost.keycloak; + +import com.amazon.aws.partners.saasfactory.saasboost.CloudFormationResponse; +import com.amazon.aws.partners.saasfactory.saasboost.Utils; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import org.keycloak.representations.idm.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.exception.SdkServiceException; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; + +import java.util.*; +import java.util.concurrent.*; + +public class KeycloakSetup implements RequestHandler, Object> { + + private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakSetup.class); + private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); + private static final String READ_SCOPE = "saas-boost/" + SAAS_BOOST_ENV + "/read"; + private static final String WRITE_SCOPE = "saas-boost/" + SAAS_BOOST_ENV + "/write"; + private static final String PRIVATE_SCOPE = "saas-boost/" + SAAS_BOOST_ENV + "/private"; + private static final String RESPONSE_DATA_KEY_KEYCLOAK_REALM = "KeycloakRealm"; + private static final String RESPONSE_DATA_KEY_WEB_APP_CLIENT_ID = "AdminWebAppClientId"; + private static final String RESPONSE_DATA_KEY_WEB_APP_CLIENT_NAME = "AdminWebAppClientName"; + private static final String RESPONSE_DATA_KEY_API_APP_CLIENT_ID = "ApiAppClientId"; + private static final String RESPONSE_DATA_KEY_API_APP_CLIENT_NAME = "ApiAppClientName"; + private static final String RESPONSE_DATA_KEY_API_APP_CLIENT_SECRET = "ApiAppClientSecret"; + private static final String RESPONSE_DATA_KEY_API_PRIVATE_APP_CLIENT_ID = "PrivateApiAppClientId"; + private static final String RESPONSE_DATA_KEY_API_PRIVATE_APP_CLIENT_NAME = "PrivateApiAppClientName"; + private static final String RESPONSE_DATA_KEY_API_PRIVATE_APP_CLIENT_SECRET = "PrivateApiAppClientSecret"; + private final SecretsManagerClient secrets; + + public KeycloakSetup() { + LOGGER.info("Version Info: {}", Utils.version(this.getClass())); + if (Utils.isBlank(SAAS_BOOST_ENV)) { + throw new IllegalStateException("Missing required environment variable SAAS_BOOST_ENV"); + } + secrets = Utils.sdkClient(SecretsManagerClient.builder(), SecretsManagerClient.SERVICE_NAME); + } + + @Override + public Object handleRequest(Map event, Context context) { + Utils.logRequestEvent(event); + + final String requestType = (String) event.get("RequestType"); + final Map resourceProperties = (Map) event.get("ResourceProperties"); + final String keycloakHost = (String) resourceProperties.get("KeycloakHost"); + final String keycloakSecretId = (String) resourceProperties.get("KeycloakCredentials"); + final String realm = (String) resourceProperties.get("Realm"); + final String adminUserSecretId = (String) resourceProperties.get("AdminUserCredentials"); + String adminWebAppUrl = (String) resourceProperties.get("AdminWebAppUrl"); + final String redirectUriPattern = (!adminWebAppUrl.endsWith("/*")) ? adminWebAppUrl + "/*" : adminWebAppUrl; + ExecutorService service = Executors.newSingleThreadExecutor(); + Map responseData = new HashMap<>(); + try { + Runnable r = () -> { + if ("Create".equalsIgnoreCase(requestType)) { + LOGGER.info("CREATE"); + try { + LOGGER.info("Fetching SaaS Boost admin user credentials from Secrets Manager"); + GetSecretValueResponse adminUserSecretValue = secrets.getSecretValue(request -> request + .secretId(adminUserSecretId) + ); + final Map adminUserCredentials = Utils.fromJson( + adminUserSecretValue.secretString(), LinkedHashMap.class); + + LOGGER.info("Fetching Keycloak super user credentials from Secrets Manager"); + GetSecretValueResponse keycloakSecretValue = secrets.getSecretValue(request -> request + .secretId(keycloakSecretId) + ); + final Map keycloakCredentials = Utils.fromJson( + keycloakSecretValue.secretString(), LinkedHashMap.class); + + KeycloakAdminApi keycloak = new KeycloakAdminApi(keycloakHost, + keycloakCredentials.get("username"), + keycloakCredentials.get("password")); + + Map keycloakRealmSetupResults = setupKeycloak(keycloak, realm, + adminUserCredentials.get("username"), adminUserCredentials.get("password"), + adminUserCredentials.get("email"), redirectUriPattern); + + if (keycloakRealmSetupResults != null && !keycloakRealmSetupResults.isEmpty()) { + responseData.putAll(keycloakRealmSetupResults); + // We're returning sensitive data, so be sure to use NoEcho = true + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html + CloudFormationResponse.send(event, context, "SUCCESS", responseData, true); + } else { + responseData.put("Reason", "Keycloak setup did not return app client details"); + CloudFormationResponse.send(event, context, "FAILED", responseData); + } + } catch (SdkServiceException secretsManagerError) { + LOGGER.error("Secrets Manager error {}", secretsManagerError.getMessage()); + LOGGER.error(Utils.getFullStackTrace(secretsManagerError)); + responseData.put("Reason", secretsManagerError.getMessage()); + CloudFormationResponse.send(event, context, "FAILED", responseData); + } catch (Exception e) { + LOGGER.error(Utils.getFullStackTrace(e)); + responseData.put("Reason", e.getMessage()); + CloudFormationResponse.send(event, context, "FAILED", responseData); + } + } else if ("Update".equalsIgnoreCase(requestType)) { + LOGGER.info("UPDATE"); + CloudFormationResponse.send(event, context, "SUCCESS", responseData); + } else if ("Delete".equalsIgnoreCase(requestType)) { + LOGGER.info("DELETE"); + CloudFormationResponse.send(event, context, "SUCCESS", responseData); + } else { + LOGGER.error("FAILED unknown requestType " + requestType); + responseData.put("Reason", "Unknown RequestType " + requestType); + CloudFormationResponse.send(event, context, "FAILED", responseData); + } + }; + Future f = service.submit(r); + f.get(context.getRemainingTimeInMillis() - 1000, TimeUnit.MILLISECONDS); + } catch (final TimeoutException | InterruptedException | ExecutionException e) { + // Timed out + LOGGER.error("FAILED unexpected error or request timed out " + e.getMessage()); + LOGGER.error(Utils.getFullStackTrace(e)); + responseData.put("Reason", e.getMessage()); + CloudFormationResponse.send(event, context, "FAILED", responseData); + } finally { + service.shutdown(); + } + return null; + } + + // Setting up a new Keycloak install programmatically is a multi step process. + // 1. Create the Keycloak realm for this SaaS Boost environment + // 2. Create the initial "admin" user + // 3. Create the realm roles + // 4. Create the user groups + // 5. Map the roles to groups + // 6. Add users to their groups + // 7. Create the client scopes + // 8. Create the app clients + // 9. Add protocol mappers to the clients to include roles and additional claims in the tokens + protected Map setupKeycloak(KeycloakAdminApi keycloak, String realmName, String initialUserUsername, + String initialUserPassword, String initialUserEmail, + String redirectUriPattern) { + // Keycloak realm for this SaaS Boost environment + RealmRepresentation realm = keycloak.createRealm(KeycloakUtils.asRealm(realmName)); + + // Initial "admin" user of the SaaS Boost admin web app + UserRepresentation adminUser = keycloak.createUser(realm, + KeycloakUtils.asUser(initialUserUsername, initialUserEmail, initialUserPassword)); + + // An "admin" role to enable RBAC attributes in the tokens + RoleRepresentation adminRole = keycloak.createRole(realm, KeycloakUtils.asRole("admin")); + + // An "admin" group for super users of the admin web app + GroupRepresentation adminGroup = keycloak.createGroup(realm, KeycloakUtils.asGroup("admin")); + + // Get the realm management client that's auto generated with every new realm in Keycloak + ClientRepresentation realmManagementClient = keycloak.getClient(realm, "realm-management"); + // Get the realm admin client role that's owned by the realm management client + RoleRepresentation realmAdminRole = keycloak.getClientRole(realm, realmManagementClient, "realm-admin"); + // Now map the realm admin client role to the admin group so that the access token generated + // during admin user sign in will have realm admin permissions in order to to create/update/delete + // other users in the realm. + keycloak.createGroupClientRoleMapping(realm, adminGroup, realmManagementClient, realmAdminRole); + + // Map the admin role to the admin group. If we don't do this, the groups claim is not added + // to the access token by the group membership protocol mapper. + keycloak.createGroupRoleMapping(realm, adminGroup, adminRole); + + // Add the initial admin user to the admin group + keycloak.addUserToGroup(realm, adminUser, adminGroup); + + ClientScopeRepresentation readScope = keycloak.createClientScope(realm, KeycloakUtils.asClientScope( + READ_SCOPE, "SaaS Boost Public API Read Access")); + ClientScopeRepresentation writeScope = keycloak.createClientScope(realm, KeycloakUtils.asClientScope( + WRITE_SCOPE, "SaaS Boost Public API Write Access")); + ClientScopeRepresentation privateScope = keycloak.createClientScope(realm, KeycloakUtils.asClientScope( + PRIVATE_SCOPE, "SaaS Boost Private API Read/Write Access")); + + // Public OAuth app client with PKCE for the admin web app + // This client will get the default scopes of "openid email profile" + String adminWebAppClientId = Utils.randomString(20); + String adminWebAppClientName = realmName + "-admin-webapp-client"; + String adminWebAppClientDescription = "SaaS Boost Admin Web App Client"; + ClientRepresentation adminWebAppClient = keycloak.createClient(realm, KeycloakUtils.asPublicClient( + adminWebAppClientName, adminWebAppClientId, adminWebAppClientDescription, redirectUriPattern)); + + // Private OAuth app client with secret for programmatic machine-to-machine API calls + String apiAppClientId = Utils.randomString(20); + String apiAppClientName = realmName + "-api-client"; + String apiAppClientDescription = "SaaS Boost API App Client"; + final ClientRepresentation apiAppClient = keycloak.createClient(realm, KeycloakUtils.asConfidentialClient( + apiAppClientName, apiAppClientId, apiAppClientDescription, + List.of(readScope.getName(), writeScope.getName()))); + + // Private OAuth app client with secret for service-to-service private API calls + String privateApiAppClientId = Utils.randomString(20); + String privateApiAppClientName = realmName + "-private-api-client"; + String privateApiAppClientDescription = "SaaS Boost Private API App Client"; + final ClientRepresentation privateApiAppClient = keycloak.createClient(realm, + KeycloakUtils.asConfidentialClient(privateApiAppClientName, privateApiAppClientId, + privateApiAppClientDescription, + List.of(readScope.getName(), writeScope.getName(), privateScope.getName()))); + + // Add the predefined group membership user realm role mapper to the dedicated scopes + // and mappers for the Admin Web App client so that the access token includes the user's + // group and role membership. This will add a "groups" claim to the id and access token + // vended by the admin web app client. + + // The Group Membership protocol mapper is under the built-in microprofile-jwt client scope + ClientScopeRepresentation microProfileScope = keycloak.getClientScope(realm, "microprofile-jwt"); + ProtocolMapperRepresentation groupsProtocolMapper = keycloak.getClientScopeProtocolMapper(realm, + microProfileScope, "groups"); + // We need to clear out the existing id that maps this model to the microprofile-jwt client + // scope or we'll get a unique key violation when we add a copy of it to the admin web app + // client's dedicated scopes + groupsProtocolMapper.setId(null); + adminWebAppClient = keycloak.addClientProtocolMapperModel(realm, adminWebAppClient, groupsProtocolMapper); + + // We need to return details of the app clients to CloudFormation + final Map setupResults = new HashMap<>(); + setupResults.put(RESPONSE_DATA_KEY_KEYCLOAK_REALM, realm.getRealm()); + setupResults.put(RESPONSE_DATA_KEY_WEB_APP_CLIENT_NAME, adminWebAppClient.getName()); + setupResults.put(RESPONSE_DATA_KEY_WEB_APP_CLIENT_ID, adminWebAppClient.getClientId()); + setupResults.put(RESPONSE_DATA_KEY_API_APP_CLIENT_NAME, apiAppClient.getName()); + setupResults.put(RESPONSE_DATA_KEY_API_APP_CLIENT_ID, apiAppClient.getClientId()); + setupResults.put(RESPONSE_DATA_KEY_API_APP_CLIENT_SECRET, apiAppClient.getSecret()); + setupResults.put(RESPONSE_DATA_KEY_API_PRIVATE_APP_CLIENT_NAME, privateApiAppClient.getName()); + setupResults.put(RESPONSE_DATA_KEY_API_PRIVATE_APP_CLIENT_ID, privateApiAppClient.getClientId()); + setupResults.put(RESPONSE_DATA_KEY_API_PRIVATE_APP_CLIENT_SECRET, privateApiAppClient.getSecret()); + + return setupResults; + } +} diff --git a/resources/custom-resources/pom.xml b/resources/custom-resources/pom.xml index 8ffdeaf3..df7412dd 100644 --- a/resources/custom-resources/pom.xml +++ b/resources/custom-resources/pom.xml @@ -14,21 +14,18 @@ pom https://github.com/awslabs/aws-saas-boost - app-services-macro - attach-ecs-capacity-provider - cidr-dynamodb clear-ecr-repo clear-s3-bucket cognito-app-client-details customize-cognito-ui keycloak-setup - set-instance-protection - fsx-dns-name - rds-bootstrap rds-options redshift-table start-codebuild + + ${project.basedir}/../.. + org.apache.logging.log4j @@ -47,12 +44,38 @@ aws-lambda-java-log4j2 - junit - junit + org.junit.jupiter + junit-jupiter-engine + + + org.junit.jupiter + junit-jupiter-api + + + com.amazonaws + aws-lambda-java-tests + + + org.mockito + mockito-core org.slf4j slf4j-nop + + com.amazon.aws.partners.saasfactory.saasboost + Utils + 1.0.0 + + provided + + + com.amazon.aws.partners.saasfactory.saasboost + CloudFormationUtils + 1.0.0 + + provided + diff --git a/resources/custom-resources/rds-bootstrap/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/RdsBootstrap.java b/resources/custom-resources/rds-bootstrap/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/RdsBootstrap.java deleted file mode 100644 index c9455e94..00000000 --- a/resources/custom-resources/rds-bootstrap/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/RdsBootstrap.java +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.core.ResponseInputStream; -import software.amazon.awssdk.core.exception.SdkServiceException; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.awssdk.services.ssm.SsmClient; - -import java.nio.charset.StandardCharsets; -import java.sql.*; -import java.util.*; -import java.util.concurrent.*; -import java.util.regex.Pattern; - -public class RdsBootstrap implements RequestHandler, Object> { - - private static final Logger LOGGER = LoggerFactory.getLogger(RdsBootstrap.class); - private static final String AWS_REGION = System.getenv("AWS_REGION"); - static final String SQL_STATEMENT_DELIMITER = ";\r?\n"; - static final int MAX_SQL_BATCH_SIZE = 25; - private final S3Client s3; - private final SsmClient ssm; - - public RdsBootstrap() { - if (Utils.isBlank(AWS_REGION)) { - throw new IllegalStateException("Missing required environment variable AWS_REGION"); - } - LOGGER.info("Version Info: {}", Utils.version(this.getClass())); - this.s3 = Utils.sdkClient(S3Client.builder(), S3Client.SERVICE_NAME); - this.ssm = Utils.sdkClient(SsmClient.builder(), SsmClient.SERVICE_NAME); - } - - @Override - public Object handleRequest(Map event, Context context) { - Utils.logRequestEvent(event); - - final String requestType = (String) event.get("RequestType"); - final Map resourceProperties = (Map) event.get("ResourceProperties"); - final String host = (String) resourceProperties.get("Host"); - final String port = (String) resourceProperties.get("Port"); - final String database = (String) resourceProperties.get("Database"); - final String username = (String) resourceProperties.get("User"); - final String passwordParam = (String) resourceProperties.get("Password"); - final String bootstrapFileBucket = (String) resourceProperties.get("BootstrapFileBucket"); - final String bootstrapFileKey = (String) resourceProperties.get("BootstrapFileKey"); - final String driverClassName = driverClassNameFromPort(port); - final String type = typeFromPort(port); - final boolean createAndBootstrap = Utils.isNotBlank(bootstrapFileBucket) && Utils.isNotBlank(bootstrapFileKey); - - ExecutorService service = Executors.newSingleThreadExecutor(); - Map responseData = new HashMap<>(); - try { - Runnable r = () -> { - if ("Create".equalsIgnoreCase(requestType)) { - LOGGER.info("CREATE"); - - LOGGER.info("Getting database password secret from Parameter Store"); - String password; - try { - password = ssm.getParameter(request -> request - .withDecryption(true) - .name(passwordParam) - ).parameter().value(); - } catch (SdkServiceException ssmError) { - LOGGER.error("ssm:GetParameter error", ssmError); - throw ssmError; - } - if (password == null) { - throw new RuntimeException("Password is null"); - } - - // We need a connection that doesn't specify the database name since we may be creating it right now - String dbCheck = null; - - // Unlike MySQL/MariaDB, you have to specify a database name to get a connection to postgres... - // And you should connect to dbo.master in SQL Server to check for a database - if (type.equals("postgresql")) { - dbCheck = "template1"; - } else if (type.equals("sqlserver")) { - dbCheck = "master"; - } - - // Create the database if it doesn't exist - this is helpful for SQL Server because - // RDS/CloudFormation won't create a database when you bring up an instance. - LOGGER.info("Checking if database {} exists", database); - try (Connection dbCheckConn = DriverManager.getConnection( - jdbcUrl(type, driverClassName, host, port, dbCheck), username, password)) { - String engine = dbCheckConn.getMetaData().getDatabaseProductName().toLowerCase(); - if (!databaseExists(dbCheckConn, engine, database)) { - createdb(dbCheckConn, engine, database); - } else { - LOGGER.info("Database {} exists", database); - } - } catch (SQLException e) { - LOGGER.error("Can't connect to database host", e); - throw new RuntimeException(e); - } - - if (createAndBootstrap) { - LOGGER.info("Getting SQL file from S3 s3://{}/{}", bootstrapFileBucket, bootstrapFileKey); - try { - ResponseInputStream bootstrapSql = s3.getObject(request -> request - .bucket(bootstrapFileBucket) - .key(bootstrapFileKey) - ); - // We have a database. Execute the SQL commands in the bootstrap file stored in S3. - LOGGER.info("Executing bootstrap SQL"); - try (Connection conn = DriverManager.getConnection( - jdbcUrl(type, driverClassName, host, port, database), username, password); - Statement sql = conn.createStatement()) { - conn.setAutoCommit(false); - int batch = 0; - Scanner sqlScanner = new Scanner(bootstrapSql, StandardCharsets.UTF_8); - sqlScanner.useDelimiter(Pattern.compile(SQL_STATEMENT_DELIMITER)); - while (sqlScanner.hasNext()) { - String ddl = sqlScanner.next().trim(); - if (!ddl.isEmpty()) { - //LOGGER.info(String.format("%02d %s", ++batch, ddl)); - sql.addBatch(ddl); - batch++; - if (batch % MAX_SQL_BATCH_SIZE == 0) { - sql.executeBatch(); - conn.commit(); - sql.clearBatch(); - } - } - } - sql.executeBatch(); - conn.commit(); - - LOGGER.info("Finished initializing database"); - CloudFormationResponse.send(event, context, "SUCCESS", responseData); - } catch (SQLException e) { - LOGGER.error("Error executing bootstrap SQL", e); - throw new RuntimeException(e); - } - } catch (SdkServiceException s3Error) { - LOGGER.error("s3:GetObject error", s3Error); - throw s3Error; - } - } else { - // We were just creating a database and there isn't a SQL file to execute - CloudFormationResponse.send(event, context, "SUCCESS", responseData); - } - } else if ("Update".equalsIgnoreCase(requestType)) { - LOGGER.info("UPDATE"); - CloudFormationResponse.send(event, context, "SUCCESS", responseData); - } else if ("Delete".equalsIgnoreCase(requestType)) { - LOGGER.info("DELETE"); - CloudFormationResponse.send(event, context, "SUCCESS", responseData); - } else { - LOGGER.error("FAILED unknown requestType " + requestType); - responseData.put("Reason", "Unknown RequestType " + requestType); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } - }; - Future f = service.submit(r); - f.get(context.getRemainingTimeInMillis() - 1000, TimeUnit.MILLISECONDS); - } catch (final TimeoutException | InterruptedException | ExecutionException e) { - // Timed out - LOGGER.error("FAILED unexpected error or request timed out " + e.getMessage()); - LOGGER.error(Utils.getFullStackTrace(e)); - responseData.put("Reason", e.getMessage()); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } finally { - service.shutdown(); - } - return null; - } - - static boolean databaseExists(Connection conn, String engine, String database) throws SQLException { - boolean databaseExists = false; - Statement sql = null; - ResultSet rs = null; - try { - if ("postgresql".equals(engine)) { - // Postgres doesn't support multiple databases (catalogs) per connection, so we can't use the JDBC - // metadata to get a list of all the databases on the host like you can with MySQL/MariaDB - sql = conn.createStatement(); - rs = sql.executeQuery("SELECT datname AS TABLE_CAT FROM pg_database WHERE datistemplate = false"); - } else { - DatabaseMetaData dbMetaData = conn.getMetaData(); - rs = dbMetaData.getCatalogs(); - } - if (rs != null) { - while (rs.next()) { - LOGGER.info("Database exists check: TABLE_CAT = {}", rs.getString("TABLE_CAT")); - if (rs.getString("TABLE_CAT").equals(database)) { - databaseExists = true; - break; - } - } - } else { - LOGGER.error("No database catalog result set!"); - } - } catch (SQLException e) { - LOGGER.error("Error checking if database exists", e); - LOGGER.error(Utils.getFullStackTrace(e)); - throw e; - } finally { - // Do our own resource cleanup instead of using try...with resources because in the PostgreSQL - // branch the Statement object will close out our ResultSet before we can loop over it. - closeQuietly(rs); - closeQuietly(sql); - } - return databaseExists; - } - - static void createdb(Connection conn, String engine, String database) throws SQLException { - LOGGER.info("Creating {} database {}", engine, database); - try (Statement create = conn.createStatement()) { - if (engine.contains("postgresql")) { - // Postgres has no real way of doing CREATE DATABASE IF NOT EXISTS... - create.executeUpdate("CREATE DATABASE " + database); - } else if (engine.contains("microsoft")) { - create.executeUpdate("IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = '" + database + "')\n" - + "BEGIN\n" - + "CREATE DATABASE " + database + "\n" - + "END" - ); - } else if (engine.contains("mysql") || engine.contains("mariadb")) { - create.executeUpdate("CREATE DATABASE IF NOT EXISTS " + database); - } - } catch (SQLException e) { - LOGGER.error("Error creating database", e); - LOGGER.error(Utils.getFullStackTrace(e)); - throw e; - } - } - - static String jdbcUrl(String type, String driverClassName, String host, String port, String database) { - StringBuilder url = new StringBuilder("jdbc:"); - url.append(type); - if (!"oracle.jdbc.driver.OracleDriver".equals(driverClassName)) { - url.append("://"); - } else { - url.append(":@"); - } - url.append(host); - url.append(":"); - url.append(port); - if (Utils.isNotBlank(database)) { - if (!"com.microsoft.sqlserver.jdbc.SQLServerDriver".equals(driverClassName)) { - url.append("/"); - } else { - url.append(";databaseName="); - } - url.append(database); - } - LOGGER.info("JDBC URL {}", url.toString()); - return url.toString(); - } - - static String typeFromPort(String port) { - String type; - switch (port) { - case "5432": - type = "postgresql"; - break; - case "3306": - type = "mariadb"; - break; - case "1433": - type = "sqlserver"; - break; - case "1521": - type = "oracle:thin"; // Probably realistic to not support the OCI driver... - break; - default: - type = null; - } - return type; - } - - static String driverClassNameFromPort(String port) { - String driverClassName; - switch (port) { - case "5432": - driverClassName = "org.postgresql.Driver"; - break; - case "3306": - driverClassName = "org.mariadb.jdbc.Driver"; // Can use this for both MariaDB and MySQL - break; - case "1433": - driverClassName = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; - break; - case "1521": - driverClassName = "oracle.jdbc.driver.OracleDriver"; - break; - default: - driverClassName = null; - } - return driverClassName; - } - - static void closeQuietly(AutoCloseable resource) { - if (resource != null) { - try { - resource.close(); - } catch (Exception e) { - LOGGER.warn("Resource close failed", e); - } - } - } -} diff --git a/resources/custom-resources/rds-bootstrap/src/main/resources/lambda-assembly.xml b/resources/custom-resources/rds-bootstrap/src/main/resources/lambda-assembly.xml deleted file mode 100644 index 26364854..00000000 --- a/resources/custom-resources/rds-bootstrap/src/main/resources/lambda-assembly.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - lambda - - zip - - false - - - - ${project.build.outputDirectory} - - com/amazon/aws/partners/saasfactory/** - log4j2.xml - git.properties - - - - - - false - true - lib - - - \ No newline at end of file diff --git a/resources/custom-resources/rds-bootstrap/src/main/resources/log4j2.xml b/resources/custom-resources/rds-bootstrap/src/main/resources/log4j2.xml deleted file mode 100644 index 04128110..00000000 --- a/resources/custom-resources/rds-bootstrap/src/main/resources/log4j2.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} %X{AWSRequestId} %-5p %C{1} - %m%n - - - - - - - - - - - - diff --git a/resources/custom-resources/rds-bootstrap/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/RdsBootstrapTest.java b/resources/custom-resources/rds-bootstrap/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/RdsBootstrapTest.java deleted file mode 100644 index 5a76f31e..00000000 --- a/resources/custom-resources/rds-bootstrap/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/RdsBootstrapTest.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import org.junit.Test; - -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Scanner; -import java.util.regex.Pattern; - -import static org.junit.Assert.*; - -public class RdsBootstrapTest { - - @Test - public void testSqlScanner() { - InputStream bootstrapSQL = Thread.currentThread().getContextClassLoader().getResourceAsStream("bootstrap.sql"); - Scanner sqlScanner = new Scanner(bootstrapSQL, "UTF-8"); - sqlScanner.useDelimiter(Pattern.compile(RdsBootstrap.SQL_STATEMENT_DELIMITER)); - List sql = new ArrayList<>(); - while (sqlScanner.hasNext()) { - String ddl = sqlScanner.next().trim(); - if (!ddl.isEmpty()) { - sql.add(ddl); - } - } - assertEquals(6, sql.size()); - } - - @Test - public void testBatch() { - InputStream bootstrapSQL = Thread.currentThread().getContextClassLoader().getResourceAsStream("large.sql"); - Scanner sqlScanner = new Scanner(bootstrapSQL, "UTF-8"); - sqlScanner.useDelimiter(Pattern.compile(RdsBootstrap.SQL_STATEMENT_DELIMITER)); - List sql = new ArrayList<>(); - int batch = 0; - int executedBatches = 0; - while (sqlScanner.hasNext()) { - String ddl = sqlScanner.next().trim(); - if (!ddl.isEmpty()) { - sql.add(ddl); - batch++; - if (batch % 25 == 0) { - executedBatches++; - //System.out.println("Executing batch of " + sql.size()); - assertEquals(25, sql.size()); - sql.clear(); - assertEquals(0, sql.size()); - } - } - } - executedBatches++; - assertEquals(2, sql.size()); - //System.out.println("Executing batch of " + sql.size()); - assertEquals(3, executedBatches); - } -} \ No newline at end of file diff --git a/resources/custom-resources/rds-bootstrap/src/test/resources/bootstrap.sql b/resources/custom-resources/rds-bootstrap/src/test/resources/bootstrap.sql deleted file mode 100644 index 2a427469..00000000 --- a/resources/custom-resources/rds-bootstrap/src/test/resources/bootstrap.sql +++ /dev/null @@ -1,70 +0,0 @@ --- Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. --- --- Licensed under the Apache License, Version 2.0 (the "License"). --- You may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE TABLE IF NOT EXISTS category ( - category_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - category VARCHAR(255) NOT NULL UNIQUE CHECK (category <> '') -) -ENGINE 'InnoDB'; - -CREATE TABLE IF NOT EXISTS product ( - product_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - sku VARCHAR(32) NOT NULL UNIQUE CHECK (sku <> ''), - product VARCHAR(255) NOT NULL UNIQUE CHECK (product <> ''), - price DECIMAL(9,2) NOT NULL, - image VARCHAR(255) -) -ENGINE 'InnoDB'; - -CREATE TABLE IF NOT EXISTS product_categories ( - product_id INT NOT NULL REFERENCES product (product_id) ON DELETE CASCADE ON UPDATE CASCADE, - category_id INT NOT NULL REFERENCES category (category_id) ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT product_categories_pk PRIMARY KEY (product_id, category_id) -) -ENGINE 'InnoDB'; - -CREATE TABLE IF NOT EXISTS purchaser ( - purchaser_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - first_name VARCHAR(64), - last_name VARCHAR(64), - UNIQUE(first_name, last_name) -) -ENGINE 'InnoDB'; - -CREATE TABLE IF NOT EXISTS order_fulfillment ( - order_fulfillment_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - order_date DATE NOT NULL, - ship_date DATE, - purchaser_id INTEGER NOT NULL REFERENCES purchaser (purchaser_id) ON DELETE RESTRICT ON UPDATE CASCADE, - ship_to_line1 VARCHAR(128), - ship_to_line2 VARCHAR(128), - ship_to_city VARCHAR(128), - ship_to_state VARCHAR(128), - ship_to_postal_code VARCHAR(128), - bill_to_line1 VARCHAR(128), - bill_to_line2 VARCHAR(128), - bill_to_city VARCHAR(128), - bill_to_state VARCHAR(128), - bill_to_postal_code VARCHAR(128) -) -ENGINE 'InnoDB'; - -CREATE TABLE IF NOT EXISTS order_line_item ( - order_line_item_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - order_fulfillment_id INT NOT NULL REFERENCES order_fulfillment (order_fulfillment_id) ON DELETE RESTRICT ON UPDATE CASCADE, - product_id INT NOT NULL REFERENCES product (product_id) ON DELETE RESTRICT ON UPDATE CASCADE, - quantity INT NOT NULL CHECK (quantity > 0), - unit_purchase_price DECIMAL(9, 2) NOT NULL -) -ENGINE 'InnoDB'; diff --git a/resources/custom-resources/rds-bootstrap/src/test/resources/large.sql b/resources/custom-resources/rds-bootstrap/src/test/resources/large.sql deleted file mode 100644 index 6de5af7c..00000000 --- a/resources/custom-resources/rds-bootstrap/src/test/resources/large.sql +++ /dev/null @@ -1,52 +0,0 @@ -SELECT 1; -SELECT 2; -SELECT 3; -SELECT 4; -SELECT 5; -SELECT 6; -SELECT 7; -SELECT 8; -SELECT 9; -SELECT 10; -SELECT 11; -SELECT 12; -SELECT 13; -SELECT 14; -SELECT 15; -SELECT 16; -SELECT 17; -SELECT 18; -SELECT 19; -SELECT 20; -SELECT 21; -SELECT 22; -SELECT 23; -SELECT 24; -SELECT 25; -SELECT 26; -SELECT 27; -SELECT 28; -SELECT 29; -SELECT 30; -SELECT 31; -SELECT 32; -SELECT 33; -SELECT 34; -SELECT 35; -SELECT 36; -SELECT 37; -SELECT 38; -SELECT 39; -SELECT 40; -SELECT 41; -SELECT 42; -SELECT 43; -SELECT 44; -SELECT 45; -SELECT 46; -SELECT 47; -SELECT 48; -SELECT 49; -SELECT 50; -SELECT 51; -SELECT 52; \ No newline at end of file diff --git a/resources/custom-resources/rds-options/pom.xml b/resources/custom-resources/rds-options/pom.xml index 0eecec0b..95d79aa3 100644 --- a/resources/custom-resources/rds-options/pom.xml +++ b/resources/custom-resources/rds-options/pom.xml @@ -31,10 +31,12 @@ limitations under the License. http://www.apache.org/licenses/LICENSE-2.0 + + ${project.basedir}/../../.. 10 - + ${project.artifactId} diff --git a/resources/custom-resources/redshift-table/pom.xml b/resources/custom-resources/redshift-table/pom.xml index 7bb5fdef..f8aa93e2 100644 --- a/resources/custom-resources/redshift-table/pom.xml +++ b/resources/custom-resources/redshift-table/pom.xml @@ -31,10 +31,12 @@ limitations under the License. http://www.apache.org/licenses/LICENSE-2.0 + + ${project.basedir}/../../.. 35 - + ${project.artifactId} diff --git a/resources/custom-resources/set-instance-protection/pom.xml b/resources/custom-resources/set-instance-protection/pom.xml deleted file mode 100644 index 3df07213..00000000 --- a/resources/custom-resources/set-instance-protection/pom.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - - 4.0.0 - - com.amazon.aws.partners.saasfactory.saasboost - saasboost-custom-resources - 1.0.0 - - SetInstanceProtection - 1.0.0 - jar - - - Apache-2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - - 0 - - - - ${project.artifactId} - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - org.apache.maven.plugins - maven-assembly-plugin - - - io.github.git-commit-id - git-commit-id-maven-plugin - - - - - - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - - - com.amazon.aws.partners.saasfactory.saasboost - CloudFormationUtils - 1.0.0 - - provided - - - software.amazon.awssdk - autoscaling - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - - diff --git a/resources/custom-resources/set-instance-protection/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SetInstanceProtection.java b/resources/custom-resources/set-instance-protection/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SetInstanceProtection.java deleted file mode 100644 index 4594189b..00000000 --- a/resources/custom-resources/set-instance-protection/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SetInstanceProtection.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.services.autoscaling.AutoScalingClient; -import software.amazon.awssdk.services.autoscaling.model.*; - -import java.util.*; -import java.util.concurrent.*; - -public class SetInstanceProtection implements RequestHandler, Object> { - - private static final Logger LOGGER = LoggerFactory.getLogger(SetInstanceProtection.class); - private final AutoScalingClient autoScaling; - - public SetInstanceProtection() { - LOGGER.info("Version Info: {}", Utils.version(this.getClass())); - autoScaling = Utils.sdkClient(AutoScalingClient.builder(), AutoScalingClient.SERVICE_NAME); - } - - @Override - public Object handleRequest(Map event, Context context) { - Utils.logRequestEvent(event); - - final String requestType = (String) event.get("RequestType"); - Map resourceProperties = (Map) event.get("ResourceProperties"); - final String autoScalingGroup = (String) resourceProperties.get("AutoScalingGroup"); - final Boolean enableInstanceProtection = Boolean.valueOf((String) resourceProperties.get("Enable")); - ExecutorService service = Executors.newSingleThreadExecutor(); - Map responseData = new HashMap<>(); - LOGGER.info("Setting instance protection to {} for Autoscaling group {}", enableInstanceProtection, - autoScalingGroup); - try { - Runnable r = () -> { - if ("Delete".equalsIgnoreCase(requestType) || "Update".equalsIgnoreCase(requestType)) { - LOGGER.info(requestType.toUpperCase()); - try { - DescribeAutoScalingGroupsResponse response = autoScaling.describeAutoScalingGroups(request -> - request.autoScalingGroupNames(autoScalingGroup) - ); - if (response.hasAutoScalingGroups()) { - LOGGER.info("AutoScaling found {} groups for {}", response.autoScalingGroups().size(), - autoScalingGroup); - if (!response.autoScalingGroups().isEmpty()) { - AutoScalingGroup asgGroup = response.autoScalingGroups().get(0); - List instancesToUpdate = new ArrayList<>(); - asgGroup.instances().forEach(ec2 -> instancesToUpdate.add(ec2.instanceId())); - try { - autoScaling.setInstanceProtection(request -> request - .instanceIds(instancesToUpdate) - .protectedFromScaleIn(enableInstanceProtection) - .autoScalingGroupName(autoScalingGroup) - ); - LOGGER.info("{} instance protection on {} instances.", - ((enableInstanceProtection) ? "Enabled" : "Disabled"), - instancesToUpdate.size() - ); - CloudFormationResponse.send(event, context, "SUCCESS", responseData); - } catch (AutoScalingException e) { - LOGGER.error("autoscaling:SetInstanceProtection error", e); - LOGGER.error(Utils.getFullStackTrace(e)); - responseData.put("Reason", e.getMessage()); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } - } else { - LOGGER.info("No auto scaling groups matched."); - } - } - } catch (AutoScalingException e) { - LOGGER.error("autoscaling:describeAutoScalingGroups error", e); - LOGGER.error(Utils.getFullStackTrace(e)); - responseData.put("Reason", e.getMessage()); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } - } else if ("Create".equalsIgnoreCase(requestType)) { - LOGGER.info("CREATE"); - CloudFormationResponse.send(event, context, "SUCCESS", responseData); - } else { - LOGGER.error("FAILED unknown requestType {}", requestType); - responseData.put("Reason", "Unknown RequestType " + requestType); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } - }; - Future f = service.submit(r); - f.get(context.getRemainingTimeInMillis() - 1000, TimeUnit.MILLISECONDS); - } catch (final TimeoutException | InterruptedException | ExecutionException e) { - // Timed out - LOGGER.error("FAILED unexpected error or request timed out " + e.getMessage()); - String stackTrace = Utils.getFullStackTrace(e); - LOGGER.error(stackTrace); - responseData.put("Reason", stackTrace); - CloudFormationResponse.send(event, context, "FAILED", responseData); - } finally { - service.shutdown(); - } - return null; - } - -} \ No newline at end of file diff --git a/resources/custom-resources/set-instance-protection/src/main/resources/lambda-assembly.xml b/resources/custom-resources/set-instance-protection/src/main/resources/lambda-assembly.xml deleted file mode 100644 index 26364854..00000000 --- a/resources/custom-resources/set-instance-protection/src/main/resources/lambda-assembly.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - lambda - - zip - - false - - - - ${project.build.outputDirectory} - - com/amazon/aws/partners/saasfactory/** - log4j2.xml - git.properties - - - - - - false - true - lib - - - \ No newline at end of file diff --git a/resources/custom-resources/set-instance-protection/src/main/resources/log4j2.xml b/resources/custom-resources/set-instance-protection/src/main/resources/log4j2.xml deleted file mode 100644 index cebb57ce..00000000 --- a/resources/custom-resources/set-instance-protection/src/main/resources/log4j2.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} %X{AWSRequestId} %-5p %C{1} - %m%n - - - - - - - - - - - - \ No newline at end of file diff --git a/resources/custom-resources/set-instance-protection/update.sh b/resources/custom-resources/set-instance-protection/update.sh deleted file mode 100755 index 72416310..00000000 --- a/resources/custom-resources/set-instance-protection/update.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -if [ -z $1 ]; then - echo "Usage: $0 [Lambda Folder]" - exit 2 -fi - -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -echo "AWS Region = $MY_AWS_REGION" - -ENVIRONMENT=$1 -LAMBDA_STAGE_FOLDER=$2 -if [ -z $LAMBDA_STAGE_FOLDER ]; then - LAMBDA_STAGE_FOLDER="lambdas" -fi -LAMBDA_CODE=SetInstanceProtection-lambda.zip - -#set this for V2 AWS CLI to disable paging -export AWS_PAGER="" - -SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) -echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" -if [ -z $SAAS_BOOST_BUCKET ]; then - echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" - exit 1 -fi - -# Do a fresh build of the project -mvn -if [ $? -ne 0 ]; then - echo "Error building project" - exit 1 -fi - -# And copy it up to S3 -aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ - -printf "Updating function code for sb-${ENVIRONMENT}-set-instance-protection\n" -eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-set-instance-protection\`)] | [].FunctionName' --output text"\) - -for FUNCTION in ${FUNCTIONS[@]}; do - #echo $FUNCTION - aws lambda --region $MY_AWS_REGION update-function-code --function-name $FUNCTION --s3-bucket $SAAS_BOOST_BUCKET --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE -done \ No newline at end of file diff --git a/resources/custom-resources/start-codebuild/pom.xml b/resources/custom-resources/start-codebuild/pom.xml index dfe018b9..31a27430 100644 --- a/resources/custom-resources/start-codebuild/pom.xml +++ b/resources/custom-resources/start-codebuild/pom.xml @@ -31,10 +31,12 @@ limitations under the License. http://www.apache.org/licenses/LICENSE-2.0 + + ${project.basedir}/../../.. 0 - + ${project.artifactId} @@ -51,8 +53,8 @@ limitations under the License. maven-assembly-plugin - pl.project13.maven - git-commit-id-plugin + io.github.git-commit-id + git-commit-id-maven-plugin diff --git a/resources/custom-resources/start-codebuild/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CodeBuildWaiter.java b/resources/custom-resources/start-codebuild/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CodeBuildWaiter.java new file mode 100644 index 00000000..c3277bbc --- /dev/null +++ b/resources/custom-resources/start-codebuild/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CodeBuildWaiter.java @@ -0,0 +1,83 @@ +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.retry.backoff.BackoffStrategy; +import software.amazon.awssdk.core.retry.backoff.FixedDelayBackoffStrategy; +import software.amazon.awssdk.core.waiters.Waiter; +import software.amazon.awssdk.core.waiters.WaiterAcceptor; +import software.amazon.awssdk.core.waiters.WaiterOverrideConfiguration; +import software.amazon.awssdk.core.waiters.WaiterResponse; +import software.amazon.awssdk.services.codebuild.CodeBuildClient; +import software.amazon.awssdk.services.codebuild.model.BatchGetBuildsRequest; +import software.amazon.awssdk.services.codebuild.model.BatchGetBuildsResponse; +import software.amazon.awssdk.services.codebuild.model.Build; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class CodeBuildWaiter { + + private static final Logger LOGGER = LoggerFactory.getLogger(CodeBuildWaiter.class); + private final CodeBuildClient client; + private final Waiter buildCompleteWaiter; + + public CodeBuildWaiter(CodeBuildClient client) { + this.client = client; + this.buildCompleteWaiter = Waiter.builder(BatchGetBuildsResponse.class) + .overrideConfiguration(buildCompleteWaiterConfig(null)) + .acceptors(buildCompleteWaiterAcceptors()) + .build(); + } + + WaiterResponse waitUntilBuildComplete(BatchGetBuildsRequest batchGetBuildsRequest) { + return buildCompleteWaiter.run(() -> client.batchGetBuilds(batchGetBuildsRequest)); + } + + private static String errorCode(Throwable error) { + if (error instanceof AwsServiceException) { + return ((AwsServiceException) error).awsErrorDetails().errorCode(); + } + return null; + } + + private static WaiterOverrideConfiguration buildCompleteWaiterConfig(WaiterOverrideConfiguration overrideConfig) { + Optional optionalOverrideConfig = Optional.ofNullable(overrideConfig); + int maxAttempts = optionalOverrideConfig + .flatMap(WaiterOverrideConfiguration::maxAttempts) + .orElse(30); + BackoffStrategy backoffStrategy = optionalOverrideConfig + .flatMap(WaiterOverrideConfiguration::backoffStrategy) + .orElse(FixedDelayBackoffStrategy.create(Duration.ofSeconds(20))); + Duration waitTimeout = optionalOverrideConfig + .flatMap(WaiterOverrideConfiguration::waitTimeout) + .orElse(null); + return WaiterOverrideConfiguration.builder() + .maxAttempts(maxAttempts) + .backoffStrategy(backoffStrategy) + .waitTimeout(waitTimeout) + .build(); + } + + private static List> buildCompleteWaiterAcceptors() { + List> result = new ArrayList<>(); + result.add(WaiterAcceptor.successOnResponseAcceptor(response -> { + String currentPhase = null; + if (!response.builds().isEmpty()) { + currentPhase = response.builds().get(0).currentPhase(); + } + long completeBuilds = response.builds().stream().filter(Build::buildComplete).count(); + LOGGER.debug("Builds {} Builds Not Found {} Complete Builds {} Current Phase {}", + response.builds().size(), response.buildsNotFound().size(), completeBuilds, currentPhase); + return completeBuilds > 0; + })); + result.add(WaiterAcceptor.retryOnExceptionAcceptor(error -> + Objects.equals(errorCode(error), "ResourceNotFoundException"))); + result.add(WaiterAcceptor.retryOnResponseAcceptor(response -> true)); + return result; + } +} diff --git a/resources/custom-resources/start-codebuild/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/StartCodeBuild.java b/resources/custom-resources/start-codebuild/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/StartCodeBuild.java index 32114758..c90c6c48 100644 --- a/resources/custom-resources/start-codebuild/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/StartCodeBuild.java +++ b/resources/custom-resources/start-codebuild/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/StartCodeBuild.java @@ -20,11 +20,10 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.internal.waiters.ResponseOrException; +import software.amazon.awssdk.core.waiters.WaiterResponse; import software.amazon.awssdk.services.codebuild.CodeBuildClient; -import software.amazon.awssdk.services.codebuild.model.Build; -import software.amazon.awssdk.services.codebuild.model.CodeBuildException; -import software.amazon.awssdk.services.codebuild.model.StartBuildResponse; -import software.amazon.awssdk.services.codebuild.model.StatusType; +import software.amazon.awssdk.services.codebuild.model.*; import java.util.*; import java.util.concurrent.*; @@ -50,6 +49,8 @@ public Object handleRequest(Map event, Context context) { final String requestType = (String) event.get("RequestType"); final Map resourceProperties = (Map) event.get("ResourceProperties"); final String project = (String) resourceProperties.get("Project"); + final String buildSpec = (String) resourceProperties.get("BuildSpec"); + final Boolean wait = Boolean.valueOf((String) resourceProperties.get("Wait")); ExecutorService service = Executors.newSingleThreadExecutor(); Map responseData = new HashMap<>(); @@ -58,17 +59,49 @@ public Object handleRequest(Map event, Context context) { if ("Create".equalsIgnoreCase(requestType) || "Update".equalsIgnoreCase(requestType)) { LOGGER.info("CREATE or UPDATE"); try { - StartBuildResponse response = codeBuild.startBuild(request -> request - .projectName(project) - .build() - ); + StartBuildRequest.Builder requestBuilder = StartBuildRequest.builder(); + requestBuilder = requestBuilder.projectName(project); + if (Utils.isNotBlank(buildSpec)) { + requestBuilder = requestBuilder.buildspecOverride(buildSpec); + } + StartBuildResponse response = codeBuild.startBuild(requestBuilder.build()); Build build = response.build(); if (StatusType.FAILED == build.buildStatus() || StatusType.FAULT == build.buildStatus()) { responseData.put("Reason", "CodeBuild start build failed"); CloudFormationResponse.send(event, context, "FAILED", responseData); } else { - responseData.put("Build", build.id()); - CloudFormationResponse.send(event, context, "SUCCESS", responseData); + if (wait) { + // wait for the build to complete + // note that the CodeBuild project has a defined timeout + LOGGER.info("Waiting for max {} minutes for build {} to complete", + build.timeoutInMinutes(), build.id()); + CodeBuildWaiter waiter = new CodeBuildWaiter(codeBuild); + WaiterResponse waiterResponse = waiter.waitUntilBuildComplete( + BatchGetBuildsRequest.builder().ids(build.id()).build() + ); + ResponseOrException completedBuildResponse = waiterResponse + .matched(); + if (completedBuildResponse.response().isPresent()) { + build = completedBuildResponse.response().get().builds().get(0); + if (StatusType.SUCCEEDED == build.buildStatus()) { + responseData.put("Build", build.id()); + responseData.put("BuildStatus", build.buildStatusAsString()); + CloudFormationResponse.send(event, context, "SUCCESS", responseData); + } else { + responseData.put("Reason", build.buildStatusAsString()); + CloudFormationResponse.send(event, context, "FAILED", responseData); + } + } else if (completedBuildResponse.exception().isPresent()) { + Throwable error = completedBuildResponse.exception().get(); + LOGGER.error(Utils.getFullStackTrace(error)); + responseData.put("Reason", error.getMessage()); + CloudFormationResponse.send(event, context, "FAILED", responseData); + } + } else { + responseData.put("Build", build.id()); + responseData.put("BuildStatus", build.buildStatusAsString()); + CloudFormationResponse.send(event, context, "SUCCESS", responseData); + } } } catch (CodeBuildException codeBuildError) { LOGGER.error("codebuild:StartBuild", codeBuildError.getMessage()); @@ -103,4 +136,5 @@ public Object handleRequest(Map event, Context context) { } return null; } + } diff --git a/resources/custom-resources/start-codebuild/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/StartCodeBuildTest.java b/resources/custom-resources/start-codebuild/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/StartCodeBuildTest.java new file mode 100644 index 00000000..81d9e568 --- /dev/null +++ b/resources/custom-resources/start-codebuild/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/StartCodeBuildTest.java @@ -0,0 +1,25 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class StartCodeBuildTest { + +} \ No newline at end of file diff --git a/resources/saas-boost-public-api.yaml b/resources/saas-boost-api.yaml similarity index 63% rename from resources/saas-boost-public-api.yaml rename to resources/saas-boost-api.yaml index ef50123b..b1d76d9a 100644 --- a/resources/saas-boost-public-api.yaml +++ b/resources/saas-boost-api.yaml @@ -1,4 +1,4 @@ ---- +#--- # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. AWSTemplateFormatVersion: 2010-09-09 -Description: AWS SaaS Boost Public API +Description: AWS SaaS Boost API Parameters: Environment: Description: Environment name @@ -27,21 +27,33 @@ Parameters: SaaSBoostUtilsLayer: Description: Utils Layer ARN Type: String - PublicApi: + StartCodeBuildLambda: + Description: StartCodeBuild Lambda ARN + Type: String + CodePipelineBucket: + Description: S3 bucket for CodePipeline artifacts + Type: String + SSMParamSaaSBoostBucket: + Description: Parameter Store entry for SaaS Boost assets bucket + Type: String + SSMParamLambdaSourceFolder: + Description: Parameter Store entry for SaaS Boost assets bucket lambda source folder + Type: String + SaaSBoostApi: Description: API Gateway REST API Type: String RootResourceId: Description: API Gateway REST API root resource id Type: String - PublicApiStage: - Description: The API Gateway REST API stage name for the SaaS Boost public API + ApiStage: + Description: The API Gateway REST API stage name for the SaaS Boost API Type: String Default: v1 IdentityProvider: Description: Identity Provider for the SaaS Boost system users and Control Plane API authorization Type: String Default: COGNITO - AllowedValues: [COGNITO, KEYCLOAK] + AllowedValues: [COGNITO, KEYCLOAK, AUTH0] CognitoUserPoolId: Description: User Pool Id for the Cognito Authorizer Type: String @@ -51,23 +63,35 @@ Parameters: KeycloakRealm: Description: The non-master Keycloak realm to authenticate against Type: String - BillingServiceGetPlans: - Description: Billing Service subscription plans Lambda ARN + AdminWebAppClientId: + Description: OAuth App Client Id for the Admin Web App Type: String - MetricsServiceQuery: - Description: Metrics Service query Lambda ARN + ApiAppClientId: + Description: OAuth App Client Id for API client credentials grant Type: String - MetricsServiceDatasets: - Description: Metrics Service datasets Lambda ARN + PrivateApiAppClientId: + Description: OAuth App Client Id for private API client credentials grant Type: String - MetricsServiceAlbQuery: - Description: Metrics Service ALB metric query Lambda ARN +# BillingServiceGetPlans: +# Description: Billing Service subscription plans Lambda ARN +# Type: String +# MetricsServiceQuery: +# Description: Metrics Service query Lambda ARN +# Type: String +# MetricsServiceDatasets: +# Description: Metrics Service datasets Lambda ARN +# Type: String +# MetricsServiceAlbQuery: +# Description: Metrics Service ALB metric query Lambda ARN +# Type: String + MetricsServicePut: + Description: Metrics Service put metrics request Lambda ARN Type: String OnboardingServiceGetAll: Description: Onboarding Service get all onboarding requests Lambda ARN Type: String - OnboardingServiceStart: - Description: Onboarding Service start onboarding Lambda ARN + OnboardingServiceInsert: + Description: Onboarding Service insert onboarding request Lambda ARN Type: String OnboardingServiceById: Description: Onboarding Service get onboarding request by id Lambda ARN @@ -78,18 +102,30 @@ Parameters: SettingsServiceById: Description: Settings Service get setting Lambda ARN Type: String - SettingsServiceOptions: - Description: Settings Service get configuration options Lambda ARN + SettingsServiceGetSecret: + Description: Settings Service get decrypted secret setting Lambda ARN Type: String - SettingsServiceGetAppConfig: - Description: Settings Service get application configuration Lambda ARN + SettingsServiceUpdate: + Description: Settings Service update setting Lambda ARN Type: String - SettingsServiceUpdateAppConfig: - Description: Settings Service update application configuration Lambda ARN + AppConfigServiceOptions: + Description: AppConfig Service get configuration options Lambda ARN + Type: String + AppConfigServiceGet: + Description: AppConfig Service get application configuration Lambda ARN + Type: String + AppConfigServiceUpdate: + Description: AppConfig Service update application configuration Lambda ARN + Type: String + AppConfigServiceDelete: + Description: AppConfig Service delete application configuration Lambda ARN Type: String TenantServiceGetAll: Description: Tenant Service get all tenants Lambda ARN Type: String + TenantServiceInsert: + Description: Tenant Service insert new tenant Lambda ARN + Type: String TenantServiceById: Description: Tenant Service get tenant by id Lambda ARN Type: String @@ -114,8 +150,8 @@ Parameters: TierServiceUpdate: Description: Tier Service update tier Lambda ARN Type: String - TierServiceCreate: - Description: Tier Service create tier Lambda ARN + TierServiceInsert: + Description: Tier Service insert tier Lambda ARN Type: String TierServiceDelete: Description: Tier Service delete tier Lambda ARN @@ -141,6 +177,15 @@ Parameters: SystemUserServiceDisable: Description: System User Service disable user Lambda ARN Type: String + IdentityServiceGetProviders: + Description: Identity Service get all providers Lambda ARN + Type: String + IdentityServiceGetProvider: + Description: Identity Service get active identity provider Lambda ARN + Type: String + IdentityServiceSetProvider: + Description: Identity Service set active identity provider Lambda ARN + Type: String Resources: AuthorizerExecRole: Type: AWS::IAM::Role @@ -182,7 +227,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-authorizer Role: !GetAtt AuthorizerExecRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 30 MemorySize: 1024 Handler: com.amazon.aws.partners.saasfactory.saasboost.ApiGatewayAuthorizer @@ -194,22 +241,24 @@ Resources: Environment: Variables: SAAS_BOOST_ENV: !Ref Environment - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' IDENTITY_PROVIDER: !Ref IdentityProvider USER_POOL_ID: !Ref CognitoUserPoolId KEYCLOAK_HOST: !Ref KeycloakHost KEYCLOAK_REALM: !Ref KeycloakRealm + ADMIN_WEB_APP_CLIENT_ID: !Ref AdminWebAppClientId + API_APP_CLIENT_ID: !Ref ApiAppClientId + PRIVATE_API_APP_CLIENT_ID: !Ref PrivateApiAppClientId Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" + - Key: Application + Value: SaaSBoost + - Key: Environment Value: !Ref Environment - - Key: "BoostService" - Value: "Authorizer" + - Key: BoostService + Value: Authorizer ApiGatewayLoggingRole: Type: AWS::IAM::Role Properties: - RoleName: !Sub sb-${Environment}-pub-api-log-role-${AWS::Region} + RoleName: !Sub sb-${Environment}-apigw-log-role-${AWS::Region} Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 @@ -229,8 +278,232 @@ Resources: ApiGatewayAccessLogGroup: Type: AWS::Logs::LogGroup Properties: - LogGroupName: !Sub /aws/apigateway/${PublicApi} + LogGroupName: !Sub /aws/apigateway/${SaaSBoostApi} + RetentionInDays: 30 + ApiDocsCodeBuildRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub sb-${Environment}-api-docs-build-role-${AWS::Region} + Path: '/' + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - codebuild.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyName: !Sub sb-${Environment}-api-docs-build-policy + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - logs:PutLogEvents + Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* + - Effect: Allow + Action: + - logs:CreateLogStream + - logs:DescribeLogStreams + Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* + - Effect: Allow + Action: + - s3:listBucket + - s3:GetBucketVersioning + - s3:GetBucketLocation + Resource: + - !Sub arn:${AWS::Partition}:s3:::${SaaSBoostBucket} + - !Sub arn:${AWS::Partition}:s3:::${CodePipelineBucket} + - Effect: Allow + Action: + - s3:GetObject + - s3:GetObjectVersion + Resource: + - !Sub arn:${AWS::Partition}:s3:::${SaaSBoostBucket}/api-docs/* + - !Sub arn:${AWS::Partition}:s3:::${CodePipelineBucket}/codebuild-cache/* + - Effect: Allow + Action: + - s3:DeleteObject + - s3:PutObject + - s3:PutObjectAcl + - s3:GetObject + - s3:GetObjectVersion + Resource: + - !Sub arn:${AWS::Partition}:s3:::${CodePipelineBucket}/codebuild-cache/* + - !Sub arn:${AWS::Partition}:s3:::${SaaSBoostBucket}/${LambdaSourceFolder}/ApiDocs-lambda.zip + - Effect: Allow + Action: + - lambda:GetFunction + - lambda:UpdateFunctionCode + Resource: + - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:sb-${Environment}-api-docs + - Effect: Allow + Action: + - ssm:GetParameter + Resource: + - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${SSMParamSaaSBoostBucket} + - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${SSMParamLambdaSourceFolder} + ApiDocsCodeBuildLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/codebuild/sb-${Environment}-api-docs + RetentionInDays: 14 + ApiDocsCodeBuildProject: + Type: AWS::CodeBuild::Project + Properties: + Name: !Sub sb-${Environment}-api-docs + ServiceRole: !Ref ApiDocsCodeBuildRole + TimeoutInMinutes: 10 + Artifacts: + Type: NO_ARTIFACTS + Cache: + Type: S3 + Location: !Sub ${CodePipelineBucket}/codebuild-cache + Environment: + ComputeType: BUILD_GENERAL1_SMALL + Image: aws/codebuild/amazonlinux2-aarch64-standard:3.0 + Type: ARM_CONTAINER + EnvironmentVariables: + - Name: SAAS_BOOST_ENV + Value: !Ref Environment + - Name: SOURCE_BUCKET + Value: !Ref SaaSBoostBucket + Source: + Type: NO_SOURCE + BuildSpec: | + version: 0.2 + phases: + pre_build: + commands: + - if [ "$AWS_DEFAULT_REGION" = "cn-northwest-1" ] || [ "$AWS_DEFAULT_REGION" = "cn-north-1" ]; then npm config set registry https://registry.npm.taobao.org; fi + - aws s3 cp s3://$SOURCE_BUCKET/api-docs/src.zip src.zip + - unzip src.zip + - aws s3 cp s3://$SOURCE_BUCKET/api-docs/swagger.json ./resources/api-docs/swagger.json + build: + commands: + - cd ./resources/api-docs + - npm install + - cd ../../ + post_build: + commands: + - cd ./resources/api-docs + - bash update.sh $SAAS_BOOST_ENV + cache: + paths: + - $CODEBUILD_SRC_DIR/resources/api-docs/node_modules/**/* + InvokeStartCodeBuild: + Type: Custom::CustomResource + Properties: + ServiceToken: !Ref StartCodeBuildLambda + Project: !Ref ApiDocsCodeBuildProject + Wait: true + ApiDocsBuildEventRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub sb-${Environment}-api-docs-event-build-role-${AWS::Region} + Path: '/' + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - events.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyName: !Sub sb-${Environment}-api-docs-event-build-policy + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - codebuild:StartBuild + Resource: !GetAtt ApiDocsCodeBuildProject.Arn + ApiDocsBuildRule: + Type: AWS::Events::Rule + Properties: + Name: !Sub sb-${Environment}-api-docs-build + Description: SaaS Boost API Docs new source event + EventPattern: !Sub | + { + "source": [ + "aws.s3" + ], + "detail-type": [ + "Object Created", + "Object Deleted" + ], + "detail": { + "bucket": { + "name": [ + "${SaaSBoostBucket}" + ] + }, + "object": { + "key": [{ + "prefix": "api-docs/" + }] + } + } + } + State: ENABLED + Targets: + - Arn: !GetAtt ApiDocsCodeBuildProject.Arn + RoleArn: !GetAtt ApiDocsBuildEventRole.Arn + Id: !Sub sb-${Environment}-api-docs-build + ApiDocsExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub sb-${Environment}-api-docs-role-${AWS::Region} + Path: '/' + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyName: !Sub sb-${Environment}-api-docs-policy + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - logs:PutLogEvents + Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* + - Effect: Allow + Action: + - logs:CreateLogStream + - logs:DescribeLogStreams + Resource: + - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* + ApiDocsLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-api-docs RetentionInDays: 30 + ApiDocs: + Type: AWS::Lambda::Function + DependsOn: InvokeStartCodeBuild + Properties: + FunctionName: !Sub sb-${Environment}-api-docs + Role: !GetAtt ApiDocsExecutionRole.Arn + Runtime: nodejs16.x + Timeout: 30 + MemorySize: 512 + Handler: app.handler + Code: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/ApiDocs-lambda.zip + Environment: + Variables: + SWAGGER_UI_URL_PATH: /docs ApiGatewayLambdaAuthorizerRole: Type: AWS::IAM::Role Properties: @@ -258,152 +531,119 @@ Resources: ApiGatewayLambdaAuthorizer: Type: AWS::ApiGateway::Authorizer Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi Type: TOKEN - Name: !Sub sb-${Environment}-pub-api-authorizer + Name: !Sub sb-${Environment}-api-authorizer AuthorizerCredentials: !GetAtt ApiGatewayLambdaAuthorizerRole.Arn IdentitySource: method.request.header.Authorization IdentityValidationExpression: ^[Bb]earer [A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$ AuthorizerUri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AuthorizerLambda.Arn}/invocations - BillingServiceResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PublicApi - ParentId: !Ref RootResourceId - PathPart: 'billing' - BillingServicePlansResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PublicApi - ParentId: !Ref BillingServiceResource - PathPart: 'plans' - BillingServicePlansResourceCORS: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref BillingServicePlansResource - HttpMethod: OPTIONS - AuthorizationType: NONE - Integration: - Type: MOCK - PassthroughBehavior: WHEN_NO_MATCH - IntegrationResponses: - - StatusCode: '200' - ResponseTemplates: {application/json: ''} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'" - method.response.header.Access-Control-Allow-Origin: "'*'" - method.response.header.Access-Control-Max-Age: "'3600'" - method.response.header.X-Requested-With: "'*'" - RequestTemplates: - application/json: '{"statusCode": 200}' - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: false - method.response.header.Access-Control-Allow-Methods: false - method.response.header.Access-Control-Allow-Origin: false - method.response.header.Access-Control-Max-Age: false - method.response.header.X-Requested-With: false - BillingServiceGetPlansMethod: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref BillingServicePlansResource - HttpMethod: GET - AuthorizationType: CUSTOM - AuthorizerId: !Ref ApiGatewayLambdaAuthorizer - Integration: - Type: AWS_PROXY - IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${BillingServiceGetPlans}/invocations - PassthroughBehavior: WHEN_NO_MATCH - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Origin: false - BillingServiceGetPlansLambdaPermission: - Type: AWS::Lambda::Permission - Properties: - Principal: apigateway.amazonaws.com - Action: lambda:InvokeFunction - FunctionName: !Ref BillingServiceGetPlans - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/GET/billing/plans +# BillingServiceResource: +# Type: AWS::ApiGateway::Resource +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ParentId: !Ref RootResourceId +# PathPart: 'billing' +# BillingServicePlansResource: +# Type: AWS::ApiGateway::Resource +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ParentId: !Ref BillingServiceResource +# PathPart: 'plans' +# BillingServicePlansResourceCORS: +# Type: AWS::ApiGateway::Method +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ResourceId: !Ref BillingServicePlansResource +# HttpMethod: OPTIONS +# AuthorizationType: NONE +# Integration: +# Type: MOCK +# PassthroughBehavior: WHEN_NO_MATCH +# IntegrationResponses: +# - StatusCode: '200' +# ResponseTemplates: {application/json: ''} +# ResponseParameters: +# method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" +# method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'" +# method.response.header.Access-Control-Allow-Origin: "'*'" +# method.response.header.Access-Control-Max-Age: "'3600'" +# method.response.header.X-Requested-With: "'*'" +# RequestTemplates: +# application/json: '{"statusCode": 200}' +# MethodResponses: +# - StatusCode: '200' +# ResponseModels: {application/json: Empty} +# ResponseParameters: +# method.response.header.Access-Control-Allow-Headers: false +# method.response.header.Access-Control-Allow-Methods: false +# method.response.header.Access-Control-Allow-Origin: false +# method.response.header.Access-Control-Max-Age: false +# method.response.header.X-Requested-With: false +# BillingServiceGetPlansMethod: +# Type: AWS::ApiGateway::Method +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ResourceId: !Ref BillingServicePlansResource +# HttpMethod: GET +# AuthorizationType: CUSTOM +# AuthorizerId: !Ref ApiGatewayLambdaAuthorizer +# Integration: +# Type: AWS_PROXY +# IntegrationHttpMethod: POST +# Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${BillingServiceGetPlans}/invocations +# PassthroughBehavior: WHEN_NO_MATCH +# MethodResponses: +# - StatusCode: '200' +# ResponseModels: {application/json: Empty} +# ResponseParameters: +# method.response.header.Access-Control-Allow-Origin: false +# BillingServiceGetPlansLambdaPermission: +# Type: AWS::Lambda::Permission +# Properties: +# Principal: apigateway.amazonaws.com +# Action: lambda:InvokeFunction +# FunctionName: !Ref BillingServiceGetPlans +# SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/billing/plans MetricsServiceResource: Type: AWS::ApiGateway::Resource Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ParentId: !Ref RootResourceId - PathPart: 'metrics' - MetricsServiceQueryResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PublicApi - ParentId: !Ref MetricsServiceResource - PathPart: 'query' - MetricsServiceDatasetsResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PublicApi - ParentId: !Ref MetricsServiceResource - PathPart: 'datasets' - MetricsServiceAlbResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PublicApi - ParentId: !Ref MetricsServiceResource - PathPart: 'alb' - MetricsServiceAlbMetricResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PublicApi - ParentId: !Ref MetricsServiceAlbResource - PathPart: '{metric}' - MetricsServiceAlbQueryResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PublicApi - ParentId: !Ref MetricsServiceAlbMetricResource - PathPart: '{timerange}' - MetricsServiceAlbQueryByTenantIdResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PublicApi - ParentId: !Ref MetricsServiceAlbQueryResource - PathPart: '{id}' - MetricsServiceQueryMethod: + PathPart: metrics + MetricsServicePutMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref MetricsServiceQueryResource + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref MetricsServiceResource HttpMethod: POST AuthorizationType: CUSTOM AuthorizerId: !Ref ApiGatewayLambdaAuthorizer Integration: Type: AWS_PROXY IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MetricsServiceQuery}/invocations + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MetricsServicePut}/invocations PassthroughBehavior: WHEN_NO_MATCH MethodResponses: - StatusCode: '200' - ResponseModels: {application/json: Empty} + ResponseModels: + application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: false - MetricsServiceQueryLambdaPermission: +# RequestModels: +# application/json: !Ref MetricListModel + MetricsServicePutLambdaPermission: Type: AWS::Lambda::Permission Properties: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction - FunctionName: !Ref MetricsServiceQuery - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/POST/metrics/query - MetricsServiceQueryResourceCORS: + FunctionName: !Ref MetricsServicePut + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/POST/metrics + MetricsServiceResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref MetricsServiceQueryResource + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref MetricsServiceResource HttpMethod: OPTIONS AuthorizationType: NONE Integration: @@ -411,10 +651,11 @@ Resources: PassthroughBehavior: WHEN_NO_MATCH IntegrationResponses: - StatusCode: '200' - ResponseTemplates: {application/json: ''} + ResponseTemplates: + application/json: '' ResponseParameters: method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'" + method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS,POST'" method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Max-Age: "'3600'" method.response.header.X-Requested-With: "'*'" @@ -422,104 +663,351 @@ Resources: application/json: '{"statusCode": 200}' MethodResponses: - StatusCode: '200' - ResponseModels: {application/json: Empty} + ResponseModels: + application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Headers: false method.response.header.Access-Control-Allow-Methods: false method.response.header.Access-Control-Allow-Origin: false method.response.header.Access-Control-Max-Age: false method.response.header.X-Requested-With: false - MetricsServiceDatasetsMethod: +# MetricsServiceQueryResource: +# Type: AWS::ApiGateway::Resource +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ParentId: !Ref MetricsServiceResource +# PathPart: query +# MetricsServiceDatasetsResource: +# Type: AWS::ApiGateway::Resource +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ParentId: !Ref MetricsServiceResource +# PathPart: datasets +# MetricsServiceAlbResource: +# Type: AWS::ApiGateway::Resource +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ParentId: !Ref MetricsServiceResource +# PathPart: alb +# MetricsServiceAlbMetricResource: +# Type: AWS::ApiGateway::Resource +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ParentId: !Ref MetricsServiceAlbResource +# PathPart: '{metric}' +# MetricsServiceAlbQueryResource: +# Type: AWS::ApiGateway::Resource +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ParentId: !Ref MetricsServiceAlbMetricResource +# PathPart: '{timerange}' +# MetricsServiceAlbQueryByTenantIdResource: +# Type: AWS::ApiGateway::Resource +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ParentId: !Ref MetricsServiceAlbQueryResource +# PathPart: '{id}' +# MetricsServiceQueryMethod: +# Type: AWS::ApiGateway::Method +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ResourceId: !Ref MetricsServiceQueryResource +# HttpMethod: POST +# AuthorizationType: CUSTOM +# AuthorizerId: !Ref ApiGatewayLambdaAuthorizer +# Integration: +# Type: AWS_PROXY +# IntegrationHttpMethod: POST +# Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MetricsServiceQuery}/invocations +# PassthroughBehavior: WHEN_NO_MATCH +# MethodResponses: +# - StatusCode: '200' +# ResponseModels: {application/json: Empty} +# ResponseParameters: +# method.response.header.Access-Control-Allow-Origin: false +# MetricsServiceQueryLambdaPermission: +# Type: AWS::Lambda::Permission +# Properties: +# Principal: apigateway.amazonaws.com +# Action: lambda:InvokeFunction +# FunctionName: !Ref MetricsServiceQuery +# SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/POST/metrics/query +# MetricsServiceQueryResourceCORS: +# Type: AWS::ApiGateway::Method +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ResourceId: !Ref MetricsServiceQueryResource +# HttpMethod: OPTIONS +# AuthorizationType: NONE +# Integration: +# Type: MOCK +# PassthroughBehavior: WHEN_NO_MATCH +# IntegrationResponses: +# - StatusCode: '200' +# ResponseTemplates: {application/json: ''} +# ResponseParameters: +# method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" +# method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'" +# method.response.header.Access-Control-Allow-Origin: "'*'" +# method.response.header.Access-Control-Max-Age: "'3600'" +# method.response.header.X-Requested-With: "'*'" +# RequestTemplates: +# application/json: '{"statusCode": 200}' +# MethodResponses: +# - StatusCode: '200' +# ResponseModels: {application/json: Empty} +# ResponseParameters: +# method.response.header.Access-Control-Allow-Headers: false +# method.response.header.Access-Control-Allow-Methods: false +# method.response.header.Access-Control-Allow-Origin: false +# method.response.header.Access-Control-Max-Age: false +# method.response.header.X-Requested-With: false +# MetricsServiceDatasetsMethod: +# Type: AWS::ApiGateway::Method +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ResourceId: !Ref MetricsServiceDatasetsResource +# HttpMethod: GET +# AuthorizationType: CUSTOM +# AuthorizerId: !Ref ApiGatewayLambdaAuthorizer +# Integration: +# Type: AWS_PROXY +# IntegrationHttpMethod: POST +# Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MetricsServiceDatasets}/invocations +# PassthroughBehavior: WHEN_NO_MATCH +# MethodResponses: +# - StatusCode: '200' +# ResponseModels: {application/json: Empty} +# ResponseParameters: +# method.response.header.Access-Control-Allow-Origin: false +# MetricsServiceDatasetsLambdaPermission: +# Type: AWS::Lambda::Permission +# Properties: +# Principal: apigateway.amazonaws.com +# Action: lambda:InvokeFunction +# FunctionName: !Ref MetricsServiceDatasets +# SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/metrics/datasets +# MetricsServiceDatasetsResourceCORS: +# Type: AWS::ApiGateway::Method +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ResourceId: !Ref MetricsServiceDatasetsResource +# HttpMethod: OPTIONS +# AuthorizationType: NONE +# Integration: +# Type: MOCK +# PassthroughBehavior: WHEN_NO_MATCH +# IntegrationResponses: +# - StatusCode: '200' +# ResponseTemplates: {application/json: ''} +# ResponseParameters: +# method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" +# method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'" +# method.response.header.Access-Control-Allow-Origin: "'*'" +# method.response.header.Access-Control-Max-Age: "'3600'" +# method.response.header.X-Requested-With: "'*'" +# RequestTemplates: +# application/json: '{"statusCode": 200}' +# MethodResponses: +# - StatusCode: '200' +# ResponseModels: {application/json: Empty} +# ResponseParameters: +# method.response.header.Access-Control-Allow-Headers: false +# method.response.header.Access-Control-Allow-Methods: false +# method.response.header.Access-Control-Allow-Origin: false +# method.response.header.Access-Control-Max-Age: false +# method.response.header.X-Requested-With: false +# MetricsServiceAlbQueryMethod: +# Type: AWS::ApiGateway::Method +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ResourceId: !Ref MetricsServiceAlbQueryResource +# HttpMethod: GET +# AuthorizationType: CUSTOM +# AuthorizerId: !Ref ApiGatewayLambdaAuthorizer +# RequestParameters: +# method.request.path.metric: true +# method.request.path.timerange: true +# Integration: +# Type: AWS_PROXY +# IntegrationHttpMethod: POST +# Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MetricsServiceAlbQuery}/invocations +# PassthroughBehavior: WHEN_NO_MATCH +# RequestParameters: +# integration.request.path.metric: 'method.request.path.metric' +# integration.request.path.timerange: 'method.request.path.timerange' +# MethodResponses: +# - StatusCode: '200' +# ResponseModels: {application/json: Empty} +# ResponseParameters: +# method.response.header.Access-Control-Allow-Origin: false +# MetricsServiceAlbQueryLambdaPermission: +# Type: AWS::Lambda::Permission +# Properties: +# Principal: apigateway.amazonaws.com +# Action: lambda:InvokeFunction +# FunctionName: !Ref MetricsServiceAlbQuery +# SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/metrics/alb/{metric}/{timerange} +# MetricsServiceAlbQueryResourceCORS: +# Type: AWS::ApiGateway::Method +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ResourceId: !Ref MetricsServiceAlbQueryResource +# HttpMethod: OPTIONS +# AuthorizationType: NONE +# Integration: +# Type: MOCK +# PassthroughBehavior: WHEN_NO_MATCH +# IntegrationResponses: +# - StatusCode: '200' +# ResponseTemplates: {application/json: ''} +# ResponseParameters: +# method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" +# method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'" +# method.response.header.Access-Control-Allow-Origin: "'*'" +# method.response.header.Access-Control-Max-Age: "'3600'" +# method.response.header.X-Requested-With: "'*'" +# RequestTemplates: +# application/json: '{"statusCode": 200}' +# MethodResponses: +# - StatusCode: '200' +# ResponseModels: {application/json: Empty} +# ResponseParameters: +# method.response.header.Access-Control-Allow-Headers: false +# method.response.header.Access-Control-Allow-Methods: false +# method.response.header.Access-Control-Allow-Origin: false +# method.response.header.Access-Control-Max-Age: false +# method.response.header.X-Requested-With: false +# MetricsServiceAlbQueryByTenantIdMethod: +# Type: AWS::ApiGateway::Method +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ResourceId: !Ref MetricsServiceAlbQueryByTenantIdResource +# HttpMethod: GET +# AuthorizationType: CUSTOM +# AuthorizerId: !Ref ApiGatewayLambdaAuthorizer +# RequestParameters: +# method.request.path.metric: true +# method.request.path.timerange: true +# method.request.path.id: true +# Integration: +# Type: AWS_PROXY +# IntegrationHttpMethod: POST +# Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MetricsServiceAlbQuery}/invocations +# PassthroughBehavior: WHEN_NO_MATCH +# RequestParameters: +# integration.request.path.metric: 'method.request.path.metric' +# integration.request.path.timerange: 'method.request.path.timerange' +# integration.request.path.id: 'method.request.path.id' +# MethodResponses: +# - StatusCode: '200' +# ResponseModels: {application/json: Empty} +# ResponseParameters: +# method.response.header.Access-Control-Allow-Origin: false +# MetricsServiceAlbQueryByTenantIdLambdaPermission: +# Type: AWS::Lambda::Permission +# Properties: +# Principal: apigateway.amazonaws.com +# Action: lambda:InvokeFunction +# FunctionName: !Ref MetricsServiceAlbQuery +# SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/metrics/alb/{metric}/{timerange}/{id} +# MetricsServiceAlbQueryByTenantIdResourceCORS: +# Type: AWS::ApiGateway::Method +# Properties: +# RestApiId: !Ref SaaSBoostApi +# ResourceId: !Ref MetricsServiceAlbQueryByTenantIdResource +# HttpMethod: OPTIONS +# AuthorizationType: NONE +# Integration: +# Type: MOCK +# PassthroughBehavior: WHEN_NO_MATCH +# IntegrationResponses: +# - StatusCode: '200' +# ResponseTemplates: {application/json: ''} +# ResponseParameters: +# method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" +# method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'" +# method.response.header.Access-Control-Allow-Origin: "'*'" +# method.response.header.Access-Control-Max-Age: "'3600'" +# method.response.header.X-Requested-With: "'*'" +# RequestTemplates: +# application/json: '{"statusCode": 200}' +# MethodResponses: +# - StatusCode: '200' +# ResponseModels: {application/json: Empty} +# ResponseParameters: +# method.response.header.Access-Control-Allow-Headers: false +# method.response.header.Access-Control-Allow-Methods: false +# method.response.header.Access-Control-Allow-Origin: false +# method.response.header.Access-Control-Max-Age: false +# method.response.header.X-Requested-With: false + OnboardingServiceResource: + Type: AWS::ApiGateway::Resource + Properties: + RestApiId: !Ref SaaSBoostApi + ParentId: !Ref RootResourceId + PathPart: onboarding + OnboardingServiceByIdResource: + Type: AWS::ApiGateway::Resource + Properties: + RestApiId: !Ref SaaSBoostApi + ParentId: !Ref OnboardingServiceResource + PathPart: '{id}' + OnboardingServiceGetAllMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref MetricsServiceDatasetsResource + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref OnboardingServiceResource HttpMethod: GET AuthorizationType: CUSTOM AuthorizerId: !Ref ApiGatewayLambdaAuthorizer Integration: Type: AWS_PROXY IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MetricsServiceDatasets}/invocations + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnboardingServiceGetAll}/invocations PassthroughBehavior: WHEN_NO_MATCH MethodResponses: - StatusCode: '200' ResponseModels: {application/json: Empty} ResponseParameters: method.response.header.Access-Control-Allow-Origin: false - MetricsServiceDatasetsLambdaPermission: + OnboardingServiceGetAllLambdaPermission: Type: AWS::Lambda::Permission Properties: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction - FunctionName: !Ref MetricsServiceDatasets - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/GET/metrics/datasets - MetricsServiceDatasetsResourceCORS: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref MetricsServiceDatasetsResource - HttpMethod: OPTIONS - AuthorizationType: NONE - Integration: - Type: MOCK - PassthroughBehavior: WHEN_NO_MATCH - IntegrationResponses: - - StatusCode: '200' - ResponseTemplates: {application/json: ''} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'" - method.response.header.Access-Control-Allow-Origin: "'*'" - method.response.header.Access-Control-Max-Age: "'3600'" - method.response.header.X-Requested-With: "'*'" - RequestTemplates: - application/json: '{"statusCode": 200}' - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: false - method.response.header.Access-Control-Allow-Methods: false - method.response.header.Access-Control-Allow-Origin: false - method.response.header.Access-Control-Max-Age: false - method.response.header.X-Requested-With: false - MetricsServiceAlbQueryMethod: + FunctionName: !Ref OnboardingServiceGetAll + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/onboarding + OnboardingServiceInsertMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref MetricsServiceAlbQueryResource - HttpMethod: GET + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref OnboardingServiceResource + HttpMethod: POST AuthorizationType: CUSTOM AuthorizerId: !Ref ApiGatewayLambdaAuthorizer - RequestParameters: - method.request.path.metric: true - method.request.path.timerange: true Integration: Type: AWS_PROXY IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MetricsServiceAlbQuery}/invocations + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnboardingServiceInsert}/invocations PassthroughBehavior: WHEN_NO_MATCH - RequestParameters: - integration.request.path.metric: 'method.request.path.metric' - integration.request.path.timerange: 'method.request.path.timerange' MethodResponses: - StatusCode: '200' ResponseModels: {application/json: Empty} ResponseParameters: method.response.header.Access-Control-Allow-Origin: false - MetricsServiceAlbQueryLambdaPermission: + OnboardingServiceInsertLambdaPermission: Type: AWS::Lambda::Permission Properties: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction - FunctionName: !Ref MetricsServiceAlbQuery - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/GET/metrics/alb/{metric}/{timerange} - MetricsServiceAlbQueryResourceCORS: + FunctionName: !Ref OnboardingServiceInsert + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/POST/onboarding + OnboardingServiceResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref MetricsServiceAlbQueryResource + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref OnboardingServiceResource HttpMethod: OPTIONS AuthorizationType: NONE Integration: @@ -530,7 +1018,7 @@ Resources: ResponseTemplates: {application/json: ''} ResponseParameters: method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'" + method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS,POST'" method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Max-Age: "'3600'" method.response.header.X-Requested-With: "'*'" @@ -545,44 +1033,38 @@ Resources: method.response.header.Access-Control-Allow-Origin: false method.response.header.Access-Control-Max-Age: false method.response.header.X-Requested-With: false - MetricsServiceAlbQueryByTenantIdMethod: + OnboardingServiceByIdMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref MetricsServiceAlbQueryByTenantIdResource + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref OnboardingServiceByIdResource HttpMethod: GET AuthorizationType: CUSTOM AuthorizerId: !Ref ApiGatewayLambdaAuthorizer - RequestParameters: - method.request.path.metric: true - method.request.path.timerange: true - method.request.path.id: true + RequestParameters: {method.request.path.id: true} Integration: Type: AWS_PROXY IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MetricsServiceAlbQuery}/invocations + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnboardingServiceById}/invocations PassthroughBehavior: WHEN_NO_MATCH - RequestParameters: - integration.request.path.metric: 'method.request.path.metric' - integration.request.path.timerange: 'method.request.path.timerange' - integration.request.path.id: 'method.request.path.id' + RequestParameters: {integration.request.path.id: 'method.request.path.id'} MethodResponses: - StatusCode: '200' ResponseModels: {application/json: Empty} ResponseParameters: method.response.header.Access-Control-Allow-Origin: false - MetricsServiceAlbQueryByTenantIdLambdaPermission: + OnboardingServiceByIdLambdaPermission: Type: AWS::Lambda::Permission Properties: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction - FunctionName: !Ref MetricsServiceAlbQuery - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/GET/metrics/alb/{metric}/{timerange}/{id} - MetricsServiceAlbQueryByTenantIdResourceCORS: + FunctionName: !Ref OnboardingServiceById + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/onboarding/{id} + OnboardingServiceByIdResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref MetricsServiceAlbQueryByTenantIdResource + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref OnboardingServiceByIdResource HttpMethod: OPTIONS AuthorizationType: NONE Integration: @@ -593,7 +1075,7 @@ Resources: ResponseTemplates: {application/json: ''} ResponseParameters: method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'" + method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS,PUT'" method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Max-Age: "'3600'" method.response.header.X-Requested-With: "'*'" @@ -608,73 +1090,54 @@ Resources: method.response.header.Access-Control-Allow-Origin: false method.response.header.Access-Control-Max-Age: false method.response.header.X-Requested-With: false - OnboardingServiceResource: + SettingsServiceResource: Type: AWS::ApiGateway::Resource Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ParentId: !Ref RootResourceId - PathPart: 'onboarding' - OnboardingServiceByIdResource: + PathPart: settings + SettingsServiceByIdResource: Type: AWS::ApiGateway::Resource Properties: - RestApiId: !Ref PublicApi - ParentId: !Ref OnboardingServiceResource + RestApiId: !Ref SaaSBoostApi + ParentId: !Ref SettingsServiceResource PathPart: '{id}' - OnboardingServiceGetAllMethod: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref OnboardingServiceResource - HttpMethod: GET - AuthorizationType: CUSTOM - AuthorizerId: !Ref ApiGatewayLambdaAuthorizer - Integration: - Type: AWS_PROXY - IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnboardingServiceGetAll}/invocations - PassthroughBehavior: WHEN_NO_MATCH - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Origin: false - OnboardingServiceGetAllLambdaPermission: - Type: AWS::Lambda::Permission + SettingsServiceSecretResource: + Type: AWS::ApiGateway::Resource Properties: - Principal: apigateway.amazonaws.com - Action: lambda:InvokeFunction - FunctionName: !Ref OnboardingServiceGetAll - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/GET/onboarding - OnboardingServiceStartMethod: + RestApiId: !Ref SaaSBoostApi + ParentId: !Ref SettingsServiceByIdResource + PathPart: secret + SettingsServiceGetAllMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref OnboardingServiceResource - HttpMethod: POST + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref SettingsServiceResource + HttpMethod: GET AuthorizationType: CUSTOM AuthorizerId: !Ref ApiGatewayLambdaAuthorizer Integration: Type: AWS_PROXY IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnboardingServiceStart}/invocations + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SettingsServiceGetAll}/invocations PassthroughBehavior: WHEN_NO_MATCH MethodResponses: - StatusCode: '200' ResponseModels: {application/json: Empty} ResponseParameters: method.response.header.Access-Control-Allow-Origin: false - OnboardingServiceStartLambdaPermission: + SettingsServiceGetAllLambdaPermission: Type: AWS::Lambda::Permission Properties: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction - FunctionName: !Ref OnboardingServiceStart - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/POST/onboarding - OnboardingServiceResourceCORS: + FunctionName: !Ref SettingsServiceGetAll + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/settings + SettingsServiceResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref OnboardingServiceResource + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref SettingsServiceResource HttpMethod: OPTIONS AuthorizationType: NONE Integration: @@ -685,7 +1148,7 @@ Resources: ResponseTemplates: {application/json: ''} ResponseParameters: method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS,POST'" + method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'" method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Max-Age: "'3600'" method.response.header.X-Requested-With: "'*'" @@ -700,11 +1163,38 @@ Resources: method.response.header.Access-Control-Allow-Origin: false method.response.header.Access-Control-Max-Age: false method.response.header.X-Requested-With: false - OnboardingServiceByIdMethod: + SettingsServiceUpdateMethod: + Type: AWS::ApiGateway::Method + Properties: + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref SettingsServiceByIdResource + HttpMethod: PUT + AuthorizationType: CUSTOM + AuthorizerId: !Ref ApiGatewayLambdaAuthorizer + RequestParameters: {method.request.path.id: true} + Integration: + Type: AWS_PROXY + IntegrationHttpMethod: POST + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SettingsServiceUpdate}/invocations + PassthroughBehavior: WHEN_NO_MATCH + RequestParameters: {integration.request.path.id: 'method.request.path.id'} + MethodResponses: + - StatusCode: '200' + ResponseModels: {application/json: Empty} + ResponseParameters: + method.response.header.Access-Control-Allow-Origin: false + SettingsServiceUpdateLambdaPermission: + Type: AWS::Lambda::Permission + Properties: + Principal: apigateway.amazonaws.com + Action: lambda:InvokeFunction + FunctionName: !Ref SettingsServiceUpdate + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/PUT/settings/{id} + SettingsServiceGetByIdMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref OnboardingServiceByIdResource + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref SettingsServiceByIdResource HttpMethod: GET AuthorizationType: CUSTOM AuthorizerId: !Ref ApiGatewayLambdaAuthorizer @@ -712,7 +1202,7 @@ Resources: Integration: Type: AWS_PROXY IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnboardingServiceById}/invocations + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SettingsServiceById}/invocations PassthroughBehavior: WHEN_NO_MATCH RequestParameters: {integration.request.path.id: 'method.request.path.id'} MethodResponses: @@ -720,18 +1210,18 @@ Resources: ResponseModels: {application/json: Empty} ResponseParameters: method.response.header.Access-Control-Allow-Origin: false - OnboardingServiceByIdLambdaPermission: + SettingsServiceByIdLambdaPermission: Type: AWS::Lambda::Permission Properties: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction - FunctionName: !Ref OnboardingServiceById - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/GET/onboarding/{id} - OnboardingServiceByIdResourceCORS: + FunctionName: !Ref SettingsServiceById + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/settings/{id} + SettingsServiceByIdResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref OnboardingServiceByIdResource + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref SettingsServiceByIdResource HttpMethod: OPTIONS AuthorizationType: NONE Integration: @@ -742,7 +1232,7 @@ Resources: ResponseTemplates: {application/json: ''} ResponseParameters: method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS,PUT'" + method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'" method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Max-Age: "'3600'" method.response.header.X-Requested-With: "'*'" @@ -757,60 +1247,38 @@ Resources: method.response.header.Access-Control-Allow-Origin: false method.response.header.Access-Control-Max-Age: false method.response.header.X-Requested-With: false - SettingsServiceResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PublicApi - ParentId: !Ref RootResourceId - PathPart: 'settings' - SettingsServiceConfigResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PublicApi - ParentId: !Ref SettingsServiceResource - PathPart: 'config' - SettingsServiceOptionsResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PublicApi - ParentId: !Ref SettingsServiceResource - PathPart: 'options' - SettingsServiceByIdResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PublicApi - ParentId: !Ref SettingsServiceResource - PathPart: '{id}' - SettingsServiceGetAllMethod: + SettingsServiceSecretMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref SettingsServiceResource + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref SettingsServiceSecretResource HttpMethod: GET AuthorizationType: CUSTOM AuthorizerId: !Ref ApiGatewayLambdaAuthorizer + RequestParameters: {method.request.path.id: true} Integration: Type: AWS_PROXY IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SettingsServiceGetAll}/invocations + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SettingsServiceGetSecret}/invocations PassthroughBehavior: WHEN_NO_MATCH + RequestParameters: {integration.request.path.id: 'method.request.path.id'} MethodResponses: - StatusCode: '200' ResponseModels: {application/json: Empty} ResponseParameters: method.response.header.Access-Control-Allow-Origin: false - SettingsServiceGetAllLambdaPermission: + SettingsServiceGetSecretLambdaPermission: Type: AWS::Lambda::Permission Properties: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction - FunctionName: !Ref SettingsServiceGetAll - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/GET/settings - SettingsServiceResourceCORS: + FunctionName: !Ref SettingsServiceGetSecret + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/settings/{id}/secret + SettingsServiceSecretResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref SettingsServiceResource + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref SettingsServiceSecretResource HttpMethod: OPTIONS AuthorizationType: NONE Integration: @@ -836,36 +1304,48 @@ Resources: method.response.header.Access-Control-Allow-Origin: false method.response.header.Access-Control-Max-Age: false method.response.header.X-Requested-With: false - SettingsServiceOptionsMethod: + AppConfigServiceResource: + Type: AWS::ApiGateway::Resource + Properties: + RestApiId: !Ref SaaSBoostApi + ParentId: !Ref RootResourceId + PathPart: appconfig + AppConfigServiceOptionsResource: + Type: AWS::ApiGateway::Resource + Properties: + RestApiId: !Ref SaaSBoostApi + ParentId: !Ref AppConfigServiceResource + PathPart: options + AppConfigServiceOptionsMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref SettingsServiceOptionsResource + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref AppConfigServiceOptionsResource HttpMethod: GET AuthorizationType: CUSTOM AuthorizerId: !Ref ApiGatewayLambdaAuthorizer Integration: Type: AWS_PROXY IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SettingsServiceOptions}/invocations + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AppConfigServiceOptions}/invocations PassthroughBehavior: WHEN_NO_MATCH MethodResponses: - StatusCode: '200' ResponseModels: {application/json: Empty} ResponseParameters: method.response.header.Access-Control-Allow-Origin: false - SettingsServiceOptionsPermission: + AppConfigServiceOptionsPermission: Type: AWS::Lambda::Permission Properties: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction - FunctionName: !Ref SettingsServiceOptions - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/GET/settings/options - SettingsServiceOptionsResourceCORS: + FunctionName: !Ref AppConfigServiceOptions + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/appconfig/options + AppConfigServiceOptionsResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref SettingsServiceOptionsResource + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref AppConfigServiceOptionsResource HttpMethod: OPTIONS AuthorizationType: NONE Integration: @@ -891,118 +1371,86 @@ Resources: method.response.header.Access-Control-Allow-Origin: false method.response.header.Access-Control-Max-Age: false method.response.header.X-Requested-With: false - SettingsServiceGetAppConfigMethod: + AppConfigServiceGetMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref SettingsServiceConfigResource + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref AppConfigServiceResource HttpMethod: GET AuthorizationType: CUSTOM AuthorizerId: !Ref ApiGatewayLambdaAuthorizer Integration: Type: AWS_PROXY IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SettingsServiceGetAppConfig}/invocations + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AppConfigServiceGet}/invocations PassthroughBehavior: WHEN_NO_MATCH MethodResponses: - StatusCode: '200' ResponseModels: {application/json: Empty} ResponseParameters: method.response.header.Access-Control-Allow-Origin: false - SettingsServiceGetAppConfigLambdaPermission: + AppConfigServiceGetLambdaPermission: Type: AWS::Lambda::Permission Properties: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction - FunctionName: !Ref SettingsServiceGetAppConfig - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/GET/settings/config - SettingsServiceUpdateAppConfigMethod: + FunctionName: !Ref AppConfigServiceGet + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/appconfig + AppConfigServiceUpdateMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref SettingsServiceConfigResource + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref AppConfigServiceResource HttpMethod: PUT AuthorizationType: CUSTOM AuthorizerId: !Ref ApiGatewayLambdaAuthorizer Integration: Type: AWS_PROXY IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SettingsServiceUpdateAppConfig}/invocations + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AppConfigServiceUpdate}/invocations PassthroughBehavior: WHEN_NO_MATCH MethodResponses: - StatusCode: '200' ResponseModels: {application/json: Empty} ResponseParameters: method.response.header.Access-Control-Allow-Origin: false - SettingsServiceUpdateAppConfigLambdaPermission: + AppConfigServiceUpdateLambdaPermission: Type: AWS::Lambda::Permission Properties: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction - FunctionName: !Ref SettingsServiceUpdateAppConfig - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/PUT/settings/config - SettingsServiceConfigResourceCORS: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref SettingsServiceConfigResource - HttpMethod: OPTIONS - AuthorizationType: NONE - Integration: - Type: MOCK - PassthroughBehavior: WHEN_NO_MATCH - IntegrationResponses: - - StatusCode: '200' - ResponseTemplates: {application/json: ''} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS,POST,PUT'" - method.response.header.Access-Control-Allow-Origin: "'*'" - method.response.header.Access-Control-Max-Age: "'3600'" - method.response.header.X-Requested-With: "'*'" - RequestTemplates: - application/json: '{"statusCode": 200}' - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: false - method.response.header.Access-Control-Allow-Methods: false - method.response.header.Access-Control-Allow-Origin: false - method.response.header.Access-Control-Max-Age: false - method.response.header.X-Requested-With: false - SettingsServiceGetByIdMethod: + FunctionName: !Ref AppConfigServiceUpdate + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/PUT/appconfig + AppConfigServiceDeleteMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref SettingsServiceByIdResource - HttpMethod: GET + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref AppConfigServiceResource + HttpMethod: DELETE AuthorizationType: CUSTOM AuthorizerId: !Ref ApiGatewayLambdaAuthorizer - RequestParameters: {method.request.path.id: true} Integration: Type: AWS_PROXY IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SettingsServiceById}/invocations + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AppConfigServiceDelete}/invocations PassthroughBehavior: WHEN_NO_MATCH - RequestParameters: {integration.request.path.id: 'method.request.path.id'} MethodResponses: - StatusCode: '200' ResponseModels: {application/json: Empty} ResponseParameters: method.response.header.Access-Control-Allow-Origin: false - SettingsServiceByIdLambdaPermission: + SettingsServiceDeleteAppConfigLambdaPermission: Type: AWS::Lambda::Permission Properties: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction - FunctionName: !Ref SettingsServiceById - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/GET/settings/{id} - SettingsServiceByIdResourceCORS: + FunctionName: !Ref AppConfigServiceDelete + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/DELETE/appconfig + AppConfigServiceResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi - ResourceId: !Ref SettingsServiceByIdResource + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref AppConfigServiceResource HttpMethod: OPTIONS AuthorizationType: NONE Integration: @@ -1013,7 +1461,7 @@ Resources: ResponseTemplates: {application/json: ''} ResponseParameters: method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'" + method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS,POST,PUT,DELETE'" method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Max-Age: "'3600'" method.response.header.X-Requested-With: "'*'" @@ -1031,31 +1479,31 @@ Resources: TenantServiceResource: Type: AWS::ApiGateway::Resource Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ParentId: !Ref RootResourceId PathPart: 'tenants' TenantServiceByIdResource: Type: AWS::ApiGateway::Resource Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ParentId: !Ref TenantServiceResource PathPart: '{id}' TenantServiceEnableResource: Type: AWS::ApiGateway::Resource Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ParentId: !Ref TenantServiceByIdResource PathPart: 'enable' TenantServiceDisableResource: Type: AWS::ApiGateway::Resource Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ParentId: !Ref TenantServiceByIdResource PathPart: 'disable' TenantServiceGetAllMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TenantServiceResource HttpMethod: GET AuthorizationType: CUSTOM @@ -1076,11 +1524,36 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref TenantServiceGetAll - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/GET/tenants + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/tenants + TenantServiceInsertMethod: + Type: AWS::ApiGateway::Method + Properties: + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref TenantServiceResource + HttpMethod: POST + AuthorizationType: CUSTOM + AuthorizerId: !Ref ApiGatewayLambdaAuthorizer + Integration: + Type: AWS_PROXY + IntegrationHttpMethod: POST + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${TenantServiceInsert}/invocations + PassthroughBehavior: WHEN_NO_MATCH + MethodResponses: + - StatusCode: '200' + ResponseModels: {application/json: Empty} + ResponseParameters: + method.response.header.Access-Control-Allow-Origin: false + TenantServiceInsertLambdaPermission: + Type: AWS::Lambda::Permission + Properties: + Principal: apigateway.amazonaws.com + Action: lambda:InvokeFunction + FunctionName: !Ref TenantServiceInsert + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/POST/tenants TenantServiceResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TenantServiceResource HttpMethod: OPTIONS AuthorizationType: NONE @@ -1110,7 +1583,7 @@ Resources: TenantServiceGetByIdMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TenantServiceByIdResource HttpMethod: GET AuthorizationType: CUSTOM @@ -1133,11 +1606,11 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref TenantServiceById - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/GET/tenants/{id} + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/tenants/{id} TenantServiceUpdateMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TenantServiceByIdResource HttpMethod: PUT AuthorizationType: CUSTOM @@ -1160,11 +1633,11 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref TenantServiceUpdate - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/PUT/tenants/{id} + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/PUT/tenants/{id} TenantServiceDeleteMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TenantServiceByIdResource HttpMethod: DELETE AuthorizationType: CUSTOM @@ -1187,11 +1660,11 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref TenantServiceDelete - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/DELETE/tenants/{id} + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/DELETE/tenants/{id} TenantServiceByIdResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TenantServiceByIdResource HttpMethod: OPTIONS AuthorizationType: NONE @@ -1221,7 +1694,7 @@ Resources: TenantServiceEnableMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TenantServiceEnableResource HttpMethod: PATCH AuthorizationType: CUSTOM @@ -1244,11 +1717,11 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref TenantServiceEnable - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/PATCH/tenants/{id}/enable + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/PATCH/tenants/{id}/enable TenantServiceEnableResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TenantServiceEnableResource HttpMethod: OPTIONS AuthorizationType: NONE @@ -1278,7 +1751,7 @@ Resources: TenantServiceDisableMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TenantServiceDisableResource HttpMethod: PATCH AuthorizationType: CUSTOM @@ -1301,11 +1774,11 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref TenantServiceDisable - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/PATCH/tenants/{id}/disable + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/PATCH/tenants/{id}/disable TenantServiceDisableResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TenantServiceDisableResource HttpMethod: OPTIONS AuthorizationType: NONE @@ -1335,13 +1808,13 @@ Resources: TierServiceResource: Type: AWS::ApiGateway::Resource Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ParentId: !Ref RootResourceId PathPart: 'tiers' TierServiceResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TierServiceResource HttpMethod: OPTIONS AuthorizationType: NONE @@ -1371,13 +1844,13 @@ Resources: TierServiceByIdResource: Type: AWS::ApiGateway::Resource Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ParentId: !Ref TierServiceResource PathPart: '{id}' TierServiceByIdResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TierServiceByIdResource HttpMethod: OPTIONS AuthorizationType: NONE @@ -1407,7 +1880,7 @@ Resources: TierServiceGetAllMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TierServiceResource HttpMethod: GET AuthorizationType: CUSTOM @@ -1430,11 +1903,11 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref TierServiceGetAll - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/GET/tiers - TierServiceCreateMethod: + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/tiers + TierServiceInsertMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TierServiceResource HttpMethod: POST AuthorizationType: CUSTOM @@ -1443,7 +1916,7 @@ Resources: Integration: Type: AWS_PROXY IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${TierServiceCreate}/invocations + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${TierServiceInsert}/invocations PassthroughBehavior: WHEN_NO_MATCH RequestParameters: {integration.request.path.id: 'method.request.path.id'} MethodResponses: @@ -1451,17 +1924,17 @@ Resources: ResponseModels: {application/json: Empty} ResponseParameters: method.response.header.Access-Control-Allow-Origin: false - TierServiceCreateLambdaPermission: + TierServiceInsertLambdaPermission: Type: AWS::Lambda::Permission Properties: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction - FunctionName: !Ref TierServiceCreate - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/POST/tiers + FunctionName: !Ref TierServiceInsert + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/POST/tiers TierServiceGetByIdMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TierServiceByIdResource HttpMethod: GET AuthorizationType: CUSTOM @@ -1484,11 +1957,11 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref TierServiceGetById - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/GET/tiers/{id} + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/tiers/{id} TierServiceUpdateMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TierServiceByIdResource HttpMethod: PUT AuthorizationType: CUSTOM @@ -1511,11 +1984,11 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref TierServiceUpdate - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/PUT/tiers/{id} + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/PUT/tiers/{id} TierServiceDeleteMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref TierServiceByIdResource HttpMethod: DELETE AuthorizationType: CUSTOM @@ -1538,35 +2011,35 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref TierServiceDelete - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/DELETE/tiers/{id} + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/DELETE/tiers/{id} SystemUserServiceResource: Type: AWS::ApiGateway::Resource Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ParentId: !Ref RootResourceId PathPart: 'sysusers' SystemUserServiceByIdResource: Type: AWS::ApiGateway::Resource Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ParentId: !Ref SystemUserServiceResource PathPart: '{id}' SystemUserServiceEnableResource: Type: AWS::ApiGateway::Resource Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ParentId: !Ref SystemUserServiceByIdResource PathPart: 'enable' SystemUserServiceDisableResource: Type: AWS::ApiGateway::Resource Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ParentId: !Ref SystemUserServiceByIdResource PathPart: 'disable' SystemUserServiceGetAllMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref SystemUserServiceResource HttpMethod: GET AuthorizationType: CUSTOM @@ -1587,11 +2060,11 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref SystemUserServiceGetAll - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/GET/sysusers + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/sysusers SystemUserServiceInsertMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref SystemUserServiceResource HttpMethod: POST AuthorizationType: CUSTOM @@ -1612,11 +2085,11 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref SystemUserServiceInsert - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/POST/sysusers + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/POST/sysusers SystemUserServiceResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref SystemUserServiceResource HttpMethod: OPTIONS AuthorizationType: NONE @@ -1646,7 +2119,7 @@ Resources: SystemUserServiceGetByIdMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref SystemUserServiceByIdResource HttpMethod: GET AuthorizationType: CUSTOM @@ -1669,11 +2142,11 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref SystemUserServiceById - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/GET/sysusers/{id} + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/sysusers/{id} SystemUserServiceUpdateMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref SystemUserServiceByIdResource HttpMethod: PUT AuthorizationType: CUSTOM @@ -1696,11 +2169,11 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref SystemUserServiceUpdate - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/PUT/sysusers/{id} + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/PUT/sysusers/{id} SystemUserServiceDeleteMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref SystemUserServiceByIdResource HttpMethod: DELETE AuthorizationType: CUSTOM @@ -1723,11 +2196,11 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref SystemUserServiceDelete - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/DELETE/sysusers/{id} + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/DELETE/sysusers/{id} SystemUserServiceByIdResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref SystemUserServiceByIdResource HttpMethod: OPTIONS AuthorizationType: NONE @@ -1757,7 +2230,7 @@ Resources: SystemUserServiceEnableMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref SystemUserServiceEnableResource HttpMethod: PATCH AuthorizationType: CUSTOM @@ -1780,11 +2253,11 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref SystemUserServiceEnable - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/PATCH/sysusers/{id}/enable + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/PATCH/sysusers/{id}/enable SystemUserServiceEnableResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref SystemUserServiceEnableResource HttpMethod: OPTIONS AuthorizationType: NONE @@ -1814,7 +2287,7 @@ Resources: SystemUserServiceDisableMethod: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref SystemUserServiceDisableResource HttpMethod: PATCH AuthorizationType: CUSTOM @@ -1837,11 +2310,11 @@ Resources: Principal: apigateway.amazonaws.com Action: lambda:InvokeFunction FunctionName: !Ref SystemUserServiceDisable - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PublicApi}/*/PATCH/sysusers/{id}/disable + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/PATCH/sysusers/{id}/disable SystemUserServiceDisableResourceCORS: Type: AWS::ApiGateway::Method Properties: - RestApiId: !Ref PublicApi + RestApiId: !Ref SaaSBoostApi ResourceId: !Ref SystemUserServiceDisableResource HttpMethod: OPTIONS AuthorizationType: NONE @@ -1868,36 +2341,258 @@ Resources: method.response.header.Access-Control-Allow-Origin: false method.response.header.Access-Control-Max-Age: false method.response.header.X-Requested-With: false + IdentityServiceResource: + Type: AWS::ApiGateway::Resource + Properties: + RestApiId: !Ref SaaSBoostApi + ParentId: !Ref RootResourceId + PathPart: 'identity' + IdentityServiceProvidersResource: + Type: AWS::ApiGateway::Resource + Properties: + RestApiId: !Ref SaaSBoostApi + ParentId: !Ref IdentityServiceResource + PathPart: 'providers' + IdentityServiceResourceCORS: + Type: AWS::ApiGateway::Method + Properties: + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref IdentityServiceResource + HttpMethod: OPTIONS + AuthorizationType: NONE + Integration: + Type: MOCK + PassthroughBehavior: WHEN_NO_MATCH + IntegrationResponses: + - StatusCode: '200' + ResponseTemplates: {application/json: ''} + ResponseParameters: + method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" + method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS,POST'" + method.response.header.Access-Control-Allow-Origin: "'*'" + method.response.header.Access-Control-Max-Age: "'3600'" + method.response.header.X-Requested-With: "'*'" + RequestTemplates: + application/json: '{"statusCode": 200}' + MethodResponses: + - StatusCode: '200' + ResponseModels: {application/json: Empty} + ResponseParameters: + method.response.header.Access-Control-Allow-Headers: false + method.response.header.Access-Control-Allow-Methods: false + method.response.header.Access-Control-Allow-Origin: false + method.response.header.Access-Control-Max-Age: false + method.response.header.X-Requested-With: false + IdentityServiceGetProviderMethod: + Type: AWS::ApiGateway::Method + Properties: + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref IdentityServiceResource + HttpMethod: GET + AuthorizationType: CUSTOM + AuthorizerId: !Ref ApiGatewayLambdaAuthorizer + Integration: + Type: AWS_PROXY + IntegrationHttpMethod: POST + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${IdentityServiceGetProvider}/invocations + PassthroughBehavior: WHEN_NO_MATCH + MethodResponses: + - StatusCode: '200' + ResponseModels: {application/json: Empty} + ResponseParameters: + method.response.header.Access-Control-Allow-Origin: false + IdentityServiceGetProviderLambdaPermission: + Type: AWS::Lambda::Permission + Properties: + Principal: apigateway.amazonaws.com + Action: lambda:InvokeFunction + FunctionName: !Ref IdentityServiceGetProvider + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/identity + IdentityServiceSetProviderMethod: + Type: AWS::ApiGateway::Method + Properties: + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref IdentityServiceResource + HttpMethod: POST + AuthorizationType: CUSTOM + AuthorizerId: !Ref ApiGatewayLambdaAuthorizer + Integration: + Type: AWS_PROXY + IntegrationHttpMethod: POST + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${IdentityServiceSetProvider}/invocations + PassthroughBehavior: WHEN_NO_MATCH + MethodResponses: + - StatusCode: '200' + ResponseModels: {application/json: Empty} + ResponseParameters: + method.response.header.Access-Control-Allow-Origin: false + IdentityServiceSetProviderLambdaPermission: + Type: AWS::Lambda::Permission + Properties: + Principal: apigateway.amazonaws.com + Action: lambda:InvokeFunction + FunctionName: !Ref IdentityServiceSetProvider + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/POST/identity + IdentityServiceGetProvidersMethod: + Type: AWS::ApiGateway::Method + Properties: + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref IdentityServiceProvidersResource + HttpMethod: GET + AuthorizationType: CUSTOM + AuthorizerId: !Ref ApiGatewayLambdaAuthorizer + Integration: + Type: AWS_PROXY + IntegrationHttpMethod: POST + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${IdentityServiceGetProviders}/invocations + PassthroughBehavior: WHEN_NO_MATCH + MethodResponses: + - StatusCode: '200' + ResponseModels: {application/json: Empty} + ResponseParameters: + method.response.header.Access-Control-Allow-Origin: false + IdentityServiceGetProvidersLambdaPermission: + Type: AWS::Lambda::Permission + Properties: + Principal: apigateway.amazonaws.com + Action: lambda:InvokeFunction + FunctionName: !Ref IdentityServiceGetProviders + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/identity/providers + IdentityServiceProvidersResourceCORS: + Type: AWS::ApiGateway::Method + Properties: + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref IdentityServiceProvidersResource + HttpMethod: OPTIONS + AuthorizationType: NONE + Integration: + Type: MOCK + PassthroughBehavior: WHEN_NO_MATCH + IntegrationResponses: + - StatusCode: '200' + ResponseTemplates: {application/json: ''} + ResponseParameters: + method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" + method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS,POST'" + method.response.header.Access-Control-Allow-Origin: "'*'" + method.response.header.Access-Control-Max-Age: "'3600'" + method.response.header.X-Requested-With: "'*'" + RequestTemplates: + application/json: '{"statusCode": 200}' + MethodResponses: + - StatusCode: '200' + ResponseModels: {application/json: Empty} + ResponseParameters: + method.response.header.Access-Control-Allow-Headers: false + method.response.header.Access-Control-Allow-Methods: false + method.response.header.Access-Control-Allow-Origin: false + method.response.header.Access-Control-Max-Age: false + method.response.header.X-Requested-With: false + ApiDocsResource: + Type: AWS::ApiGateway::Resource + Properties: + RestApiId: !Ref SaaSBoostApi + ParentId: !Ref RootResourceId + PathPart: 'docs' + ApiDocsGetMethod: + Type: AWS::ApiGateway::Method + Properties: + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref ApiDocsResource + HttpMethod: GET + AuthorizationType: NONE + Integration: + Type: AWS_PROXY + IntegrationHttpMethod: POST + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiDocs.Arn}/invocations + PassthroughBehavior: WHEN_NO_MATCH + MethodResponses: + - StatusCode: '200' + ResponseModels: + application/json: Empty + ResponseParameters: + method.response.header.Access-Control-Allow-Origin: false + ApiDocsLambdaPermission: + Type: AWS::Lambda::Permission + Properties: + Principal: apigateway.amazonaws.com + Action: lambda:InvokeFunction + FunctionName: !Ref ApiDocs + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/GET/docs + ApiDocsProxyResource: + Type: AWS::ApiGateway::Resource + Properties: + RestApiId: !Ref SaaSBoostApi + ParentId: !Ref ApiDocsResource + PathPart: '{proxy+}' + ApiDocsProxyMethod: + Type: AWS::ApiGateway::Method + Properties: + RestApiId: !Ref SaaSBoostApi + ResourceId: !Ref ApiDocsProxyResource + HttpMethod: ANY + AuthorizationType: NONE + RequestParameters: + method.request.path.proxy: true + Integration: + Type: AWS_PROXY + IntegrationHttpMethod: POST + Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiDocs.Arn}/invocations + PassthroughBehavior: WHEN_NO_MATCH + RequestParameters: + integration.request.path.proxy: method.request.path.proxy + CacheKeyParameters: + - method.request.path.proxy + MethodResponses: + - StatusCode: '200' + ResponseModels: + application/json: Empty + ResponseParameters: + method.response.header.Access-Control-Allow-Origin: false + ApiDocsProxyLambdaPermission: + Type: AWS::Lambda::Permission + Properties: + Principal: apigateway.amazonaws.com + Action: lambda:InvokeFunction + FunctionName: !Ref ApiDocs + SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostApi}/*/ANY/docs/{proxy+} # To Do: Remove deployment from CloudFormation and deal with it either through # CodePipeline or a custom resource, or some other external deployment action. ApiDeployment: Type: AWS::ApiGateway::Deployment DependsOn: - - BillingServicePlansResourceCORS - - BillingServiceGetPlansMethod - - MetricsServiceQueryMethod - - MetricsServiceQueryResourceCORS - - MetricsServiceDatasetsMethod - - MetricsServiceDatasetsResourceCORS - - MetricsServiceAlbQueryMethod - - MetricsServiceAlbQueryResourceCORS - - MetricsServiceAlbQueryByTenantIdMethod - - MetricsServiceAlbQueryByTenantIdResourceCORS +# - BillingServicePlansResourceCORS +# - BillingServiceGetPlansMethod +# - MetricsServiceQueryMethod +# - MetricsServiceQueryResourceCORS +# - MetricsServiceDatasetsMethod +# - MetricsServiceDatasetsResourceCORS +# - MetricsServiceAlbQueryMethod +# - MetricsServiceAlbQueryResourceCORS +# - MetricsServiceAlbQueryByTenantIdMethod +# - MetricsServiceAlbQueryByTenantIdResourceCORS + - MetricsServiceResourceCORS + - MetricsServicePutMethod - OnboardingServiceGetAllMethod - - OnboardingServiceStartMethod + - OnboardingServiceInsertMethod - OnboardingServiceResourceCORS - OnboardingServiceByIdMethod - OnboardingServiceByIdResourceCORS - SettingsServiceGetAllMethod - SettingsServiceResourceCORS - - SettingsServiceOptionsMethod - - SettingsServiceOptionsResourceCORS - - SettingsServiceGetAppConfigMethod - - SettingsServiceUpdateAppConfigMethod - - SettingsServiceConfigResourceCORS - SettingsServiceGetByIdMethod + - SettingsServiceUpdateMethod - SettingsServiceByIdResourceCORS + - SettingsServiceSecretMethod + - SettingsServiceSecretResourceCORS + - AppConfigServiceOptionsMethod + - AppConfigServiceOptionsResourceCORS + - AppConfigServiceGetMethod + - AppConfigServiceUpdateMethod + - AppConfigServiceDeleteMethod + - AppConfigServiceResourceCORS - TenantServiceGetAllMethod + - TenantServiceInsertMethod - TenantServiceResourceCORS - TenantServiceGetByIdMethod - TenantServiceUpdateMethod @@ -1907,6 +2602,13 @@ Resources: - TenantServiceEnableResourceCORS - TenantServiceDisableMethod - TenantServiceDisableResourceCORS + - TierServiceGetAllMethod + - TierServiceInsertMethod + - TierServiceGetByIdMethod + - TierServiceUpdateMethod + - TierServiceDeleteMethod + - TierServiceByIdResourceCORS + - TierServiceResourceCORS - SystemUserServiceGetAllMethod - SystemUserServiceInsertMethod - SystemUserServiceResourceCORS @@ -1918,14 +2620,21 @@ Resources: - SystemUserServiceEnableResourceCORS - SystemUserServiceDisableMethod - SystemUserServiceDisableResourceCORS - Properties: - RestApiId: !Ref PublicApi - ApiStage: + - IdentityServiceResourceCORS + - IdentityServiceGetProviderMethod + - IdentityServiceSetProviderMethod + - IdentityServiceGetProvidersMethod + - IdentityServiceProvidersResourceCORS + - ApiDocsProxyMethod + - ApiDocsGetMethod + Properties: + RestApiId: !Ref SaaSBoostApi + ApiGatewayStage: Type: AWS::ApiGateway::Stage DependsOn: ApiGatewayLoggingAccount Properties: - RestApiId: !Ref PublicApi - StageName: !Ref PublicApiStage + RestApiId: !Ref SaaSBoostApi + StageName: !Ref ApiStage DeploymentId: !Ref ApiDeployment AccessLogSetting: DestinationArn: !GetAtt ApiGatewayAccessLogGroup.Arn @@ -1936,7 +2645,7 @@ Resources: LoggingLevel: INFO ResourcePath: '/*' Outputs: - PublicApiGatewayEndpoint: - Description: SaaS Boost Admin PI Gateway Invoke URL - Value: !Sub 'https://${PublicApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${ApiStage}' + ApiGatewayEndpoint: + Description: SaaS Boost API Gateway Invoke URL + Value: !Sub 'https://${SaaSBoostApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${ApiStage}' ... \ No newline at end of file diff --git a/resources/saas-boost-app-integration.yaml b/resources/saas-boost-app-integration.yaml new file mode 100644 index 00000000..ca648fd2 --- /dev/null +++ b/resources/saas-boost-app-integration.yaml @@ -0,0 +1,370 @@ +--- +AWSTemplateFormatVersion: 2010-09-09 +Description: AWS SaaS Boost Control Plane Integration +Parameters: + Environment: + Description: SaaS Boost control plane environment label + Type: String + EventBusArn: + Description: SaaS Boost control plane EventBus ARN + Type: String + ApiAppClientSecretArn: + Description: SaaS Boost API App Client SecretsManager ARN + Type: String + EncryptionKeyArn: + Description: SaaS Boost KMS key ARN for SecretsManager secrets + Type: String + UtilsLayerArn: + Description: SaaS Boost Utils Lambda Layer ARN + Type: String + CloudFormationUtilsLayerArn: + Description: SaaS Boost CloudFormation Utils Lambda Layer ARN + Type: String + ApiHelperLayerArn: + Description: SaaS Boost API Helper Lambda Layer ARN + Type: String +Resources: + # Event bus to receive from and relay events to the SaaS Boost control plane + # Permissions for which SaaS Boost events this account is allowed to put on the + # control plane EventBus are defined by the control plane + AppPlaneEventBus: + Type: AWS::Events::EventBus + Properties: + Name: !Select [1, !Split ['/', !Ref EventBusArn]] + AppPlaneEventBusDlq: + Type: AWS::SQS::Queue + AppPlaneEventBusDlqPolicy: + Type: AWS::SQS::QueuePolicy + Properties: + Queues: + - Ref: AppPlaneEventBusDlq + PolicyDocument: + Statement: + - Effect: Allow + Action: + - SQS:SendMessage + Resource: !GetAtt AppPlaneEventBusDlq.Arn + Principal: + Service: events.amazonaws.com + Condition: + ArnEquals: + "aws:SourceArn": + - !GetAtt SaaSBoostEventsSubscriberRule.Arn + # Allow the control plane to publish events on the app plane EventBus + ControlPlaneEventBusToAppPlaneEventBusRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub sb-${Environment}-ctrl-plane-events-role-${AWS::Region} + Path: '/' + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - events.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyName: !Sub sb-${Environment}-ctrl-plane-events-policy + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: events:PutEvents + Resource: !GetAtt AppPlaneEventBus.Arn + # Events the app plane should be listening for from the SaaS Boost control plane + SaaSBoostEventsSubscriberRule: + Type: AWS::Events::Rule + Properties: + Name: !Sub sb-${Environment}-ctrl-plane-events + Description: App Plane Subscription to SaaS Boost Control Plane Events + EventBusName: !Ref EventBusArn + EventPattern: + source: + - saas-boost + detail-type: + - Application Configuration Changed + - Onboarding Initiated + - Onboarding Tenant Assigned + - Onboarding Completed + - Tenant Deleted + - Tenant Enabled + - Tenant Disabled + - Tenant Tier Changed + State: ENABLED + Targets: + - Arn: !GetAtt AppPlaneEventBus.Arn + Id: !Sub sb-${Environment}-ctrl-plane-events + RoleArn: !GetAtt ControlPlaneEventBusToAppPlaneEventBusRole.Arn + # This policy allows access to the OAuth app client details in the control + # plane's Secrets Manager. This app client can do a client credentials grant + # for an access token the API authorizer will accept. This policy should be + # added to any execution role for compute that will be calling the SaaS Boost API. + ControlPlaneApiTrustPolicy: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: !Sub sb-${Environment}-api-policy-${AWS::Region} + Description: Access to the API client secret from the SaaS Boost control plane account + Path: '/' + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: + - !Ref ApiAppClientSecretArn + - Effect: Allow + Action: + - kms:Decrypt + - kms:DescribeKey + Resource: + - !Ref EncryptionKeyArn + # Save some things in Parameter Store to make them easier to use in other places + ControlPlaneApiTrustPolicyParam: + Type: AWS::SSM::Parameter + Properties: + Name: !Sub /saas-boost/${Environment}/app/API_CLIENT_TRUST_POLICY + Type: String + Value: !Ref ControlPlaneApiTrustPolicy + AppPlaneEventBusParam: + Type: AWS::SSM::Parameter + Properties: + Name: !Sub /saas-boost/${Environment}/app/EVENT_BUS + Type: String + Value: !Ref AppPlaneEventBus + ApiClientSecretParam: + Type: AWS::SSM::Parameter + Properties: + Name: !Sub /saas-boost/${Environment}/app/API_APP_CLIENT_SECRET + Type: String + Value: !Ref ApiAppClientSecretArn + UtilsLayerParam: + Type: AWS::SSM::Parameter + Properties: + Name: !Sub /saas-boost/${Environment}/app/UTILS_LAYER + Type: String + Value: !Ref UtilsLayerArn + CloudFormationUtilsLayerParam: + Type: AWS::SSM::Parameter + Properties: + Name: !Sub /saas-boost/${Environment}/app/CFN_LAYER + Type: String + Value: !Ref CloudFormationUtilsLayerArn + ApiClientHelperLayerParam: + Type: AWS::SSM::Parameter + Properties: + Name: !Sub /saas-boost/${Environment}/app/API_CLIENT_HELPER_LAYER + Type: String + Value: !Ref ApiHelperLayerArn + + # + # Below are some optional resources for debugging and getting started + # + + # LogGroup to debug EventBridge cross account communication + SaaSBoostEventsLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/events/${AppPlaneEventBus} + RetentionInDays: 30 + SaaSBoostEventsLogsResourcePolicy: + Type: AWS::Logs::ResourcePolicy + Properties: + PolicyName: !Sub sb-${Environment}-event-logs + PolicyDocument: + !Join + - '' + - - "{\"Version\": \"2012-10-17\",\"Statement\": [{\"Effect\": \"Allow\",\"Action\": [\"logs:CreateLogStream\",\"logs:PutLogEvents\"]," + - "\"Principal\": {\"Service\": [\"events.amazonaws.com\",\"delivery.logs.amazonaws.com\"]}, \"Resource\": [\"arn:" + - !Ref AWS::Partition + - ":logs:" + - !Ref AWS::Region + - ":" + - !Ref AWS::AccountId + - ":log-group:/aws/events/" + - !Ref AppPlaneEventBus + - ":*" + - "\"]}]}" + SaaSBoostEventsTargetRule: + Type: AWS::Events::Rule + Properties: + Name: !Sub sb-${Environment}-ctrl-plane-event-logger + Description: SaaS Boost Control Plane Event Logger Target + EventBusName: !Ref AppPlaneEventBus + EventPattern: + source: + - saas-boost + detail-type: + - Application Configuration Changed + - Onboarding Initiated + - Onboarding Tenant Assigned + - Onboarding Completed + - Tenant Deleted + - Tenant Enabled + - Tenant Disabled + - Tenant Tier Changed + State: ENABLED + Targets: + - Arn: !GetAtt SaaSBoostEventsLogs.Arn + Id: !Sub sb-${Environment}-event-logger + # Example event producer role + AppPlaneEventProducerExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub sb-${Environment}-event-producer-role-${AWS::Region} + Path: '/' + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyName: !Sub sb-${Environment}-event-producer-policy + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - logs:PutLogEvents + Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* + - Effect: Allow + Action: + - logs:CreateLogStream + - logs:DescribeLogStreams + Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* + - Effect: Allow + Action: + - events:PutEvents + Resource: !Ref EventBusArn + AppPlaneEventProducerLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-app-plane-events + RetentionInDays: 30 + # Sample app plane event producer + AppPlaneEventProducer: + Type: AWS::Lambda::Function + DependsOn: AppPlaneEventProducerLogs + Properties: + FunctionName: !Sub sb-${Environment}-app-plane-events + Role: !GetAtt AppPlaneEventProducerExecutionRole.Arn + Runtime: python3.11 + Architectures: + - arm64 + Timeout: 10 + MemorySize: 512 + Handler: index.lambda_handler + Code: + ZipFile: | + import json + import os + import boto3 + from botocore.exceptions import ClientError + + SAAS_BOOST_EVENT_BUS = os.environ['SAAS_BOOST_EVENT_BUS'] + events = boto3.client('events') + + # Invoke with an event object that contains a valid SaaS Boost + # event detail type and corresponding detail object (escaped JSON) + # For example: + # { + # "detailType": "Onboarding Validated", + # "detail": "{\"onboardingId\": \"UUID Value\"}" + # } + def lambda_handler(event, context): + print(json.dumps(event, default=str)) + detail_type = event['detailType'] + detail = json.loads(event['detail']) + try: + response = events.put_events( + Entries=[ + { + 'EventBusName': SAAS_BOOST_EVENT_BUS, + 'Source': 'saas-boost', + 'DetailType': detail_type, + 'Detail': json.dumps(detail) + } + ] + ) + print(f"Event sent to {SAAS_BOOST_EVENT_BUS}") + print(response['Entries']) + except ClientError as eventbridge_error: + print(str(eventbridge_error)) + Environment: + Variables: + SAAS_BOOST_EVENT_BUS: !Ref EventBusArn + # Example SaaS Boost API client role + AppPlaneApiClientExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub sb-${Environment}-api-client-role-${AWS::Region} + Path: '/' + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + ManagedPolicyArns: + - !Ref ControlPlaneApiTrustPolicy + - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + AppPlaneApiClientLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-app-plane-api-client + RetentionInDays: 30 + # Sample SaaS Boost API client using the helper Lambda Layer + AppPlaneApiClient: + Type: AWS::Lambda::Function + DependsOn: AppPlaneApiClientLogs + Properties: + FunctionName: !Sub sb-${Environment}-app-plane-api-client + Role: !GetAtt AppPlaneApiClientExecutionRole.Arn + Runtime: python3.11 + Architectures: + - arm64 + Timeout: 30 + MemorySize: 512 + Handler: index.lambda_handler + Code: + ZipFile: | + import json + import os + from SaaSBoostApiHelper import SaaSBoostApiHelper + + API_CLIENT_SECRET = os.environ['API_CLIENT_SECRET'] + + # Invoke with an event object that contains the HTTP method + # (e.g. GET, POST, PUT) and the API resource (e.g. /onboarding/{id}, + # /tenant?status=provisioned) and the optional request body + # For example to get a list of all tenantonboarding requests: + # { + # "method": "GET", + # "resource": "onboarding" + # } + def lambda_handler(event, context): + print(json.dumps(event, default=str)) + + api = SaaSBoostApiHelper(API_CLIENT_SECRET) + + method = event['method'] + resource = event['resource'] + body = event.get('body') + response = api.authorized_request(method, resource, body) + + return response + Layers: + - !Ref ApiHelperLayerArn + Environment: + Variables: + API_CLIENT_SECRET: !Ref ApiAppClientSecretArn +... \ No newline at end of file diff --git a/resources/saas-boost-core.yaml b/resources/saas-boost-core.yaml deleted file mode 100644 index 975712e0..00000000 --- a/resources/saas-boost-core.yaml +++ /dev/null @@ -1,1057 +0,0 @@ ---- -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -AWSTemplateFormatVersion: 2010-09-09 -Description: AWS SaaS Boost Core Resources -Transform: - - saas-boost-app-services-macro -Parameters: - Environment: - Description: SaaS Boost environment name - Type: String - SaaSBoostBucket: - Description: SaaS Boost assets S3 bucket - Type: String - LambdaSourceFolder: - Description: Folder for lambda source code to change on each deployment - Type: String - SaaSBoostUtilsLayer: - Description: Utils Layer ARN - Type: String - ApiGatewayHelperLayer: - Description: API Gateway Helper Layer ARN - Type: String - CloudFormationUtilsLayer: - Description: CloudFormation Utils Layer ARN - Type: String - CodePipelineBucket: - Description: S3 bucket for CodePipeline artifacts - Type: String - LoggingBucket: - Description: S3 bucket for S3 access logging - Type: String - PublicApiStage: - Description: The API Gateway REST API stage name for the SaaS Boost public API - Type: String - Default: v1 - PrivateApiStage: - Description: The API Gateway REST API stage name for the SaaS Boost private API - Type: String - Default: v1 - ApplicationServices: - Description: Comma separated list of application service names to create ECR repositories for - Type: String - Default: '' - AppExtensions: - Description: Comma separated list of extension names to apply to the entire application - Type: String - Default: '' - EventBus: - Description: SaaS Boost Eventbridge Bus - Type: String -Resources: - WorkloadDeployLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-workload-deploy - RetentionInDays: 30 - WorkloadDeployExecRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub sb-${Environment}-workload-deploy-role-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: !Sub sb-${Environment}-workload-deploy-policy - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:CreateLogStream - - logs:DescribeLogStreams - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - s3:ListBucket - - s3:ListBucketVersions - - s3:GetBucketVersioning - Resource: - - !Sub arn:${AWS::Partition}:s3:::${CodePipelineBucket} - - Effect: Allow - Action: - - s3:GetObject - - s3:PutObject - - s3:DeleteObject - - s3:DeleteObjectVersion - Resource: !Sub arn:${AWS::Partition}:s3:::${CodePipelineBucket}/* - - Effect: Allow - Action: - - codepipeline:StartPipelineExecution - Resource: - - !Sub arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:* - - Effect: Allow - Action: - - sts:AssumeRole - Resource: !GetAtt SaaSBoostSystemRole.Arn - WorkloadDeployLambda: - Type: AWS::Lambda::Function - DependsOn: WorkloadDeployLogs - Properties: - FunctionName: !Sub sb-${Environment}-workload-deploy - Role: !GetAtt WorkloadDeployExecRole.Arn - Runtime: java11 - Timeout: 600 - MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.WorkloadDeploy - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/WorkloadDeploy-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref ApiGatewayHelperLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - API_TRUST_ROLE: !GetAtt SaaSBoostSystemRole.Arn - API_GATEWAY_HOST: !Sub ${SaaSBoostPrivateApi}.execute-api.${AWS::Region}.${AWS::URLSuffix} - API_GATEWAY_STAGE: !Ref PrivateApiStage - CODE_PIPELINE_BUCKET: !Ref CodePipelineBucket - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "ECSDeploy" - WorkloadDeployEventRule: - Type: AWS::Events::Rule - Properties: - EventBusName: !Ref EventBus - Name: !Sub sb-${Environment}-workload-deploy-event - Description: SaaS Boost tenant workload deploy events - State: ENABLED - EventPattern: - { - "source": [ - "saas-boost" - ], - "detail-type": [ - "Workload Ready For Deployment" - ] - } - Targets: - - Arn: !GetAtt WorkloadDeployLambda.Arn - Id: !Sub sb-${Environment}-workload-deploy-event - WorkloadDeployEventPermission: - Type: AWS::Lambda::Permission - Properties: - Action: lambda:InvokeFunction - FunctionName: !Ref WorkloadDeployLambda - Principal: events.amazonaws.com - SourceArn: !GetAtt WorkloadDeployEventRule.Arn - OnboardingStackListenerExecRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub sb-${Environment}-onboarding-listener-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: !Sub sb-${Environment}-onboarding-listener - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:CreateLogStream - - logs:DescribeLogStreams - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - cloudformation:DescribeStacks - - cloudformation:ListStackResources - Resource: - - !Sub arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/* - - Effect: Allow - Action: - - events:PutEvents - Resource: - - !Sub 'arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/{{resolve:ssm:/saas-boost/${Environment}/EVENT_BUS}}' - OnboardingStackListenerLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-onboarding-listener - RetentionInDays: 30 - OnboardingStackListener: - Type: AWS::Lambda::Function - DependsOn: OnboardingStackListenerLogs - Properties: - FunctionName: !Sub sb-${Environment}-onboarding-listener - Role: !GetAtt OnboardingStackListenerExecRole.Arn - Runtime: java11 - Timeout: 600 - MemorySize: 512 - Handler: com.amazon.aws.partners.saasfactory.saasboost.OnboardingStackListener - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/OnboardingStackListener-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref CloudFormationUtilsLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - SAAS_BOOST_EVENT_BUS: !Sub '{{resolve:ssm:/saas-boost/${Environment}/EVENT_BUS}}' - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "Onboarding" - OnboardingStackListenerTopic: - Type: AWS::SNS::Topic - Properties: - DisplayName: Tenant Onboarding Stack Notifications - TopicName: !Sub sb-${Environment}-onboarding-listener - KmsMasterKeyId: alias/aws/sns - OnboardingStackListenerSubscription: - Type: AWS::SNS::Subscription - Properties: - Protocol: lambda - Endpoint: !GetAtt OnboardingStackListener.Arn - TopicArn: !Ref OnboardingStackListenerTopic - OnboardingStackListenerPermission: - Type: AWS::Lambda::Permission - Properties: - FunctionName: !Ref OnboardingStackListener - Principal: sns.amazonaws.com - Action: lambda:InvokeFunction - SourceArn: !Ref OnboardingStackListenerTopic - SSMParamOnboardingSNS: - Type: AWS::SSM::Parameter - Properties: - Name: !Sub /saas-boost/${Environment}/ONBOARDING_STACK_SNS - Type: String - Value: !Ref OnboardingStackListenerTopic - OnboardingAppStackListenerLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-onboarding-app-listener - RetentionInDays: 30 - OnboardingAppStackListener: - Type: AWS::Lambda::Function - DependsOn: OnboardingAppStackListenerLogs - Properties: - FunctionName: !Sub sb-${Environment}-onboarding-app-listener - Role: !GetAtt OnboardingStackListenerExecRole.Arn # Can use the same IAM role as the tenant-onboarding.yaml stack - Runtime: java11 - Timeout: 600 - MemorySize: 512 - Handler: com.amazon.aws.partners.saasfactory.saasboost.OnboardingAppStackListener - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/OnboardingAppStackListener-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref CloudFormationUtilsLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - SAAS_BOOST_EVENT_BUS: !Sub '{{resolve:ssm:/saas-boost/${Environment}/EVENT_BUS}}' - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "Onboarding" - OnboardingAppStackListenerTopic: - Type: AWS::SNS::Topic - Properties: - DisplayName: Tenant Onboarding App Stack Notifications - TopicName: !Sub sb-${Environment}-onboarding-app-listener - KmsMasterKeyId: alias/aws/sns - OnboardingAppStackListenerSubscription: - Type: AWS::SNS::Subscription - Properties: - Protocol: lambda - Endpoint: !GetAtt OnboardingAppStackListener.Arn - TopicArn: !Ref OnboardingAppStackListenerTopic - OnboardingAppStackListenerPermission: - Type: AWS::Lambda::Permission - Properties: - FunctionName: !Ref OnboardingAppStackListener - Principal: sns.amazonaws.com - Action: lambda:InvokeFunction - SourceArn: !Ref OnboardingAppStackListenerTopic - SSMParamOnboardingAppSNS: - Type: AWS::SSM::Parameter - Properties: - Name: !Sub /saas-boost/${Environment}/ONBOARDING_APP_STACK_SNS - Type: String - Value: !Ref OnboardingAppStackListenerTopic - TenantCodePipelineRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub sb-${Environment}-tenant-pipeline-role-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - codepipeline.amazonaws.com - Action: - - sts:AssumeRole - ManagedPolicyArns: - - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonECS_FullAccess - Policies: - - PolicyName: !Sub sb-${Environment}-tenant-pipeline-policy - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - iam:PassRole - Resource: '*' - Condition: - StringEqualsIfExists: - iamPassedToService: - - ecs-tasks.amazonaws.com - - Effect: Allow - Action: - - ecr:DescribeImages - Resource: !Sub arn:${AWS::Partition}:ecr:${AWS::Region}:${AWS::AccountId}:repository/* - - Effect: Allow - Action: - - s3:ListBucket - - s3:GetBucketVersioning - Resource: !Sub arn:${AWS::Partition}:s3:::${CodePipelineBucket} - - Effect: Allow - Action: - - s3:PutObject - - s3:GetObject - - s3:GetObjectVersion - Resource: !Sub arn:${AWS::Partition}:s3:::${CodePipelineBucket}/* - - Effect: Allow - Action: - - lambda:ListFunctions - Resource: '*' - - Effect: Allow - Action: - - lambda:InvokeFunction - Resource: - - !GetAtt CodePipelineUpdateEcsServiceLambda.Arn - SSMParamCodePipelineRole: - Type: AWS::SSM::Parameter - Properties: - Name: !Sub /saas-boost/${Environment}/CODE_PIPELINE_ROLE - Type: String - Value: !GetAtt TenantCodePipelineRole.Arn - CodePipelineUpdateEcsServiceExecRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub sb-${Environment}-update-ecs-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: !Sub sb-${Environment}-update-ecs - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:CreateLogStream - - logs:DescribeLogStreams - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - codepipeline:PutJobSuccessResult - - codepipeline:PutJobFailureResult - Resource: '*' - - Effect: Allow - Action: - - ecs:DescribeServices - - ecs:UpdateService - Resource: - - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:service/* - Condition: - StringLike: - ecs:cluster: - - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/sb-${Environment}-tenant* - - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/sb-${Environment}-keycloak - CodePipelineUpdateEcsServiceLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-update-ecs - RetentionInDays: 30 - CodePipelineUpdateEcsServiceLambda: - Type: AWS::Lambda::Function - DependsOn: CodePipelineUpdateEcsServiceLogs - Properties: - FunctionName: !Sub sb-${Environment}-update-ecs - Role: !GetAtt CodePipelineUpdateEcsServiceExecRole.Arn - Runtime: java11 - Timeout: 300 - MemorySize: 512 - Handler: com.amazon.aws.partners.saasfactory.saasboost.EcsServiceUpdate - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/EcsServiceUpdate-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - Environment: - Variables: - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "ECSDeploy" - SaaSBoostPublicApi: - Type: AWS::ApiGateway::RestApi - Properties: - Name: !Sub sb-${Environment}-public-api - EndpointConfiguration: - Types: - - REGIONAL - SaaSBoostPrivateApi: - Type: AWS::ApiGateway::RestApi - Properties: - Name: !Sub sb-${Environment}-private-api - EndpointConfiguration: - Types: - - REGIONAL - SaaSBoostSystemRole: - Type: AWS::IAM::Role - DependsOn: - - SaaSBoostPublicApi - - SaaSBoostPrivateApi - Properties: - RoleName: !Sub sb-${Environment}-private-api-trust-role-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root - Action: - - sts:AssumeRole - Policies: - - PolicyName: !Sub sb-${Environment}-private-api-trust-policy - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - execute-api:Invoke - Resource: - - !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostPrivateApi}/*/*/* - - !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${SaaSBoostPublicApi}/*/*/* - SSMParamPrivateApiRole: - Type: AWS::SSM::Parameter - Properties: - Name: !Sub /saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE - Type: String - Value: !GetAtt SaaSBoostSystemRole.Arn - SystemRestClientExecRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub sb-${Environment}-private-api-client-role-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: !Sub sb-${Environment}-private-api-client-policy - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:CreateLogStream - - logs:DescribeLogStreams - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - sts:AssumeRole - Resource: !GetAtt SaaSBoostSystemRole.Arn - SystemRestApiClientLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-private-api-client - RetentionInDays: 30 - SystemRestApiClient: - Type: AWS::Lambda::Function - Properties: - FunctionName: !Sub sb-${Environment}-private-api-client - Role: !GetAtt SystemRestClientExecRole.Arn - Runtime: java11 - Timeout: 300 - MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.SystemRestApiClient - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/SystemRestApiClient-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref ApiGatewayHelperLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - API_TRUST_ROLE: !GetAtt SaaSBoostSystemRole.Arn - API_GATEWAY_HOST: !Sub ${SaaSBoostPrivateApi}.execute-api.${AWS::Region}.${AWS::URLSuffix} - API_GATEWAY_STAGE: !Ref PrivateApiStage - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "APIClient" - SystemApiEventRule: - Type: AWS::Events::Rule - Properties: - EventBusName: !Ref EventBus - Name: !Sub sb-${Environment}-system-api-call - State: ENABLED - EventPattern: - source: ['saas-boost'] - detail-type: ['System API Call'] - Targets: - - Arn: !GetAtt SystemRestApiClient.Arn - Id: !Sub sb-${Environment}-private-api-client - SystemRestApiClientEventPermission: - Type: AWS::Lambda::Permission - Properties: - FunctionName: !Ref SystemRestApiClient - Principal: events.amazonaws.com - Action: lambda:InvokeFunction - SourceArn: !GetAtt SystemApiEventRule.Arn - EcsShutdownServicesExecRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub sb-${Environment}-ecs-shutdown-services-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: !Sub sb-${Environment}-ecs-shutdown-services-policy - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:CreateLogStream - - logs:DescribeLogStreams - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - ecs:DescribeServices - - ecs:UpdateService - Resource: - - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:service/* - Condition: - StringLike: - ecs:cluster: - - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/sb-${Environment}-tenant* - - Effect: Allow - Action: - - sts:AssumeRole - Resource: !GetAtt SaaSBoostSystemRole.Arn - EcsShutdownServicesLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-ecs-shutdown-services - RetentionInDays: 30 - EcsShutdownServicesLambda: - Type: AWS::Lambda::Function - DependsOn: EcsShutdownServicesLogs - Properties: - FunctionName: !Sub sb-${Environment}-ecs-shutdown-services - Role: !GetAtt EcsShutdownServicesExecRole.Arn - Runtime: java11 - Timeout: 600 - MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.EcsShutdownServices - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/EcsShutdownServices-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref ApiGatewayHelperLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - API_TRUST_ROLE: !GetAtt SaaSBoostSystemRole.Arn - API_GATEWAY_HOST: !Sub ${SaaSBoostPrivateApi}.execute-api.${AWS::Region}.${AWS::URLSuffix} - API_GATEWAY_STAGE: !Ref PrivateApiStage - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "EcsShutdownServices" - EcsStartupServicesExecRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub sb-${Environment}-ecs-startup-services-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: !Sub sb-${Environment}-ecs-startup-services-policy - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:CreateLogStream - - logs:DescribeLogStreams - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - ecs:DescribeServices - - ecs:UpdateService - Resource: - - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:service/* - Condition: - StringLike: - ecs:cluster: - - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/sb-${Environment}-tenant* - - Effect: Allow - Action: - - sts:AssumeRole - Resource: !GetAtt SaaSBoostSystemRole.Arn - EcsStartupServicesLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-ecs-startup-services - RetentionInDays: 30 - EcsStartupServicesLambda: - Type: AWS::Lambda::Function - DependsOn: EcsStartupServicesLogs - Properties: - FunctionName: !Sub sb-${Environment}-ecs-startup-services - Role: !GetAtt EcsStartupServicesExecRole.Arn - Runtime: java11 - Timeout: 600 - MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.EcsStartupServices - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/EcsStartupServices-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref ApiGatewayHelperLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - API_TRUST_ROLE: !GetAtt SaaSBoostSystemRole.Arn - API_GATEWAY_HOST: !Sub ${SaaSBoostPrivateApi}.execute-api.${AWS::Region}.${AWS::URLSuffix} - API_GATEWAY_STAGE: !Ref PrivateApiStage - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "EcsStartupServices" - SetInstanceProtectionExecRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub sb-${Environment}-set-instance-protection-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: !Sub sb-${Environment}-set-instance-protection - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:DescribeLogStreams - - logs:CreateLogStream - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - autoscaling:DescribeAutoScalingInstances - - autoscaling:DescribeAutoScalingGroups - - autoscaling:DescribeTags - Resource: '*' - - Effect: Allow - Action: - - autoscaling:SetInstanceProtection - Resource: !Sub arn:${AWS::Partition}:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/sb-${Environment}-tenant-* - SetInstanceProtectionLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-set-instance-protection - RetentionInDays: 30 - SetInstanceProtectionFunction: - Type: AWS::Lambda::Function - DependsOn: - - SetInstanceProtectionLogs - Properties: - FunctionName: !Sub sb-${Environment}-set-instance-protection - Role: !GetAtt SetInstanceProtectionExecRole.Arn - Runtime: java11 - Timeout: 870 - MemorySize: 640 - Handler: com.amazon.aws.partners.saasfactory.saasboost.SetInstanceProtection - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/SetInstanceProtection-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref CloudFormationUtilsLayer - Environment: - Variables: - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "SetInstanceProtection" - AttachCapacityProviderExecRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub sb-${Environment}-attach-capacity-provider-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: !Sub sb-${Environment}-attach-capacity-provider-${AWS::Region} - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:DescribeLogStreams - - logs:CreateLogStream - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - dynamodb:Scan - - dynamodb:UpdateItem - Resource: !Sub arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/sb-${Environment}-onboarding - - Effect: Allow - Action: - - ecs:DescribeClusters - - ecs:PutClusterCapacityProviders - Resource: !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/* - AttachCapacityProviderLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-attach-capacity-provider - RetentionInDays: 30 - AttachCapacityProviderFunction: - Type: AWS::Lambda::Function - DependsOn: - - AttachCapacityProviderLogs - Properties: - FunctionName: !Sub sb-${Environment}-attach-capacity-provider - Role: !GetAtt AttachCapacityProviderExecRole.Arn - Runtime: java11 - Timeout: 870 - MemorySize: 640 - Handler: com.amazon.aws.partners.saasfactory.saasboost.AttachEcsCapacityProvider - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/AttachEcsCapacityProvider-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref CloudFormationUtilsLayer - Environment: - Variables: - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "AttachEcsCapacityProvider" - CodeBuildStartLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-start-build - RetentionInDays: 30 - CodeBuildStartExecRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub sb-${Environment}-start-build-role-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: !Sub sb-${Environment}-start-build-policy - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:CreateLogStream - - logs:DescribeLogStreams - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - codebuild:StartBuild - Resource: !Sub arn:${AWS::Partition}:codebuild:${AWS::Region}:${AWS::AccountId}:project/* - CodeBuildStartLambda: - Type: AWS::Lambda::Function - DependsOn: CodeBuildStartLogs - Properties: - FunctionName: !Sub sb-${Environment}-start-build - Role: !GetAtt CodeBuildStartExecRole.Arn - Runtime: java11 - Timeout: 600 - MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.StartCodeBuild - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/StartCodeBuild-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref CloudFormationUtilsLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - CodePipelineWaitHandlerLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-pipeline-waithandler - RetentionInDays: 30 - CodePipelineWaitHandlerExecRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub sb-${Environment}-pipeline-waithandler-role-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: !Sub sb-${Environment}-pipeline-waithandler-policy - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:CreateLogStream - - logs:DescribeLogStreams - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - codepipeline:PutJobSuccessResult - - codepipeline:PutJobFailureResult - Resource: '*' - CodePipelineWaitHandlerLambda: - Type: AWS::Lambda::Function - DependsOn: CodePipelineWaitHandlerLogs - Properties: - FunctionName: !Sub sb-${Environment}-pipeline-waithandler - Role: !GetAtt CodePipelineWaitHandlerExecRole.Arn - Runtime: java11 - Timeout: 600 - MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.CodePipelineWaitHandler - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/CodePipelineWaitHandler-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref CloudFormationUtilsLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - # Macro transform will add as many AWS::ECR::Repository resources as necessary - # based on the length of the list of ApplicationServices passed as a parameter -Outputs: - CodePipelineUpdateEcsService: - Description: Lambda to update ECS desired count - Value: !Ref CodePipelineUpdateEcsServiceLambda - StartCodeBuildLambda: - Description: Lambda ARN to trigger a CodeBuild project - Value: !GetAtt CodeBuildStartLambda.Arn - CodePipelineWaitHandler: - Description: Lambda to process CloudFormation wait condition as part of a CodePipeline - Value: !Ref CodePipelineWaitHandlerLambda - SaaSBoostPublicApi: - Description: SaaS Boost Public API - Value: !Ref SaaSBoostPublicApi - SaaSBoostPublicApiRootResourceId: - Description: SaaS Boost public API root resource id - Value: !GetAtt SaaSBoostPublicApi.RootResourceId - SaaSBoostPrivateApi: - Description: SaaS Boost Private API - Value: !Ref SaaSBoostPrivateApi - SaaSBoostPrivateApiRootResourceId: - Description: SaaS Boost private API root resource id - Value: !GetAtt SaaSBoostPrivateApi.RootResourceId -... \ No newline at end of file diff --git a/resources/saas-boost-idp-auth0.yaml b/resources/saas-boost-idp-auth0.yaml new file mode 100644 index 00000000..478d4462 --- /dev/null +++ b/resources/saas-boost-idp-auth0.yaml @@ -0,0 +1,140 @@ +--- +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +AWSTemplateFormatVersion: 2010-09-09 +Description: AWS SaaS Boost System User Auth0 IdP +Parameters: + Environment: + Description: SaaS Boost environment name + Type: String + SaaSBoostBucket: + Description: SaaS Boost assets S3 bucket + Type: String + LambdaSourceFolder: + Description: Folder for lambda source code to change on each deployment + Type: String + SaaSBoostUtilsLayer: + Description: Utils Layer ARN + Type: String + CloudFormationUtilsLayer: + Description: CloudFormation Utils Layer ARN + Type: String + AdminCredentials: + Description: Secrets Manager secret for the SaaS Boost initial admin user + Type: String + AdminWebUrl: + Description: The SaaS Boost admin web URL. + Type: String +Resources: + Auth0SetupLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-auth0-setup + RetentionInDays: 30 + Auth0SetupExecRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub sb-${Environment}-auth0-setup-role-${AWS::Region} + Path: '/' + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyName: !Sub sb-${Environment}-auth0-setup-policy + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - logs:PutLogEvents + Resource: + - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* + - Effect: Allow + Action: + - logs:CreateLogStream + - logs:DescribeLogStreams + Resource: + - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: + - !Ref AdminCredentials + - !Sub arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:sb-${Environment}-auth0-admin* + Auth0SetupLambda: + Type: AWS::Lambda::Function + DependsOn: Auth0SetupLogs + Properties: + FunctionName: !Sub sb-${Environment}-auth0-setup + Role: !GetAtt Auth0SetupExecRole.Arn + Runtime: java21 + Architectures: + - arm64 + Timeout: 600 + MemorySize: 1024 + Handler: com.amazon.aws.partners.saasfactory.saasboost.Auth0Setup + Code: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/Auth0Setup-lambda.zip + Layers: + - !Ref SaaSBoostUtilsLayer + - !Ref CloudFormationUtilsLayer + Environment: + Variables: + SAAS_BOOST_ENV: !Ref Environment + Tags: + - Key: Application + Value: SaaSBoost + - Key: Environment + Value: !Ref Environment + InvokeAuth0Setup: + Type: Custom::CustomResource + Properties: + ServiceToken: !GetAtt Auth0SetupLambda.Arn + Auth0Credentials: !Sub sb-${Environment}-auth0-admin + AdminUserCredentials: !Ref AdminCredentials + ConnectionName: !Sub sb-${Environment} + AdminWebAppUrl: !Ref AdminWebUrl +Outputs: + OidcIssuer: + Description: The OIDC issuer for this Auth0 Connection + Value: '' + OidcDomain: + Description: The OIDC domain URL (this can be blank because Auth0 supports OIDC end_session_endpoint) + Value: '' + OidcTokenEndpoint: + Description: The OIDC token endpoint for this Auth0 Connection + Value: '' + AdminWebAppClientName: + Description: Public OAuth app client with PKCE for authorization code grant + Value: !GetAtt InvokeAuth0Setup.AdminWebAppClientName + AdminWebAppClientId: + Description: Public OAuth app client id + Value: !GetAtt InvokeAuth0Setup.AdminWebAppClientId + ApiAppClientName: + Description: Private OAuth app client for client credentials grant + Value: !GetAtt InvokeAuth0Setup.ApiAppClientName + ApiAppClientId: + Description: Private OAuth app client id + Value: !GetAtt InvokeAuth0Setup.ApiAppClientId + ApiAppClientSecret: + Description: Private OAuth app client secret + Value: !GetAtt InvokeAuth0Setup.ApiAppClientSecret +... \ No newline at end of file diff --git a/resources/saas-boost-idp-cognito.yaml b/resources/saas-boost-idp-cognito.yaml new file mode 100644 index 00000000..7b93e5df --- /dev/null +++ b/resources/saas-boost-idp-cognito.yaml @@ -0,0 +1,382 @@ +--- +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +AWSTemplateFormatVersion: 2010-09-09 +Description: AWS SaaS Boost System User Cognito IdP +Parameters: + Environment: + Description: SaaS Boost environment name + Type: String + SaaSBoostBucket: + Description: SaaS Boost assets S3 bucket + Type: String + LambdaSourceFolder: + Description: Folder for lambda source code to change on each deployment + Type: String + SaaSBoostUtilsLayer: + Description: Utils Layer ARN + Type: String + CloudFormationUtilsLayer: + Description: CloudFormation Utils Layer ARN + Type: String + AdminUsername: + Description: SaaS Boost initial admin user name + Type: String + Default: admin + AdminEmailAddress: + Description: Email address of admin user to receive temporary password notification + AllowedPattern: ^[^\s@]+@[^\s@]+\.[^\s@]+$ + ConstraintDescription: Must be a valid email address. + Type: String + AdminWebUrl: + Description: The SaaS Boost admin web URL. + Type: String +Resources: + UserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: !Sub sb-${Environment}-system-users + MfaConfiguration: 'OFF' + Policies: + PasswordPolicy: + MinimumLength: 8 + RequireLowercase: true + RequireNumbers: true + RequireSymbols: false + RequireUppercase: true + TemporaryPasswordValidityDays: 7 + AdminCreateUserConfig: + AllowAdminCreateUserOnly: true + InviteMessageTemplate: + EmailMessage: !Sub | + Welcome to AWS SaaS Boost!
    +
    + You can login to your AWS SaaS Boost environment at ${AdminWebUrl}. +
    + Your username is: {username} +
    + Your temporary password is: {####} +
    + EmailSubject: !Sub AWS SaaS Boost temporary password for environment ${Environment} + UserPoolResourceServer: + Type: AWS::Cognito::UserPoolResourceServer + Properties: + UserPoolId: !Ref UserPool + Identifier: !Sub saas-boost/${Environment} + Name: !Sub sb-${Environment}-api + Scopes: + - ScopeName: read + ScopeDescription: Read Public API Access + - ScopeName: write + ScopeDescription: Write Public API Access + - ScopeName: private + ScopeDescription: Read/Write Private API Access + AdminWebAppClient: + Type: AWS::Cognito::UserPoolClient + DependsOn: UserPoolResourceServer + Properties: + ClientName: !Sub sb-${Environment}-admin-webapp-client + UserPoolId: !Ref UserPool + SupportedIdentityProviders: + - COGNITO + ExplicitAuthFlows: + - ALLOW_ADMIN_USER_PASSWORD_AUTH + - ALLOW_USER_PASSWORD_AUTH + - ALLOW_REFRESH_TOKEN_AUTH + - ALLOW_USER_SRP_AUTH + GenerateSecret: false + AllowedOAuthFlowsUserPoolClient: true + AllowedOAuthFlows: + - code + AllowedOAuthScopes: + - openid + - email + - profile + - !Sub saas-boost/${Environment}/read + - !Sub saas-boost/${Environment}/write + CallbackURLs: + - !Ref AdminWebUrl + - http://localhost:3000 + LogoutURLs: + - !Ref AdminWebUrl + - http://localhost:3000 + ApiAppClient: + Type: AWS::Cognito::UserPoolClient + DependsOn: UserPoolResourceServer + Properties: + ClientName: !Sub sb-${Environment}-api-client + UserPoolId: !Ref UserPool + SupportedIdentityProviders: + - COGNITO + GenerateSecret: true + AccessTokenValidity: 5 + TokenValidityUnits: + AccessToken: minutes + AllowedOAuthFlowsUserPoolClient: true + AllowedOAuthFlows: + - client_credentials + AllowedOAuthScopes: + - !Sub saas-boost/${Environment}/read + - !Sub saas-boost/${Environment}/write + PrivateApiAppClient: + Type: AWS::Cognito::UserPoolClient + DependsOn: UserPoolResourceServer + Properties: + ClientName: !Sub sb-${Environment}-private-api-client + UserPoolId: !Ref UserPool + SupportedIdentityProviders: + - COGNITO + GenerateSecret: true + AccessTokenValidity: 5 + TokenValidityUnits: + AccessToken: minutes + AllowedOAuthFlowsUserPoolClient: true + AllowedOAuthFlows: + - client_credentials + AllowedOAuthScopes: + - !Sub saas-boost/${Environment}/read + - !Sub saas-boost/${Environment}/write + - !Sub saas-boost/${Environment}/private + UserPoolDomain: + Type: AWS::Cognito::UserPoolDomain + Properties: + Domain: !Sub + - sb-${Environment}-saas-boost-${RandomString} + - RandomString: !Select [2, !Split ['/', !Ref AWS::StackId]] + UserPoolId: !Ref UserPool + UserPoolAdminGroup: + Type: AWS::Cognito::UserPoolGroup + Properties: + GroupName: admin + UserPoolId: !Ref UserPool + UserPoolAdminUser: + Type: AWS::Cognito::UserPoolUser + Properties: + DesiredDeliveryMediums: + - EMAIL + ForceAliasCreation: false + UserAttributes: + - Name: email + Value: !Ref AdminEmailAddress + - Name: email_verified + Value: 'true' + Username: !Ref AdminUsername + UserPoolId: !Ref UserPool + AdminUserGroupMembership: + Type: AWS::Cognito::UserPoolUserToGroupAttachment + Properties: + GroupName: !Ref UserPoolAdminGroup + Username: !Ref AdminUsername + UserPoolId: !Ref UserPool + CustomizeCognitoUiLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-customize-cognito-ui + RetentionInDays: 30 + CustomizeCognitoUiExecRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub sb-${Environment}-customize-cognito-ui-role-${AWS::Region} + Path: '/' + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyName: !Sub sb-${Environment}-customize-cognito-ui-policy + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - logs:PutLogEvents + Resource: + - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* + - Effect: Allow + Action: + - logs:CreateLogStream + - logs:DescribeLogStreams + Resource: + - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* + - Effect: Allow + Action: + - s3:GetObject + Resource: !Sub arn:${AWS::Partition}:s3:::${SaaSBoostBucket}/* + - Effect: Allow + Action: + - cognito-idp:SetUICustomization + Resource: !Sub arn:${AWS::Partition}:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPool} + - Effect: Allow + Action: + - cognito-idp:DescribeUserPoolDomain + Resource: '*' + CustomizeCognitoUiLambda: + Type: AWS::Lambda::Function + DependsOn: CustomizeCognitoUiLogs + Properties: + FunctionName: !Sub sb-${Environment}-customize-cognito-ui + Role: !GetAtt CustomizeCognitoUiExecRole.Arn + Runtime: java21 + Architectures: + - arm64 + Timeout: 600 + MemorySize: 1024 + Handler: com.amazon.aws.partners.saasfactory.saasboost.CustomizeCognitoUi + Code: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/CustomizeCognitoUi-lambda.zip + Layers: + - !Ref SaaSBoostUtilsLayer + - !Ref CloudFormationUtilsLayer + Environment: + Variables: + SAAS_BOOST_ENV: !Ref Environment + ADMIN_WEB_SOURCE_KEY: client/web/src.zip + ADMIN_WEB_LOGO: client/web/public/saas-boost-login.png + ADMIN_WEB_BG_COLOR: rgb(50, 31, 219) + Tags: + - Key: Application + Value: SaaSBoost + - Key: Environment + Value: !Ref Environment + CustomizeCognitoUi: + Type: Custom::CustomResource + Properties: + ServiceToken: !GetAtt CustomizeCognitoUiLambda.Arn + UserPoolId: !Ref UserPool + UserPoolDomain: !Ref UserPoolDomain + SourceBucket: !Ref SaaSBoostBucket + CognitoAppClientDetailsLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-cognito-client-details + RetentionInDays: 30 + CognitoAppClientDetailsExecRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub sb-${Environment}-cognito-client-details-role-${AWS::Region} + Path: '/' + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyName: !Sub sb-${Environment}-cognito-client-details-policy + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - logs:PutLogEvents + Resource: + - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* + - Effect: Allow + Action: + - logs:CreateLogStream + - logs:DescribeLogStreams + Resource: + - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* + - Effect: Allow + Action: + - cognito-idp:DescribeUserPoolClient + Resource: + - !Sub arn:${AWS::Partition}:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPool} + CognitoAppClientDetailsLambda: + Type: AWS::Lambda::Function + DependsOn: CognitoAppClientDetailsLogs + Properties: + FunctionName: !Sub sb-${Environment}-cognito-client-details + Role: !GetAtt CognitoAppClientDetailsExecRole.Arn + Runtime: java21 + Architectures: + - arm64 + Timeout: 600 + MemorySize: 1024 + Handler: com.amazon.aws.partners.saasfactory.saasboost.CognitoAppClientDetails + Code: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/CognitoAppClientDetails-lambda.zip + Layers: + - !Ref SaaSBoostUtilsLayer + - !Ref CloudFormationUtilsLayer + Environment: + Variables: + SAAS_BOOST_ENV: !Ref Environment + Tags: + - Key: Application + Value: SaaSBoost + - Key: Environment + Value: !Ref Environment + InvokeApiAppClientDetails: + Type: Custom::CustomResource + Properties: + ServiceToken: !GetAtt CognitoAppClientDetailsLambda.Arn + UserPoolId: !Ref UserPool + ClientId: !Ref ApiAppClient + InvokePrivateApiAppClientDetails: + Type: Custom::CustomResource + Properties: + ServiceToken: !GetAtt CognitoAppClientDetailsLambda.Arn + UserPoolId: !Ref UserPool + ClientId: !Ref PrivateApiAppClient +Outputs: + UserPool: + Description: Cognito User Pool ID + Value: !Ref UserPool + # We need the authorization server domain from Cognito so we can make manual + # logout calls since Cognito does not support OIDC end_session_endpoint + CognitoDomain: + Description: The OIDC domain URL for this Cognito User Pool + Value: !Sub https://${UserPoolDomain}.auth.${AWS::Region}.amazoncognito.com + OidcIssuer: + Description: The OIDC issuer URL for this Cognito User Pool + Value: !Sub https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool} + OidcTokenEndpoint: + Description: The OIDC token endpoint for this Cognito user pool domain + Value: !Sub https://${UserPoolDomain}.auth.${AWS::Region}.amazoncognito.com/oauth2/token + AdminWebAppClientName: + Description: Public OAuth app client with PKCE for authorization code grant + Value: !Sub sb-${Environment}-admin-webapp-client + AdminWebAppClientId: + Description: Public OAuth app client id + Value: !Ref AdminWebAppClient + ApiAppClientName: + Description: Private OAuth app client for client credentials grant + Value: !GetAtt InvokeApiAppClientDetails.ClientName + ApiAppClientId: + Description: Private OAuth app client id + Value: !GetAtt InvokeApiAppClientDetails.ClientId + ApiAppClientSecret: + Description: Private OAuth app client secret + Value: !GetAtt InvokeApiAppClientDetails.ClientSecret + PrivateApiAppClientName: + Description: Private OAuth app client for client credentials grant for private API access + Value: !GetAtt InvokePrivateApiAppClientDetails.ClientName + PrivateApiAppClientId: + Description: Private OAuth app client id for private API access + Value: !GetAtt InvokePrivateApiAppClientDetails.ClientId + PrivateApiAppClientSecret: + Description: Private OAuth app client secret for private API access + Value: !GetAtt InvokePrivateApiAppClientDetails.ClientSecret +... \ No newline at end of file diff --git a/resources/saas-boost-keycloak.yaml b/resources/saas-boost-idp-keycloak.yaml similarity index 97% rename from resources/saas-boost-keycloak.yaml rename to resources/saas-boost-idp-keycloak.yaml index 620c234a..ec069199 100644 --- a/resources/saas-boost-keycloak.yaml +++ b/resources/saas-boost-idp-keycloak.yaml @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. AWSTemplateFormatVersion: 2010-09-09 -Description: AWS SaaS Boost Keycloak +Description: AWS SaaS Boost System User Keycloak IdP Parameters: Environment: Description: SaaS Boost environment name @@ -30,6 +30,9 @@ Parameters: CloudFormationUtilsLayer: Description: CloudFormation Utils Layer ARN Type: String + KeycloakHelperLayer: + Description: Keycloak Helper Layer ARN + Type: String CodePipelineBucket: Description: S3 bucket for CodePipeline artifacts Type: String @@ -794,20 +797,22 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-keycloak-setup Role: !GetAtt KeycloakSetupExecRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 600 MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.KeycloakSetup + Handler: com.amazon.aws.partners.saasfactory.saasboost.keycloak.KeycloakSetup Code: S3Bucket: !Ref SaaSBoostBucket S3Key: !Sub ${LambdaSourceFolder}/KeycloakSetup-lambda.zip Layers: - !Ref SaaSBoostUtilsLayer - !Ref CloudFormationUtilsLayer + - !Ref KeycloakHelperLayer Environment: Variables: SAAS_BOOST_ENV: !Ref Environment - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: "Application" Value: "SaaSBoost" @@ -831,21 +836,21 @@ Outputs: KeycloakRealm: Description: Configured realm for the Keycloak install Value: !GetAtt InvokeKeycloakSetup.KeycloakRealm - KeycloakIssuer: + KeycloakDatabaseEndpoint: + Description: Keycloak database hostname + Value: !GetAtt KeycloakDatabaseInstance.Endpoint.Address + OidcIssuer: Description: The OIDC issuer for this Keycloak install Value: !If - HasCustomDomain - !Sub https://${CustomDomainName}/realms/${InvokeKeycloakSetup.KeycloakRealm} - !Sub http://${KeycloakLoadBalancer.DNSName}/realms/sb-${Environment} - KeycloakTokenEndpoint: + OidcTokenEndpoint: Description: The OIDC token endpoint for this Keycloak install Value: !If - HasCustomDomain - !Sub https://${CustomDomainName}/realms/${InvokeKeycloakSetup.KeycloakRealm}/protocol/openid-connect/token - !Sub http://${KeycloakLoadBalancer.DNSName}/realms/sb-${Environment}/protocol/openid-connect/token - KeycloakDatabaseEndpoint: - Description: Keycloak database hostname - Value: !GetAtt KeycloakDatabaseInstance.Endpoint.Address AdminWebAppClientName: Description: Public OAuth app client with PKCE for authorization code grant Value: !GetAtt InvokeKeycloakSetup.AdminWebAppClientName @@ -861,4 +866,13 @@ Outputs: ApiAppClientSecret: Description: Private OAuth app client secret Value: !GetAtt InvokeKeycloakSetup.ApiAppClientSecret + PrivateApiAppClientName: + Description: Private OAuth app client for client credentials grant for private API access + Value: !GetAtt InvokeKeycloakSetup.PrivateApiAppClientName + PrivateApiAppClientId: + Description: Private OAuth app client id for private API access + Value: !GetAtt InvokeKeycloakSetup.PrivateApiAppClientId + PrivateApiAppClientSecret: + Description: Private OAuth app client secret for private API access + Value: !GetAtt InvokeKeycloakSetup.PrivateApiAppClientSecret ... \ No newline at end of file diff --git a/resources/saas-boost-idp.yaml b/resources/saas-boost-idp.yaml index 2deafc4c..c47a6cf1 100644 --- a/resources/saas-boost-idp.yaml +++ b/resources/saas-boost-idp.yaml @@ -30,6 +30,9 @@ Parameters: CloudFormationUtilsLayer: Description: CloudFormation Utils Layer ARN Type: String + KeycloakHelperLayer: + Description: Keycloak Helper Layer ARN + Type: String CodePipelineBucket: Description: S3 bucket for CodePipeline artifacts Type: String @@ -46,7 +49,7 @@ Parameters: Description: Identity Provider for the SaaS Boost system users and Control Plane API authorization Type: String Default: COGNITO - AllowedValues: [COGNITO, KEYCLOAK] + AllowedValues: [COGNITO, KEYCLOAK, AUTH0] AdminUsername: Description: SaaS Boost initial admin user name Type: String @@ -83,9 +86,14 @@ Parameters: ClearEcrRepoArn: Description: Lambda custom resource ARN to delete images before deleting ECR Repositories Type: String + AppPlaneAccountId: + Description: AWS Account hosting the "Application Plane" resources + Type: String + AllowedPattern: ^\d{12}$ Conditions: UseCognito: !Equals [!Ref IdentityProvider, 'COGNITO'] UseKeycloak: !Equals [!Ref IdentityProvider, 'KEYCLOAK'] + UseAuth0: !Equals [!Ref IdentityProvider, 'AUTH0'] Resources: SaaSBoostAdminCredentials: Type: AWS::SecretsManager::Secret @@ -97,314 +105,32 @@ Resources: PasswordLength: 12 GenerateStringKey: password SecretStringTemplate: !Sub '{"username": "${AdminUsername}", "email": "${AdminEmailAddress}"}' - UserPool: - Type: AWS::Cognito::UserPool - Condition: UseCognito - Properties: - UserPoolName: !Sub sb-${Environment}-system-users - MfaConfiguration: 'OFF' - Policies: - PasswordPolicy: - MinimumLength: 8 - RequireLowercase: true - RequireNumbers: true - RequireSymbols: false - RequireUppercase: true - TemporaryPasswordValidityDays: 7 - AdminCreateUserConfig: - AllowAdminCreateUserOnly: true - InviteMessageTemplate: - EmailMessage: !Sub | - Welcome to AWS SaaS Boost!
    -
    - You can login to your AWS SaaS Boost environment at ${AdminWebUrl}. -
    - Your username is: {username} -
    - Your temporary password is: {####} -
    - EmailSubject: !Sub AWS SaaS Boost temporary password for environment ${Environment} - UserPoolResourceServer: - Type: AWS::Cognito::UserPoolResourceServer - Condition: UseCognito - Properties: - UserPoolId: !Ref UserPool - Identifier: !Sub saas-boost/${Environment} - Name: !Sub sb-${Environment}-api - Scopes: - - ScopeName: read - ScopeDescription: Read Public API Access - - ScopeName: write - ScopeDescription: Write Public API Access - - ScopeName: private - ScopeDescription: Read/Write Private API Access - AdminWebAppClient: - Type: AWS::Cognito::UserPoolClient - Condition: UseCognito - Properties: - ClientName: !Sub sb-${Environment}-admin-webapp-client - UserPoolId: !Ref UserPool - SupportedIdentityProviders: - - COGNITO - ExplicitAuthFlows: - - ALLOW_ADMIN_USER_PASSWORD_AUTH - - ALLOW_USER_PASSWORD_AUTH - - ALLOW_REFRESH_TOKEN_AUTH - - ALLOW_USER_SRP_AUTH - GenerateSecret: false - AllowedOAuthFlowsUserPoolClient: true - AllowedOAuthFlows: - - code - AllowedOAuthScopes: - - openid - - email - - profile - CallbackURLs: - - !Ref AdminWebUrl - - http://localhost:3000 - LogoutURLs: - - !Ref AdminWebUrl - - http://localhost:3000 - ApiAppClient: - Type: AWS::Cognito::UserPoolClient - Condition: UseCognito - DependsOn: UserPoolResourceServer - Properties: - ClientName: !Sub sb-${Environment}-api-client - UserPoolId: !Ref UserPool - SupportedIdentityProviders: - - COGNITO - GenerateSecret: true - AccessTokenValidity: 5 - TokenValidityUnits: - AccessToken: minutes - AllowedOAuthFlowsUserPoolClient: true - AllowedOAuthFlows: - - client_credentials - AllowedOAuthScopes: - - !Sub saas-boost/${Environment}/read - - !Sub saas-boost/${Environment}/write - PrivateApiAppClient: - Type: AWS::Cognito::UserPoolClient - Condition: UseCognito - DependsOn: UserPoolResourceServer - Properties: - ClientName: !Sub sb-${Environment}-private-api-client - UserPoolId: !Ref UserPool - SupportedIdentityProviders: - - COGNITO - GenerateSecret: true - AccessTokenValidity: 5 - TokenValidityUnits: - AccessToken: minutes - AllowedOAuthFlowsUserPoolClient: true - AllowedOAuthFlows: - - client_credentials - AllowedOAuthScopes: - - !Sub saas-boost/${Environment}/read - - !Sub saas-boost/${Environment}/write - - !Sub saas-boost/${Environment}/private - UserPoolDomain: - Type: AWS::Cognito::UserPoolDomain - Condition: UseCognito - Properties: - Domain: !Sub - - sb-${Environment}-saas-boost-${RandomString} - - RandomString: !Select [2, !Split ['/', !Ref AWS::StackId]] - UserPoolId: !Ref UserPool - UserPoolAdminUser: - Type: AWS::Cognito::UserPoolUser - Condition: UseCognito - Properties: - DesiredDeliveryMediums: - - EMAIL - ForceAliasCreation: false - UserAttributes: - - Name: email - Value: !Ref AdminEmailAddress - - Name: email_verified - Value: 'true' - Username: !Ref AdminUsername - UserPoolId: !Ref UserPool - CustomizeCognitoUiLogs: - Type: AWS::Logs::LogGroup - Condition: UseCognito - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-customize-cognito-ui - RetentionInDays: 30 - CustomizeCognitoUiExecRole: - Type: AWS::IAM::Role - Condition: UseCognito - Properties: - RoleName: !Sub sb-${Environment}-customize-cognito-ui-role-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: !Sub sb-${Environment}-customize-cognito-ui-policy - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:CreateLogStream - - logs:DescribeLogStreams - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - s3:GetObject - Resource: !Sub arn:${AWS::Partition}:s3:::${SaaSBoostBucket}/* - - Effect: Allow - Action: - - cognito-idp:SetUICustomization - Resource: !Sub arn:${AWS::Partition}:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPool} - - Effect: Allow - Action: - - cognito-idp:DescribeUserPoolDomain - Resource: '*' - CustomizeCognitoUiLambda: - Type: AWS::Lambda::Function - Condition: UseCognito - DependsOn: CustomizeCognitoUiLogs - Properties: - FunctionName: !Sub sb-${Environment}-customize-cognito-ui - Role: !GetAtt CustomizeCognitoUiExecRole.Arn - Runtime: java11 - Timeout: 600 - MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.CustomizeCognitoUi - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/CustomizeCognitoUi-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref CloudFormationUtilsLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - ADMIN_WEB_SOURCE_KEY: client/web/src.zip - ADMIN_WEB_LOGO: client/web/public/saas-boost-login.png - ADMIN_WEB_BG_COLOR: rgb(50, 31, 219) - Tags: - - Key: Application - Value: SaaSBoost - - Key: Environment - Value: !Ref Environment - CustomizeCognitoUi: - Type: Custom::CustomResource - Condition: UseCognito - Properties: - ServiceToken: !GetAtt CustomizeCognitoUiLambda.Arn - UserPoolId: !Ref UserPool - UserPoolDomain: !Ref UserPoolDomain - SourceBucket: !Ref SaaSBoostBucket - CognitoAppClientDetailsLogs: - Type: AWS::Logs::LogGroup - Condition: UseCognito - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-cognito-client-details - RetentionInDays: 30 - CognitoAppClientDetailsExecRole: - Type: AWS::IAM::Role - Condition: UseCognito - Properties: - RoleName: !Sub sb-${Environment}-cognito-client-details-role-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: !Sub sb-${Environment}-cognito-client-details-policy - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:CreateLogStream - - logs:DescribeLogStreams - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - cognito-idp:DescribeUserPoolClient - Resource: - - !Sub arn:${AWS::Partition}:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPool} - CognitoAppClientDetailsLambda: - Type: AWS::Lambda::Function - Condition: UseCognito - DependsOn: CognitoAppClientDetailsLogs - Properties: - FunctionName: !Sub sb-${Environment}-cognito-client-details - Role: !GetAtt CognitoAppClientDetailsExecRole.Arn - Runtime: java11 - Timeout: 600 - MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.CognitoAppClientDetails - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/CognitoAppClientDetails-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref CloudFormationUtilsLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: Application - Value: SaaSBoost - - Key: Environment - Value: !Ref Environment - InvokeApiAppClientDetails: - Type: Custom::CustomResource - Condition: UseCognito - Properties: - ServiceToken: !GetAtt CognitoAppClientDetailsLambda.Arn - UserPoolId: !Ref UserPool - ClientId: !Ref ApiAppClient - InvokePrivateApiAppClientDetails: - Type: Custom::CustomResource + cognito: + Type: AWS::CloudFormation::Stack Condition: UseCognito Properties: - ServiceToken: !GetAtt CognitoAppClientDetailsLambda.Arn - UserPoolId: !Ref UserPool - ClientId: !Ref PrivateApiAppClient + TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-idp-cognito.yaml + Parameters: + Environment: !Ref Environment + SaaSBoostBucket: !Ref SaaSBoostBucket + LambdaSourceFolder: !Ref LambdaSourceFolder + SaaSBoostUtilsLayer: !Ref SaaSBoostUtilsLayer + CloudFormationUtilsLayer: !Ref CloudFormationUtilsLayer + AdminUsername: !Ref AdminUsername + AdminEmailAddress: !Ref AdminEmailAddress + AdminWebUrl: !Ref AdminWebUrl keycloak: Type: AWS::CloudFormation::Stack Condition: UseKeycloak Properties: - TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-keycloak.yaml + TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-idp-keycloak.yaml Parameters: Environment: !Ref Environment SaaSBoostBucket: !Ref SaaSBoostBucket LambdaSourceFolder: !Ref LambdaSourceFolder SaaSBoostUtilsLayer: !Ref SaaSBoostUtilsLayer CloudFormationUtilsLayer: !Ref CloudFormationUtilsLayer + KeycloakHelperLayer: !Ref KeycloakHelperLayer CodePipelineBucket: !Ref CodePipelineBucket CodePipelineUpdateEcsService: !Ref CodePipelineUpdateEcsService StartCodeBuildLambda: !Ref StartCodeBuildLambda @@ -418,49 +144,173 @@ Resources: PublicSubnets: !Ref PublicSubnets PrivateSubnets: !Ref PrivateSubnets ClearEcrRepoArn: !Ref ClearEcrRepoArn + auth0: + Type: AWS::CloudFormation::Stack + Condition: UseAuth0 + Properties: + TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-idp-auth0.yaml + Parameters: + Environment: !Ref Environment + SaaSBoostBucket: !Ref SaaSBoostBucket + LambdaSourceFolder: !Ref LambdaSourceFolder + SaaSBoostUtilsLayer: !Ref SaaSBoostUtilsLayer + CloudFormationUtilsLayer: !Ref CloudFormationUtilsLayer + AdminCredentials: !Ref SaaSBoostAdminCredentials + AdminWebUrl: !Ref AdminWebUrl AdminWebAppClientName: Type: AWS::SSM::Parameter Properties: Name: !Sub /saas-boost/${Environment}/ADMIN_WEB_APP_CLIENT Type: String - Value: !If [UseCognito, !Ref AdminWebAppClient, !GetAtt keycloak.Outputs.AdminWebAppClientName] + Value: + Fn::If: + - UseCognito + - !GetAtt cognito.Outputs.AdminWebAppClientName + - !If + - UseKeycloak + - !GetAtt keycloak.Outputs.AdminWebAppClientName + - !GetAtt auth0.Outputs.AdminWebAppClientName AdminWebAppClientId: Type: AWS::SSM::Parameter Properties: Name: !Sub /saas-boost/${Environment}/ADMIN_WEB_APP_CLIENT_ID Type: String - Value: !If [UseCognito, !Ref AdminWebAppClient, !GetAtt keycloak.Outputs.AdminWebAppClientId] + Value: + Fn::If: + - UseCognito + - !GetAtt cognito.Outputs.AdminWebAppClientId + - !If + - UseKeycloak + - !GetAtt keycloak.Outputs.AdminWebAppClientId + - !GetAtt auth0.Outputs.AdminWebAppClientId + EncryptionKey: + Type: AWS::KMS::Key + Properties: + KeyPolicy: + Version: 2012-10-17 + Id: !Sub sb-${Environment}-api-app-client + Statement: + - Effect: Allow + Principal: + AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root + Action: kms:* + Resource: '*' + - Effect: Allow + Principal: + AWS: !Sub arn:${AWS::Partition}:iam::${AppPlaneAccountId}:root + Action: + - kms:Encrypt + - kms:Decrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + - kms:CreateGrant + - kms:ListGrants + - kms:DescribeKey + Resource: '*' + Condition: + StringEquals: + kms:ViaService: !Sub secretsmanager.${AWS::Region}.amazonaws.com + EncryptionKeyAlias: + Type: AWS::KMS::Alias + Properties: + AliasName: !Sub alias/sb-${Environment}-api-app-client + TargetKeyId: !Ref EncryptionKey ApiAppClientSecret: Type: AWS::SecretsManager::Secret Properties: Name: !Sub /saas-boost/${Environment}/API_APP_CLIENT - SecretString: !If - - UseCognito - - !Sub '{"client_name": "${InvokeApiAppClientDetails.ClientName}", "client_id": "${InvokeApiAppClientDetails.ClientId}", "client_secret": "${InvokeApiAppClientDetails.ClientSecret}", "token_endpoint": "https://${UserPoolDomain}.auth.${AWS::Region}.amazoncognito.com/oauth2/token", "api_endpoint": "${ApiGatewayUrl}"}' - - !Sub '{"client_name": "${keycloak.Outputs.ApiAppClientName}", "client_id": "${keycloak.Outputs.ApiAppClientId}", "client_secret": "${keycloak.Outputs.ApiAppClientSecret}", "token_endpoint": "${keycloak.Outputs.KeycloakTokenEndpoint}", "api_endpoint": "${ApiGatewayUrl}"}' + KmsKeyId: !Ref EncryptionKeyAlias + SecretString: + Fn::If: + - UseCognito + - !Sub '{"client_name": "${cognito.Outputs.ApiAppClientName}", "client_id": "${cognito.Outputs.ApiAppClientId}", "client_secret": "${cognito.Outputs.ApiAppClientSecret}", "token_endpoint": "${cognito.Outputs.OidcTokenEndpoint}", "api_endpoint": "${ApiGatewayUrl}"}' + - !If + - UseKeycloak + - !Sub '{"client_name": "${keycloak.Outputs.ApiAppClientName}", "client_id": "${keycloak.Outputs.ApiAppClientId}", "client_secret": "${keycloak.Outputs.ApiAppClientSecret}", "token_endpoint": "${keycloak.Outputs.OidcTokenEndpoint}", "api_endpoint": "${ApiGatewayUrl}"}' + - !Sub '{"client_name": "${auth0.Outputs.ApiAppClientName}", "client_id": "${auth0.Outputs.ApiAppClientId}", "client_secret": "${auth0.Outputs.ApiAppClientSecret}", "token_endpoint": "${auth0.Outputs.OidcTokenEndpoint}", "api_endpoint": "${ApiGatewayUrl}"}' + # Allow the Application Plane account to get the API app client + # details from Secrets Manager + ApiAppClientSecretPolicy: + Type: AWS::SecretsManager::ResourcePolicy + Properties: + SecretId: !Ref ApiAppClientSecret + ResourcePolicy: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + AWS: !Sub arn:${AWS::Partition}:iam::${AppPlaneAccountId}:root + Action: + - secretsmanager:GetSecretValue + Resource: + - '*' + PrivateApiAppClientSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Sub /saas-boost/${Environment}/PRIVATE_API_APP_CLIENT + SecretString: + Fn::If: + - UseCognito + - !Sub '{"client_name": "${cognito.Outputs.PrivateApiAppClientName}", "client_id": "${cognito.Outputs.PrivateApiAppClientId}", "client_secret": "${cognito.Outputs.PrivateApiAppClientSecret}", "token_endpoint": "${cognito.Outputs.OidcTokenEndpoint}", "api_endpoint": "${ApiGatewayUrl}"}' + - !If + - UseKeycloak + - !Sub '{"client_name": "${keycloak.Outputs.PrivateApiAppClientName}", "client_id": "${keycloak.Outputs.PrivateApiAppClientId}", "client_secret": "${keycloak.Outputs.PrivateApiAppClientSecret}", "token_endpoint": "${keycloak.Outputs.OidcTokenEndpoint}", "api_endpoint": "${ApiGatewayUrl}"}' + - !Sub '{"client_name": "${auth0.Outputs.PrivateApiAppClientName}", "client_id": "${auth0.Outputs.PrivateApiAppClientId}", "client_secret": "${auth0.Outputs.PrivateApiAppClientSecret}", "token_endpoint": "${auth0.Outputs.OidcTokenEndpoint}", "api_endpoint": "${ApiGatewayUrl}"}' Outputs: + # These outputs are used by the admin web app stack OidcIssuerUrl: Description: OIDC issuer for System User IdP - Value: !If - - UseCognito - - !Sub https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool} - - !GetAtt keycloak.Outputs.KeycloakIssuer - # OidcDomainUrl is explicitly different for Cognito and is needed for manual - # logout calls since Cognito does not support OIDC end_session_endpoint + Value: + Fn::If: + - UseCognito + - !GetAtt cognito.Outputs.OidcIssuer + - !If + - UseKeycloak + - !GetAtt keycloak.Outputs.OidcIssuer + - !GetAtt auth0.Outputs.OidcIssuer OidcDomainUrl: - Description: Domain System User IdP sits behind - Value: !If - - UseCognito - - !Sub 'https://${UserPoolDomain}.auth.${AWS::Region}.amazoncognito.com' - - '' + Description: The OIDC authorization server URL + Value: + Fn::If: + - UseCognito + - !GetAtt cognito.Outputs.CognitoDomain + - '' # Can be blank for any IdP that supports OIDC end_session_endpoint AdminWebAppClient: - Description: Admin Web App Public App Client for authorization code grants with PKCE - Value: !If [UseCognito, !Ref AdminWebAppClient, !GetAtt keycloak.Outputs.AdminWebAppClientId] - # The following outputs are used conditionally by the system user service + Description: Admin Web App Public App Client Id + Value: + Fn::If: + - UseCognito + - !GetAtt cognito.Outputs.AdminWebAppClientId + - !If + - UseKeycloak + - !GetAtt keycloak.Outputs.AdminWebAppClientId + - !GetAtt auth0.Outputs.AdminWebAppClientId + # These outputs are used by the API Gateway Authorizer + ApiAppClient: + Description: Private OAuth app client id for API access + Value: + Fn::If: + - UseCognito + - !GetAtt cognito.Outputs.ApiAppClientId + - !If + - UseKeycloak + - !GetAtt keycloak.Outputs.ApiAppClientId + - !GetAtt auth0.Outputs.ApiAppClientId + PrivateApiAppClient: + Description: Private OAuth app client id for private API access + Value: + Fn::If: + - UseCognito + - !GetAtt cognito.Outputs.PrivateApiAppClientId + - !If + - UseKeycloak + - !GetAtt keycloak.Outputs.PrivateApiAppClientId + - !GetAtt auth0.Outputs.PrivateApiAppClientId + # These outputs are used conditionally by the system user service CognitoUserPool: Condition: UseCognito Description: Cognito User Pool ID - Value: !Ref UserPool + Value: !GetAtt cognito.Outputs.UserPool KeycloakHost: Condition: UseKeycloak Description: Keycloak database hostname @@ -473,4 +323,11 @@ Outputs: Condition: UseKeycloak Description: Keycloak database hostname Value: !GetAtt keycloak.Outputs.KeycloakDatabaseEndpoint + # This is used for Application Plane integration + ApiAppClientEncryptionKey: + Description: KMS key used for application plane cross-account access to the API App Client details + Value: !GetAtt EncryptionKey.Arn + ApiAppClientSecret: + Description: Secrets Manager secret with API App Client details + Value: !Ref ApiAppClientSecret ... \ No newline at end of file diff --git a/resources/saas-boost-network.yaml b/resources/saas-boost-network.yaml index b714661e..4633779b 100644 --- a/resources/saas-boost-network.yaml +++ b/resources/saas-boost-network.yaml @@ -19,19 +19,7 @@ Parameters: Description: SaaS Boost "environment" such as test, prod, beta, etc... Type: String Resources: - # VPC and Transit Gateway for egress from Tenant VPCs - TransitGateway: - Type: AWS::EC2::TransitGateway - Properties: - AutoAcceptSharedAttachments: enable - DefaultRouteTableAssociation: disable - DefaultRouteTablePropagation: disable - Description: SaaS Boost Egress VPC Transit Gateway - Tags: - - Key: Name - Value: !Sub sb-${Environment}-tgw - # VPC for egress that will be attached to TGW - EgressVPC: + VPC: Type: AWS::EC2::VPC Properties: EnableDnsHostnames: true @@ -39,51 +27,51 @@ Resources: CidrBlock: 192.168.0.0/16 Tags: - Key: Name - Value: !Sub sb-${Environment}-egress-vpc - EgressVpcPublicSubnet1: + Value: !Sub sb-${Environment}-vpc + PublicSubnet1: Type: AWS::EC2::Subnet Properties: - VpcId: !Ref EgressVPC - CidrBlock: 192.168.1.0/24 + VpcId: !Ref VPC + CidrBlock: !Select [0, !Cidr [!GetAtt VPC.CidrBlock, 4, 8]] AvailabilityZone: !Select [0, !GetAZs ''] Tags: - Key: Application Value: !Ref AWS::StackId - Key: Name - Value: !Sub sb-${Environment}-egress-public-az1 - EgressVpcPublicSubnet2: + Value: !Sub sb-${Environment}-public-az1 + PublicSubnet2: Type: AWS::EC2::Subnet Properties: - VpcId: !Ref EgressVPC - CidrBlock: 192.168.2.0/24 + VpcId: !Ref VPC + CidrBlock: !Select [1, !Cidr [!GetAtt VPC.CidrBlock, 4, 8]] AvailabilityZone: !Select [1, !GetAZs ''] Tags: - Key: Application Value: !Ref AWS::StackId - Key: Name - Value: !Sub sb-${Environment}-egress-public-az2 - PrivateEgressSubnet1: + Value: !Sub sb-${Environment}-public-az2 + PrivateSubnet1: Type: AWS::EC2::Subnet Properties: - VpcId: !Ref EgressVPC - CidrBlock: 192.168.3.0/24 - AvailabilityZone: !Select [0, !GetAZs ''] + VpcId: !Ref VPC + CidrBlock: !Select [2, !Cidr [!GetAtt VPC.CidrBlock, 4, 8]] + AvailabilityZone: !Select [0, !GetAZs ''] Tags: - Key: Application Value: !Ref AWS::StackId - Key: Name - Value: !Sub sb-${Environment}-egress-private-az1 - PrivateEgressSubnet2: + Value: !Sub sb-${Environment}-private-az1 + PrivateSubnet2: Type: AWS::EC2::Subnet Properties: - VpcId: !Ref EgressVPC - CidrBlock: 192.168.4.0/24 + VpcId: !Ref VPC + CidrBlock: !Select [3, !Cidr [!GetAtt VPC.CidrBlock, 4, 8]] AvailabilityZone: !Select [1, !GetAZs ''] Tags: - Key: Application Value: !Ref AWS::StackId - Key: Name - Value: !Sub sb-${Environment}-egress-private-az2 + Value: !Sub sb-${Environment}-private-az2 InternetGateway: Type: AWS::EC2::InternetGateway Properties: @@ -93,7 +81,7 @@ Resources: AttachIGW: Type: AWS::EC2::VPCGatewayAttachment Properties: - VpcId: !Ref EgressVPC + VpcId: !Ref VPC InternetGatewayId: !Ref InternetGateway IPAddress1: Type: AWS::EC2::EIP @@ -102,7 +90,7 @@ Resources: Domain: vpc Tags: - Key: Name - Value: !Sub sb-${Environment}-egress-nat-ip1 + Value: !Sub sb-${Environment}-nat-ip1 IPAddress2: Type: AWS::EC2::EIP DependsOn: AttachIGW @@ -110,198 +98,99 @@ Resources: Domain: vpc Tags: - Key: Name - Value: !Sub sb-${Environment}-egress-nat-ip2 + Value: !Sub sb-${Environment}-nat-ip2 NATGateway1: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt IPAddress1.AllocationId - SubnetId: !Ref EgressVpcPublicSubnet1 + SubnetId: !Ref PublicSubnet1 Tags: - - Key: Application - Value: !Ref AWS::StackId + - Key: Name + Value: !Sub sb-${Environment}-natgw-1 NATGateway2: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt IPAddress2.AllocationId - SubnetId: !Ref EgressVpcPublicSubnet2 + SubnetId: !Ref PublicSubnet2 Tags: - - Key: Application - Value: !Ref AWS::StackId - EgressRouteTable: + - Key: Name + Value: !Sub sb-${Environment}-natgw-2 + PublicRouteTable: Type: AWS::EC2::RouteTable Properties: - VpcId: !Ref EgressVPC + VpcId: !Ref VPC Tags: - - Key: Application - Value: !Ref AWS::StackId - Key: Name - Value: !Sub sb-${Environment}-egress-public-rt - PrivateEgressRouteTable1: + Value: !Sub sb-${Environment}-public-rt + PrivateRouteTable1: Type: AWS::EC2::RouteTable Properties: - VpcId: !Ref EgressVPC + VpcId: !Ref VPC Tags: - - Key: Application - Value: !Ref AWS::StackId - Key: Name - Value: !Sub sb-${Environment}-egress-private-rt-az1 - PrivateEgressRouteTable2: + Value: !Sub sb-${Environment}-private-rt-az1 + PrivateRouteTable2: Type: AWS::EC2::RouteTable Properties: - VpcId: !Ref EgressVPC + VpcId: !Ref VPC Tags: - - Key: Application - Value: !Ref AWS::StackId - Key: Name - Value: !Sub sb-${Environment}-egress-private-rt-az2 + Value: !Sub sb-${Environment}-private-rt-az2 PublicRoute: Type: AWS::EC2::Route DependsOn: AttachIGW Properties: - RouteTableId: !Ref EgressRouteTable + RouteTableId: !Ref PublicRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway SubnetRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: - SubnetId: !Ref EgressVpcPublicSubnet1 - RouteTableId: !Ref EgressRouteTable + SubnetId: !Ref PublicSubnet1 + RouteTableId: !Ref PublicRouteTable SubnetRouteTableAssociation2: Type: AWS::EC2::SubnetRouteTableAssociation Properties: - SubnetId: !Ref EgressVpcPublicSubnet2 - RouteTableId: !Ref EgressRouteTable - PrivateEgressRoute1: + SubnetId: !Ref PublicSubnet2 + RouteTableId: !Ref PublicRouteTable + PrivateRoute1: Type: AWS::EC2::Route DependsOn: AttachIGW Properties: - RouteTableId: !Ref PrivateEgressRouteTable1 + RouteTableId: !Ref PrivateRouteTable1 DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NATGateway1 - PrivateEgressRoute2: + PrivateRoute2: Type: AWS::EC2::Route DependsOn: AttachIGW Properties: - RouteTableId: !Ref PrivateEgressRouteTable2 + RouteTableId: !Ref PrivateRouteTable2 DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NATGateway2 - PrivateEgressRouteTable1Association: + PrivateRouteTable1Association: Type: AWS::EC2::SubnetRouteTableAssociation Properties: - SubnetId: !Ref PrivateEgressSubnet1 - RouteTableId: !Ref PrivateEgressRouteTable1 - PrivateEgressRouteTable2Association: + SubnetId: !Ref PrivateSubnet1 + RouteTableId: !Ref PrivateRouteTable1 + PrivateRouteTable2Association: Type: AWS::EC2::SubnetRouteTableAssociation Properties: - SubnetId: !Ref PrivateEgressSubnet2 - RouteTableId: !Ref PrivateEgressRouteTable2 - # Attach Egress subnets to TGW - EgressVpcAttachment: - Type: AWS::EC2::TransitGatewayAttachment - Properties: - SubnetIds: - - !Ref PrivateEgressSubnet1 - - !Ref PrivateEgressSubnet2 - Tags: - - Key: Name - Value: !Sub sb-${Environment}-egress-attachment - TransitGatewayId: !Ref TransitGateway - VpcId: !Ref EgressVPC - EgressTransitGatewayRouteTable: - Type: AWS::EC2::TransitGatewayRouteTable - Properties: - Tags: - - Key: Name - Value: !Sub sb-${Environment}-egress-routetbl - TransitGatewayId: !Ref TransitGateway - TenantTransitGatewayRouteTable: - Type: AWS::EC2::TransitGatewayRouteTable - Properties: - Tags: - - Key: Name - Value: !Sub sb-${Environment}-tenant-routetbl - TransitGatewayId: !Ref TransitGateway - # Add a default route and black hole to the app route table - AppDefaultTGWRoute: - Type: AWS::EC2::TransitGatewayRoute - Properties: - DestinationCidrBlock: 0.0.0.0/0 - TransitGatewayAttachmentId: !Ref EgressVpcAttachment - TransitGatewayRouteTableId: !Ref TenantTransitGatewayRouteTable - # Black hole to prevent traffic from tenant vpc to antoher tenant vpc - AppBlackhole10Route: - Type: AWS::EC2::TransitGatewayRoute - Properties: - Blackhole: Yes - DestinationCidrBlock: 10.0.0.0/8 - TransitGatewayRouteTableId: !Ref TenantTransitGatewayRouteTable - AppBlackhole172Route: - Type: AWS::EC2::TransitGatewayRoute - Properties: - Blackhole: Yes - DestinationCidrBlock: 172.16.0.0/12 - TransitGatewayRouteTableId: !Ref TenantTransitGatewayRouteTable - AppBlackhole192Route: - Type: AWS::EC2::TransitGatewayRoute - Properties: - Blackhole: Yes - DestinationCidrBlock: 192.168.0.0/16 - TransitGatewayRouteTableId: !Ref TenantTransitGatewayRouteTable - EgressVpcTgwAssociation: - Type: AWS::EC2::TransitGatewayRouteTableAssociation - Properties: - TransitGatewayAttachmentId: !Ref EgressVpcAttachment - TransitGatewayRouteTableId: !Ref EgressTransitGatewayRouteTable - # Update VPC route tables to point towards transit gateway for appropriate target CIDR ranges - UpdateEgressRouteTable: - Type: AWS::EC2::Route - DependsOn: EgressVpcAttachment - Properties: - RouteTableId: !Ref EgressRouteTable - # This is to route to Tenant VPCs through egress TGW - DestinationCidrBlock: 10.0.0.0/8 - TransitGatewayId: !Ref TransitGateway - SSMParamTransitGateway: - Type: AWS::SSM::Parameter - Properties: - Name: !Sub /saas-boost/${Environment}/TRANSIT_GATEWAY - Type: String - Value: !Ref TransitGateway - SSMParamTransitGatewayRouteTable: - Type: AWS::SSM::Parameter - Properties: - Name: !Sub /saas-boost/${Environment}/TRANSIT_GATEWAY_ROUTE_TABLE - Type: String - Value: !Ref TenantTransitGatewayRouteTable - SSMParamEgressRouteTable: - Type: AWS::SSM::Parameter - Properties: - Name: !Sub /saas-boost/${Environment}/EGRESS_ROUTE_TABLE - Type: String - Value: !Ref EgressTransitGatewayRouteTable + SubnetId: !Ref PrivateSubnet2 + RouteTableId: !Ref PrivateRouteTable2 Outputs: - EgressVpc: - Description: Egress VPC Id - Value: !Ref EgressVPC - TransitGateway: - Description: Transit Gateway for Egress to Public Internet - Value: !Ref TransitGateway - TenantTransitGatewayRouteTable: - Description: Transit Gateway Route table for tenant - Value: !Ref TenantTransitGatewayRouteTable - EgressTransitGatewayRouteTable: - Description: Transit Gateway Route table for egress - Value: !Ref EgressTransitGatewayRouteTable + Vpc: + Description: VPC Id + Value: !Ref VPC PublicSubnet1: Description: Public Subnet AZ 1 - Value: !Ref EgressVpcPublicSubnet1 + Value: !Ref PublicSubnet1 PublicSubnet2: Description: Public Subnet AZ 2 - Value: !Ref EgressVpcPublicSubnet2 + Value: !Ref PublicSubnet2 PrivateSubnet1: Description: Private Subnet AZ 1 - Value: !Ref PrivateEgressSubnet1 + Value: !Ref PrivateSubnet1 PrivateSubnet2: Description: Private Subnet AZ 2 - Value: !Ref PrivateEgressSubnet2 + Value: !Ref PrivateSubnet2 ... \ No newline at end of file diff --git a/resources/saas-boost-private-api.yaml b/resources/saas-boost-private-api.yaml deleted file mode 100644 index add84d49..00000000 --- a/resources/saas-boost-private-api.yaml +++ /dev/null @@ -1,559 +0,0 @@ ---- -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -AWSTemplateFormatVersion: 2010-09-09 -Description: AWS SaaS Boost Private API -Parameters: - Environment: - Description: Environment name - Type: String - PrivateApi: - Description: API Gateway REST API - Type: String - RootResourceId: - Description: API Gateway REST API root resource id - Type: String - PrivateApiStage: - Description: The API Gateway REST API stage name for the SaaS Boost private API - Type: String - Default: v1 - QuotasServiceCheck: - Description: Quota Service check limits Lambda ARN - Type: String - TenantServiceInsert: - Description: Tenant Service insert new tenant Lambda ARN - Type: String - TenantServiceById: - Description: Tenant Service get tenant by id Lambda ARN - Type: String - TenantServiceGetAll: - Description: Tenant Service get all tenants Lambda ARN - Type: String - TenantServiceDelete: - Description: Tenant Service delete tenant Lambda ARN - Type: String - SettingsServiceGetAll: - Description: Settings Service get all settings Lambda ARN - Type: String - SettingsServiceGetSecret: - Description: Settings Service get decrypted secret setting Lambda ARN - Type: String - SettingsServiceDeleteAppConfig: - Description: Settings Service delete application configuration Lambda ARN - Type: String - SettingsServiceGetAppConfig: - Description: Settings Service get application configuration Lambda ARN - Type: String -Resources: - ApiGatewayLoggingRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub sb-${Environment}-priv-api-log-role-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - apigateway.amazonaws.com - Action: - - sts:AssumeRole - ManagedPolicyArns: - - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs - ApiGatewayLoggingAccount: - Type: AWS::ApiGateway::Account - Properties: - CloudWatchRoleArn: !GetAtt ApiGatewayLoggingRole.Arn - QuotaServiceResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PrivateApi - ParentId: !Ref RootResourceId - PathPart: 'quotas' - QuotaServiceCheckResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PrivateApi - ParentId: !Ref QuotaServiceResource - PathPart: 'check' - QuotaServiceCheckMethod: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PrivateApi - ResourceId: !Ref QuotaServiceCheckResource - HttpMethod: GET - AuthorizationType: AWS_IAM - Integration: - Type: AWS_PROXY - IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${QuotasServiceCheck}/invocations - PassthroughBehavior: WHEN_NO_MATCH - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Origin: false - QuotasServiceLambdaPermission: - Type: AWS::Lambda::Permission - Properties: - Principal: apigateway.amazonaws.com - Action: lambda:InvokeFunction - FunctionName: !Ref QuotasServiceCheck - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PrivateApi}/*/GET/quotas/check - QuotaServiceCheckResourceCORS: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PrivateApi - ResourceId: !Ref QuotaServiceCheckResource - HttpMethod: OPTIONS - AuthorizationType: NONE - Integration: - Type: MOCK - PassthroughBehavior: WHEN_NO_MATCH - IntegrationResponses: - - StatusCode: '200' - ResponseTemplates: {application/json: ''} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'" - method.response.header.Access-Control-Allow-Origin: "'*'" - method.response.header.Access-Control-Max-Age: "'3600'" - method.response.header.X-Requested-With: "'*'" - RequestTemplates: - application/json: '{"statusCode": 200}' - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: false - method.response.header.Access-Control-Allow-Methods: false - method.response.header.Access-Control-Allow-Origin: false - method.response.header.Access-Control-Max-Age: false - method.response.header.X-Requested-With: false - TenantServiceResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PrivateApi - ParentId: !Ref RootResourceId - PathPart: 'tenants' - TenantServiceByIdResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PrivateApi - ParentId: !Ref TenantServiceResource - PathPart: '{id}' - TenantServiceGetByIdMethod: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PrivateApi - ResourceId: !Ref TenantServiceByIdResource - HttpMethod: GET - AuthorizationType: AWS_IAM - RequestParameters: {method.request.path.id: true} - Integration: - Type: AWS_PROXY - IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${TenantServiceById}/invocations - PassthroughBehavior: WHEN_NO_MATCH - RequestParameters: {integration.request.path.id: 'method.request.path.id'} - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Origin: false - TenantServiceByIdLambdaPermission: - Type: AWS::Lambda::Permission - Properties: - Principal: apigateway.amazonaws.com - Action: lambda:InvokeFunction - FunctionName: !Ref TenantServiceById - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PrivateApi}/*/GET/tenants/{id} - TenantServiceGetAllMethod: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PrivateApi - ResourceId: !Ref TenantServiceResource - HttpMethod: GET - AuthorizationType: AWS_IAM - Integration: - Type: AWS_PROXY - IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${TenantServiceGetAll}/invocations - PassthroughBehavior: WHEN_NO_MATCH - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Origin: false - TenantServiceGetAllLambdaPermission: - Type: AWS::Lambda::Permission - Properties: - Principal: apigateway.amazonaws.com - Action: lambda:InvokeFunction - FunctionName: !Ref TenantServiceGetAll - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PrivateApi}/*/GET/tenants - TenantServiceDeleteMethod: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PrivateApi - ResourceId: !Ref TenantServiceByIdResource - HttpMethod: DELETE - AuthorizationType: AWS_IAM - RequestParameters: {method.request.path.id: true} - Integration: - Type: AWS_PROXY - IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${TenantServiceDelete}/invocations - PassthroughBehavior: WHEN_NO_MATCH - RequestParameters: {integration.request.path.id: 'method.request.path.id'} - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Origin: false - TenantServiceDeleteLambdaPermission: - Type: AWS::Lambda::Permission - Properties: - Principal: apigateway.amazonaws.com - Action: lambda:InvokeFunction - FunctionName: !Ref TenantServiceDelete - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PrivateApi}/*/DELETE/tenants/{id} - TenantServiceInsertMethod: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PrivateApi - ResourceId: !Ref TenantServiceResource - HttpMethod: POST - AuthorizationType: AWS_IAM - Integration: - Type: AWS_PROXY - IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${TenantServiceInsert}/invocations - PassthroughBehavior: WHEN_NO_MATCH - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Origin: false - TenantServiceInsertLambdaPermission: - Type: AWS::Lambda::Permission - Properties: - Principal: apigateway.amazonaws.com - Action: lambda:InvokeFunction - FunctionName: !Ref TenantServiceInsert - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PrivateApi}/*/POST/tenants - TenantServiceResourceCORS: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PrivateApi - ResourceId: !Ref TenantServiceResource - HttpMethod: OPTIONS - AuthorizationType: NONE - Integration: - Type: MOCK - PassthroughBehavior: WHEN_NO_MATCH - IntegrationResponses: - - StatusCode: '200' - ResponseTemplates: {application/json: ''} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS,POST'" - method.response.header.Access-Control-Allow-Origin: "'*'" - method.response.header.Access-Control-Max-Age: "'3600'" - method.response.header.X-Requested-With: "'*'" - RequestTemplates: - application/json: '{"statusCode": 200}' - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: false - method.response.header.Access-Control-Allow-Methods: false - method.response.header.Access-Control-Allow-Origin: false - method.response.header.Access-Control-Max-Age: false - method.response.header.X-Requested-With: false - TenantServiceByIdResourceCORS: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PrivateApi - ResourceId: !Ref TenantServiceByIdResource - HttpMethod: OPTIONS - AuthorizationType: NONE - Integration: - Type: MOCK - PassthroughBehavior: WHEN_NO_MATCH - IntegrationResponses: - - StatusCode: '200' - ResponseTemplates: {application/json: ''} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS,DELETE'" - method.response.header.Access-Control-Allow-Origin: "'*'" - method.response.header.Access-Control-Max-Age: "'3600'" - method.response.header.X-Requested-With: "'*'" - RequestTemplates: - application/json: '{"statusCode": 200}' - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: false - method.response.header.Access-Control-Allow-Methods: false - method.response.header.Access-Control-Allow-Origin: false - method.response.header.Access-Control-Max-Age: false - method.response.header.X-Requested-With: false - SettingsServiceResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PrivateApi - ParentId: !Ref RootResourceId - PathPart: 'settings' - SettingsServiceByIdResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PrivateApi - ParentId: !Ref SettingsServiceResource - PathPart: '{id}' - SettingsServiceSecretResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PrivateApi - ParentId: !Ref SettingsServiceByIdResource - PathPart: 'secret' - SettingsServiceConfigResource: - Type: AWS::ApiGateway::Resource - Properties: - RestApiId: !Ref PrivateApi - ParentId: !Ref SettingsServiceResource - PathPart: 'config' - SettingsServiceGetAllMethod: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PrivateApi - ResourceId: !Ref SettingsServiceResource - HttpMethod: GET - AuthorizationType: AWS_IAM - Integration: - Type: AWS_PROXY - IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SettingsServiceGetAll}/invocations - PassthroughBehavior: WHEN_NO_MATCH - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Origin: false - SettingsServiceGetAllPrivateLambdaPermission: - Type: AWS::Lambda::Permission - Properties: - Principal: apigateway.amazonaws.com - Action: lambda:InvokeFunction - FunctionName: !Ref SettingsServiceGetAll - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PrivateApi}/*/GET/settings - SettingsServiceResourceCORS: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PrivateApi - ResourceId: !Ref SettingsServiceResource - HttpMethod: OPTIONS - AuthorizationType: NONE - Integration: - Type: MOCK - PassthroughBehavior: WHEN_NO_MATCH - IntegrationResponses: - - StatusCode: '200' - ResponseTemplates: {application/json: ''} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'" - method.response.header.Access-Control-Allow-Origin: "'*'" - method.response.header.Access-Control-Max-Age: "'3600'" - method.response.header.X-Requested-With: "'*'" - RequestTemplates: - application/json: '{"statusCode": 200}' - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: false - method.response.header.Access-Control-Allow-Methods: false - method.response.header.Access-Control-Allow-Origin: false - method.response.header.Access-Control-Max-Age: false - method.response.header.X-Requested-With: false - SettingsServiceSecretMethod: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PrivateApi - ResourceId: !Ref SettingsServiceSecretResource - HttpMethod: GET - AuthorizationType: AWS_IAM - RequestParameters: {method.request.path.id: true} - Integration: - Type: AWS_PROXY - IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SettingsServiceGetSecret}/invocations - PassthroughBehavior: WHEN_NO_MATCH - RequestParameters: {integration.request.path.id: 'method.request.path.id'} - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Origin: false - SettingsServiceGetSecretLambdaPermission: - Type: AWS::Lambda::Permission - Properties: - Principal: apigateway.amazonaws.com - Action: lambda:InvokeFunction - FunctionName: !Ref SettingsServiceGetSecret - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PrivateApi}/*/GET/settings/{id}/secret - SettingsServiceSecretResourceCORS: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PrivateApi - ResourceId: !Ref SettingsServiceSecretResource - HttpMethod: OPTIONS - AuthorizationType: NONE - Integration: - Type: MOCK - PassthroughBehavior: WHEN_NO_MATCH - IntegrationResponses: - - StatusCode: '200' - ResponseTemplates: {application/json: ''} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'" - method.response.header.Access-Control-Allow-Origin: "'*'" - method.response.header.Access-Control-Max-Age: "'3600'" - method.response.header.X-Requested-With: "'*'" - RequestTemplates: - application/json: '{"statusCode": 200}' - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: false - method.response.header.Access-Control-Allow-Methods: false - method.response.header.Access-Control-Allow-Origin: false - method.response.header.Access-Control-Max-Age: false - method.response.header.X-Requested-With: false - SettingsServiceGetAppConfigMethod: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PrivateApi - ResourceId: !Ref SettingsServiceConfigResource - HttpMethod: GET - AuthorizationType: AWS_IAM - Integration: - Type: AWS_PROXY - IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SettingsServiceGetAppConfig}/invocations - PassthroughBehavior: WHEN_NO_MATCH - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Origin: false - SettingsServiceGetAppConfigLambdaPermission: - Type: AWS::Lambda::Permission - Properties: - Principal: apigateway.amazonaws.com - Action: lambda:InvokeFunction - FunctionName: !Ref SettingsServiceGetAppConfig - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PrivateApi}/*/GET/settings/config - SettingsServiceDeleteAppConfigMethod: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PrivateApi - ResourceId: !Ref SettingsServiceConfigResource - HttpMethod: DELETE - AuthorizationType: AWS_IAM - Integration: - Type: AWS_PROXY - IntegrationHttpMethod: POST - Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SettingsServiceDeleteAppConfig}/invocations - PassthroughBehavior: WHEN_NO_MATCH - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Origin: false - SettingsServiceDeleteAppConfigLambdaPermission: - Type: AWS::Lambda::Permission - Properties: - Principal: apigateway.amazonaws.com - Action: lambda:InvokeFunction - FunctionName: !Ref SettingsServiceDeleteAppConfig - SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PrivateApi}/*/DELETE/settings/config - SettingsServiceConfigResourceCORS: - Type: AWS::ApiGateway::Method - Properties: - RestApiId: !Ref PrivateApi - ResourceId: !Ref SettingsServiceConfigResource - HttpMethod: OPTIONS - AuthorizationType: NONE - Integration: - Type: MOCK - PassthroughBehavior: WHEN_NO_MATCH - IntegrationResponses: - - StatusCode: '200' - ResponseTemplates: {application/json: ''} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" - method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS,DELETE'" - method.response.header.Access-Control-Allow-Origin: "'*'" - method.response.header.Access-Control-Max-Age: "'3600'" - method.response.header.X-Requested-With: "'*'" - RequestTemplates: - application/json: '{"statusCode": 200}' - MethodResponses: - - StatusCode: '200' - ResponseModels: {application/json: Empty} - ResponseParameters: - method.response.header.Access-Control-Allow-Headers: false - method.response.header.Access-Control-Allow-Methods: false - method.response.header.Access-Control-Allow-Origin: false - method.response.header.Access-Control-Max-Age: false - method.response.header.X-Requested-With: false - # To Do: Remove deployment from CloudFormation and deal with it either through - # CodePipeline or a custom resource, or some other external deployment action. - ApiDeployment: - Type: AWS::ApiGateway::Deployment - DependsOn: - - QuotaServiceCheckMethod - - QuotaServiceCheckResourceCORS - - TenantServiceGetAllMethod - - TenantServiceGetByIdMethod - - TenantServiceInsertMethod - - TenantServiceResourceCORS - - SettingsServiceGetAllMethod - - SettingsServiceResourceCORS - - SettingsServiceSecretMethod - - SettingsServiceSecretResourceCORS - - SettingsServiceDeleteAppConfigMethod - - SettingsServiceGetAppConfigMethod - - SettingsServiceConfigResourceCORS - Properties: - RestApiId: !Ref PrivateApi - #StageDescription: - # DataTraceEnabled: true - # LoggingLevel: ERROR - #StageName: !Ref PrivateApiStage - ApiStage: - Type: AWS::ApiGateway::Stage - Properties: - RestApiId: !Ref PrivateApi - StageName: !Ref PrivateApiStage - DeploymentId: !Ref ApiDeployment -... \ No newline at end of file diff --git a/resources/saas-boost-svc-app-config.yaml b/resources/saas-boost-svc-app-config.yaml new file mode 100644 index 00000000..f8d56e13 --- /dev/null +++ b/resources/saas-boost-svc-app-config.yaml @@ -0,0 +1,476 @@ +--- +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +AWSTemplateFormatVersion: 2010-09-09 +Description: AWS SaaS Boost App Config Service +Parameters: + Environment: + Description: Environment name + Type: String + SaaSBoostBucket: + Description: SaaS Boost assets S3 bucket + Type: String + LambdaSourceFolder: + Description: Folder for lambda source code to change on each deployment + Type: String + SaaSBoostUtilsLayer: + Description: Utils Layer ARN + Type: String + ApiGatewayHelperLayer: + Description: API Gateway Helper Layer ARN + Type: String + CloudFormationUtilsLayer: + Description: CloudFormation Utils Layer ARN + Type: String + SaaSBoostEventBus: + Description: SaaS Boost Eventbridge Bus + Type: String + ResourcesBucket: + Description: S3 bucket containing tenant custom config files (zip archive) + Type: String +Resources: + RdsOptionsTable: + Type: AWS::DynamoDB::Table + Properties: + TableName: !Sub sb-${Environment}-rds-options + AttributeDefinitions: + - AttributeName: region + AttributeType: S + - AttributeName: engine + AttributeType: S + KeySchema: + - AttributeName: region + KeyType: HASH + - AttributeName: engine + KeyType: RANGE + ProvisionedThroughput: + ReadCapacityUnits: 5 + WriteCapacityUnits: 5 + RdsOptionsExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub sb-${Environment}-rds-options-role-${AWS::Region} + Path: '/' + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyName: !Sub sb-${Environment}-rds-options-policy + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - logs:PutLogEvents + Resource: + - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* + - Effect: Allow + Action: + - logs:DescribeLogGroups + - logs:DescribeLogStreams + - logs:CreateLogStream + Resource: + - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* + - Effect: Allow + Action: + - dynamodb:DescribeTable + - dynamodb:PutItem + - dynamodb:Scan + - dynamodb:UpdateItem + Resource: + - !Sub arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${RdsOptionsTable} + - Effect: Allow + Action: + - rds:DescribeOrderableDBInstanceOptions + - rds:DescribeDBEngineVersions + Resource: '*' + RdsOptionsLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-rds-options + RetentionInDays: 30 + RdsOptions: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub sb-${Environment}-rds-options + Role: !GetAtt RdsOptionsExecutionRole.Arn + Runtime: java21 + Architectures: + - arm64 + Timeout: 720 + MemorySize: 768 + Handler: com.amazon.aws.partners.saasfactory.saasboost.RdsOptions + Layers: + - !Ref SaaSBoostUtilsLayer + - !Ref CloudFormationUtilsLayer + Code: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/RdsOptions-lambda.zip + Tags: + - Key: Application + Value: SaaSBoost + - Key: Environment + Value: !Ref Environment + - Key: BoostService + Value: AppConfig + InvokeRdsOptions: + Type: Custom::CustomResource + Properties: + ServiceToken: !GetAtt RdsOptions.Arn + Table: !Ref RdsOptionsTable + AppConfigTable: + Type: AWS::DynamoDB::Table + Properties: + TableName: !Sub sb-${Environment}-app-config + AttributeDefinitions: + - AttributeName: id + AttributeType: S + KeySchema: + - AttributeName: id + KeyType: HASH + ProvisionedThroughput: + ReadCapacityUnits: 5 + WriteCapacityUnits: 5 + AppConfigServiceExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub sb-${Environment}-app-config-svc-role-${AWS::Region} + Path: '/' + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyName: !Sub sb-${Environment}-app-config-svc-policy + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - logs:PutLogEvents + Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* + - Effect: Allow + Action: + - logs:CreateLogStream + - logs:DescribeLogStreams + Resource: + - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* + - Effect: Allow + Action: + - dynamodb:DescribeTable + - dynamodb:GetItem + - dynamodb:Scan + - dynamodb:Query + - dynamodb:PutItem + - dynamodb:DeleteItem + - dynamodb:UpdateItem + Resource: + - !Sub arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${RdsOptionsTable} + - !Sub arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${AppConfigTable} + - Effect: Allow + Action: + - events:PutEvents + Resource: + - !Sub arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/${SaaSBoostEventBus} + - Effect: Allow + Action: + - s3:PutObject + Resource: + - !Sub arn:${AWS::Partition}:s3:::${ResourcesBucket}/* + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: + - !Sub arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:/saas-boost/${Environment}/PRIVATE_API_APP_CLIENT* + - Effect: Allow + Action: + - acm:ListCertificates + Resource: '*' + - Effect: Allow + Action: + - route53:ListHostedZones + Resource: '*' + AppConfigServiceUpdateAppConfigLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-app-config-update + RetentionInDays: 30 + AppConfigServiceUpdateAppConfig: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub sb-${Environment}-app-config-update + Role: !GetAtt AppConfigServiceExecutionRole.Arn + Runtime: java21 + Architectures: + - arm64 + Timeout: 300 + MemorySize: 512 + Handler: com.amazon.aws.partners.saasfactory.saasboost.AppConfigService::updateAppConfig + Code: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/AppConfigService-lambda.zip + Layers: + - !Ref SaaSBoostUtilsLayer + - !Ref ApiGatewayHelperLayer + Environment: + Variables: + SAAS_BOOST_ENV: !Ref Environment + SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus + API_APP_CLIENT: !Sub /saas-boost/${Environment}/PRIVATE_API_APP_CLIENT + RESOURCES_BUCKET: !Ref ResourcesBucket + OPTIONS_TABLE: !Ref RdsOptionsTable + APP_CONFIG_TABLE: !Ref AppConfigTable + Tags: + - Key: Application + Value: SaaSBoost + - Key: Environment + Value: !Ref Environment + - Key: BoostService + Value: AppConfig + AppConfigServiceGetAppConfigLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-app-config-get + RetentionInDays: 30 + AppConfigServiceGetAppConfig: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub sb-${Environment}-app-config-get + Role: !GetAtt AppConfigServiceExecutionRole.Arn + Runtime: java21 + Architectures: + - arm64 + Timeout: 300 + MemorySize: 1024 + Handler: com.amazon.aws.partners.saasfactory.saasboost.AppConfigService::getAppConfig + Code: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/AppConfigService-lambda.zip + Layers: + - !Ref SaaSBoostUtilsLayer + Environment: + Variables: + SAAS_BOOST_ENV: !Ref Environment + SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus + API_APP_CLIENT: !Sub /saas-boost/${Environment}/PRIVATE_API_APP_CLIENT + RESOURCES_BUCKET: !Ref ResourcesBucket + OPTIONS_TABLE: !Ref RdsOptionsTable + APP_CONFIG_TABLE: !Ref AppConfigTable + Tags: + - Key: Application + Value: SaaSBoost + - Key: Environment + Value: !Ref Environment + - Key: BoostService + Value: AppConfig + AppConfigServiceDeleteAppConfigLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-app-config-delete + RetentionInDays: 30 + AppConfigServiceDeleteAppConfig: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub sb-${Environment}-app-config-delete + Role: !GetAtt AppConfigServiceExecutionRole.Arn + Runtime: java21 + Architectures: + - arm64 + Timeout: 300 + MemorySize: 1024 + Handler: com.amazon.aws.partners.saasfactory.saasboost.AppConfigService::deleteAppConfig + Code: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/AppConfigService-lambda.zip + Layers: + - !Ref SaaSBoostUtilsLayer + Environment: + Variables: + SAAS_BOOST_ENV: !Ref Environment + SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus + API_APP_CLIENT: !Sub /saas-boost/${Environment}/PRIVATE_API_APP_CLIENT + RESOURCES_BUCKET: !Ref ResourcesBucket + OPTIONS_TABLE: !Ref RdsOptionsTable + APP_CONFIG_TABLE: !Ref AppConfigTable + Tags: + - Key: Application + Value: SaaSBoost + - Key: Environment + Value: !Ref Environment + - Key: BoostService + Value: AppConfig + AppConfigEventHandlerLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-app-config-events + RetentionInDays: 30 + AppConfigEventHandler: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub sb-${Environment}-settings-app-config-events + Role: !GetAtt AppConfigServiceExecutionRole.Arn + Runtime: java21 + Architectures: + - arm64 + Timeout: 300 + MemorySize: 1024 + Handler: com.amazon.aws.partners.saasfactory.saasboost.AppConfigService::handleAppConfigEvent + Code: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/AppConfigService-lambda.zip + Layers: + - !Ref SaaSBoostUtilsLayer + - !Ref ApiGatewayHelperLayer + Environment: + Variables: + SAAS_BOOST_ENV: !Ref Environment + SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus + API_APP_CLIENT: !Sub /saas-boost/${Environment}/PRIVATE_API_APP_CLIENT + RESOURCES_BUCKET: !Ref ResourcesBucket + OPTIONS_TABLE: !Ref RdsOptionsTable + APP_CONFIG_TABLE: !Ref AppConfigTable + Tags: + - Key: Application + Value: SaaSBoost + - Key: Environment + Value: !Ref Environment + - Key: BoostService + Value: AppConfig + AppConfigServiceOptionsLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-app-config-options + RetentionInDays: 30 + AppConfigServiceOptions: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub sb-${Environment}-app-config-options + Role: !GetAtt AppConfigServiceExecutionRole.Arn + Runtime: java21 + Architectures: + - arm64 + Timeout: 300 + MemorySize: 1024 + Handler: com.amazon.aws.partners.saasfactory.saasboost.AppConfigService::configOptions + Code: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/AppConfigService-lambda.zip + Layers: + - !Ref SaaSBoostUtilsLayer + Environment: + Variables: + SAAS_BOOST_ENV: !Ref Environment + SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus + API_APP_CLIENT: !Sub /saas-boost/${Environment}/PRIVATE_API_APP_CLIENT + RESOURCES_BUCKET: !Ref ResourcesBucket + OPTIONS_TABLE: !Ref RdsOptionsTable + APP_CONFIG_TABLE: !Ref AppConfigTable + Tags: + - Key: Application + Value: SaaSBoost + - Key: Environment + Value: !Ref Environment + - Key: BoostService + Value: AppConfig + AppConfigEventRule: + Type: AWS::Events::Rule + Properties: + Name: !Sub sb-${Environment}-app-config-events + Description: SaaS Boost application config events + EventBusName: !Ref SaaSBoostEventBus + EventPattern: + { + "source": [ + "saas-boost" + ], + "detail-type": [{ + "prefix": "Application Configuration " + }] + } + State: ENABLED + Targets: + - Arn: !GetAtt AppConfigEventHandler.Arn + Id: !Sub sb-${Environment}-app-config-events + AppConfigEventsPermission: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: !Ref AppConfigEventHandler + Principal: events.amazonaws.com + SourceArn: !GetAtt AppConfigEventRule.Arn + AppConfigResourceFilesEventRule: + Type: AWS::Events::Rule + Properties: + Name: !Sub sb-${Environment}-app-config-resources + Description: SaaS Boost application config resources bucket events + EventPattern: !Sub | + { + "source": [ + "aws.s3" + ], + "detail-type": [ + "Object Created" + ], + "detail": { + "reason": [ + "PutObject" + ], + "bucket": { + "name": [ + "${ResourcesBucket}" + ] + }, + "object": { + "key": [{ + "prefix": "services" + }] + } + } + } + State: ENABLED + Targets: + - Arn: !GetAtt AppConfigEventHandler.Arn + Id: !Sub sb-${Environment}-settings-app-config-file-event + AppConfigResourceFilesEventPermission: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: !Ref AppConfigEventHandler + Principal: events.amazonaws.com + SourceArn: !GetAtt AppConfigResourceFilesEventRule.Arn +Outputs: + AppConfigServiceOptionsArn: + Description: Settings Service get options Lambda ARN + Value: !GetAtt AppConfigServiceOptions.Arn + AppConfigServiceGetArn: + Description: Settings Service get application configuration Lambda ARN + Value: !GetAtt AppConfigServiceGetAppConfig.Arn + AppConfigServiceUpdateArn: + Description: Settings Service update application configuration Lambda ARN + Value: !GetAtt AppConfigServiceUpdateAppConfig.Arn + AppConfigServiceDeleteArn: + Description: Settings Service delete application configuration Lambda ARN + Value: !GetAtt AppConfigServiceDeleteAppConfig.Arn +... diff --git a/resources/saas-boost-metering-billing.yaml b/resources/saas-boost-svc-billing-stripe.yaml similarity index 93% rename from resources/saas-boost-metering-billing.yaml rename to resources/saas-boost-svc-billing-stripe.yaml index f662dae6..6ad0b6bf 100644 --- a/resources/saas-boost-metering-billing.yaml +++ b/resources/saas-boost-svc-billing-stripe.yaml @@ -28,11 +28,11 @@ Parameters: EventBus: Description: SaaS Boost Event Bus Type: String - SaaSBoostPrivateApi: - Description: SaaS Boost Private API + SaaSBoostApi: + Description: SaaS Boost API Type: String - PrivateApiStage: - Description: Private API Stage + ApiStage: + Description: SaaS Boost API Stage Type: String SaaSBoostUtilsLayer: Description: Arn of the Utils Layer @@ -69,7 +69,7 @@ Resources: Type: AWS::Lambda::Function Properties: FunctionName: !Sub sb-${Environment}-bill-product-setup - Runtime: java11 + Runtime: java21 Timeout: 300 MemorySize: 384 Environment: @@ -159,7 +159,7 @@ Resources: Type: AWS::Lambda::Function Properties: FunctionName: !Sub sb-${Environment}-bill-event-process - Runtime: java11 + Runtime: java21 Timeout: 300 MemorySize: 384 Environment: @@ -235,7 +235,7 @@ Resources: Type: AWS::Lambda::Function Properties: FunctionName: !Sub sb-${Environment}-bill-aggregate - Runtime: java11 + Runtime: java21 Timeout: 900 MemorySize: 384 Environment: @@ -425,8 +425,9 @@ Resources: Resource: '*' - Effect: Allow Action: - - sts:AssumeRole - Resource: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' + - secretsmanager:GetSecretValue + Resource: + - !Sub arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:/saas-boost/${Environment}/PRIVATE_API_APP_CLIENT* BillPublishLogGroup: @@ -439,16 +440,14 @@ Resources: Type: AWS::Lambda::Function Properties: FunctionName: !Sub sb-${Environment}-bill-publish-external - Runtime: java11 + Runtime: java21 Timeout: 300 MemorySize: 384 Environment: Variables: DYNAMODB_TABLE_NAME: !Ref MeteringBillingTable DYNAMODB_CONFIG_INDEX_NAME: !Ref TenantConfigurationIndexName - API_TRUST_ROLE: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' - API_GATEWAY_HOST: !Sub ${SaaSBoostPrivateApi}.execute-api.${AWS::Region}.${AWS::URLSuffix} - API_GATEWAY_STAGE: !Ref PrivateApiStage + API_APP_CLIENT: !Sub /saas-boost/${Environment}/PRIVATE_API_APP_CLIENT Handler: com.amazon.aws.partners.saasfactory.metering.aggregation.StripeBillingPublish::handleRequest Code: S3Bucket: !Ref SaaSBoostBucket @@ -513,8 +512,9 @@ Resources: - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - Effect: Allow Action: - - sts:AssumeRole - Resource: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' + - secretsmanager:GetSecretValue + Resource: + - !Sub arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:/saas-boost/${Environment}/PRIVATE_API_APP_CLIENT* ##### # Data persistence resources @@ -581,15 +581,13 @@ Resources: Type: AWS::Lambda::Function Properties: FunctionName: !Sub sb-${Environment}-bill-system-setup - Runtime: java11 + Runtime: java21 Timeout: 300 MemorySize: 384 Environment: Variables: SAAS_BOOST_EVENT_BUS: !Ref EventBus - API_TRUST_ROLE: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' - API_GATEWAY_HOST: !Sub ${SaaSBoostPrivateApi}.execute-api.${AWS::Region}.${AWS::URLSuffix} - API_GATEWAY_STAGE: !Ref PrivateApiStage + API_APP_CLIENT: !Sub /saas-boost/${Environment}/PRIVATE_API_APP_CLIENT BILL_PUBLISH_EVENT: !Sub sb-${Environment}-bill-publish-event Handler: com.amazon.aws.partners.saasfactory.metering.onboarding.BillingIntegration::setupBillingSystemListener Code: @@ -656,8 +654,9 @@ Resources: - !Sub arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/sb-${Environment}-bill-publish-event - Effect: Allow Action: - - sts:AssumeRole - Resource: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' + - secretsmanager:GetSecretValue + Resource: + - !Sub arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:/saas-boost/${Environment}/PRIVATE_API_APP_CLIENT* ## Function to setup Tenant in Billing system BillingTenantSetupEventRule: @@ -686,15 +685,13 @@ Resources: Type: AWS::Lambda::Function Properties: FunctionName: !Sub sb-${Environment}-bill-tenant-setup - Runtime: java11 + Runtime: java21 Timeout: 300 MemorySize: 384 Environment: Variables: SAAS_BOOST_EVENT_BUS: !Ref EventBus - API_TRUST_ROLE: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' - API_GATEWAY_HOST: !Sub ${SaaSBoostPrivateApi}.execute-api.${AWS::Region}.${AWS::URLSuffix} - API_GATEWAY_STAGE: !Ref PrivateApiStage + API_APP_CLIENT: !Sub /saas-boost/${Environment}/PRIVATE_API_APP_CLIENT BILL_PUBLISH_EVENT: !Sub sb-${Environment}-bill-publish-event Handler: com.amazon.aws.partners.saasfactory.metering.onboarding.BillingIntegration::setupTenantBillingListener Code: @@ -758,8 +755,9 @@ Resources: - !Sub arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/${EventBus} - Effect: Allow Action: - - sts:AssumeRole - Resource: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' + - secretsmanager:GetSecretValue + Resource: + - !Sub arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:/saas-boost/${Environment}/PRIVATE_API_APP_CLIENT* ## Function to disable Tenant/Subscription in Billing system BillingTenantDisableEventRule: Type: AWS::Events::Rule @@ -787,15 +785,13 @@ Resources: Type: AWS::Lambda::Function Properties: FunctionName: !Sub sb-${Environment}-bill-tenant-disable - Runtime: java11 + Runtime: java21 Timeout: 300 MemorySize: 384 Environment: Variables: SAAS_BOOST_EVENT_BUS: !Ref EventBus - API_TRUST_ROLE: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' - API_GATEWAY_HOST: !Sub ${SaaSBoostPrivateApi}.execute-api.${AWS::Region}.${AWS::URLSuffix} - API_GATEWAY_STAGE: !Ref PrivateApiStage + API_APP_CLIENT: !Sub /saas-boost/${Environment}/PRIVATE_API_APP_CLIENT Handler: com.amazon.aws.partners.saasfactory.metering.onboarding.BillingIntegration::disableTenantBillingListener Code: S3Bucket: !Ref SaaSBoostBucket @@ -857,5 +853,6 @@ Resources: - !Sub arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/${EventBus} - Effect: Allow Action: - - sts:AssumeRole - Resource: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' \ No newline at end of file + - secretsmanager:GetSecretValue + Resource: + - !Sub arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:/saas-boost/${Environment}/PRIVATE_API_APP_CLIENT* \ No newline at end of file diff --git a/resources/saas-boost-svc-billing.yaml b/resources/saas-boost-svc-billing.yaml index f87f34a1..6fcc94a2 100644 --- a/resources/saas-boost-svc-billing.yaml +++ b/resources/saas-boost-svc-billing.yaml @@ -24,19 +24,32 @@ Parameters: LambdaSourceFolder: Description: Folder for lambda source code to change on each deployment Type: String + SaaSBoostEventBus: + Description: SaaS Boost Eventbridge Bus + Type: String SaaSBoostUtilsLayer: Description: Utils Layer ARN Type: String - SaaSBoostPrivateApi: - Description: SaaS Boost Private API - Type: String - PrivateApiStage: - Description: Private API Stage - Type: String ApiGatewayHelperLayer: Description: API Gateway Helper Layer ARN Type: String Resources: + BillingTable: + Type: AWS::DynamoDB::Table + Properties: + TableName: !Sub sb-${Environment}-billing + AttributeDefinitions: + - AttributeName: id + AttributeType: S + KeySchema: + - AttributeName: id + KeyType: HASH + ProvisionedThroughput: + ReadCapacityUnits: 5 + WriteCapacityUnits: 5 + Tags: + - Key: SaaS Boost + Value: !Ref Environment BillingServiceExecutionRole: Type: AWS::IAM::Role Properties: @@ -68,22 +81,41 @@ Resources: - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - Effect: Allow Action: - - sts:AssumeRole - Resource: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' - BillingServiceGetPlansLogs: + - dynamodb:DescribeTable + - dynamodb:GetItem + - dynamodb:PutItem + - dynamodb:DeleteItem + - dynamodb:Scan + - dynamodb:Query + - dynamodb:UpdateItem + Resource: + - !Sub arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${BillingTable} + - Effect: Allow + Action: + - events:PutEvents + Resource: + - !Sub arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/${SaaSBoostEventBus} + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: + - !Sub arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:/saas-boost/${Environment}/PRIVATE_API_APP_CLIENT* + BillingServiceEventHandlerLogs: Type: AWS::Logs::LogGroup Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-billing-plans-get + LogGroupName: !Sub /aws/lambda/sb-${Environment}-billing-events RetentionInDays: 30 - BillingServiceGetPlans: + BillingServiceEventHandler: Type: AWS::Lambda::Function Properties: - FunctionName: !Sub sb-${Environment}-billing-plans-get + FunctionName: !Sub sb-${Environment}-billing-events Role: !GetAtt BillingServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 - Handler: com.amazon.aws.partners.saasfactory.metering.onboarding.SubscriptionService::getPlans + Handler: com.amazon.aws.partners.saasfactory.saasboost.BillingService::handleBillingEvent Code: S3Bucket: !Ref SaaSBoostBucket S3Key: !Sub ${LambdaSourceFolder}/BillingService-lambda.zip @@ -92,18 +124,44 @@ Resources: - !Ref ApiGatewayHelperLayer Environment: Variables: - API_TRUST_ROLE: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' - API_GATEWAY_HOST: !Sub ${SaaSBoostPrivateApi}.execute-api.${AWS::Region}.${AWS::URLSuffix} - API_GATEWAY_STAGE: !Ref PrivateApiStage + SAAS_BOOST_ENV: !Ref Environment + BILLING_TABLE: !Ref BillingTable + API_APP_CLIENT: !Sub /saas-boost/${Environment}/PRIVATE_API_APP_CLIENT + SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" + - Key: Application + Value: SaaSBoost + - Key: Environment Value: !Ref Environment - - Key: "BoostService" - Value: "Billing" + BillingServiceEventRule: + Type: AWS::Events::Rule + Properties: + Name: !Sub sb-${Environment}-billing-events + Description: SaaS Boost billing events + EventBusName: !Ref SaaSBoostEventBus + EventPattern: + { + "source": [ + "saas-boost" + ], + "detail-type": [ + {"prefix": "Billing "}, + {"prefix": "Tenant "} + ] + } + State: ENABLED + Targets: + - Arn: !GetAtt BillingServiceEventHandler.Arn + Id: !Sub sb-${Environment}-billing-events + BillingServiceEventsPermission: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: !Ref BillingServiceEventHandler + Principal: events.amazonaws.com + SourceArn: !GetAtt BillingServiceEventRule.Arn Outputs: - BillingServiceGetPlansArn: - Description: Billing Service subscription plans Lambda ARN - Value: !GetAtt BillingServiceGetPlans.Arn + BillingServiceEventHandlerArn: + Description: Billing Service event handler Lambda ARN + Value: !GetAtt BillingServiceEventHandler.Arn ... \ No newline at end of file diff --git a/resources/saas-boost-svc-identity.yaml b/resources/saas-boost-svc-identity.yaml new file mode 100644 index 00000000..6b3c61fa --- /dev/null +++ b/resources/saas-boost-svc-identity.yaml @@ -0,0 +1,234 @@ +--- +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +AWSTemplateFormatVersion: 2010-09-09 +Description: AWS SaaS Boost Identity Service +Parameters: + Environment: + Description: Environment name + Type: String + SaaSBoostBucket: + Description: SaaS Boost assets S3 bucket + Type: String + LambdaSourceFolder: + Description: Folder for lambda source code to change on each deployment + Type: String + SaaSBoostEventBus: + Description: SaaS Boost Eventbridge Bus + Type: String + SaaSBoostUtilsLayer: + Description: Utils Layer ARN + Type: String + AppPlaneAccountId: + Description: Application Plane account to communicate with this Control Plane. Leave blank to use the same AWS account for your application plane resources. + Type: String + Default: '' +Conditions: + AppPlaneSameAccount: !Equals ['', !Ref AppPlaneAccountId] +Resources: + IdentityServiceAppPlaneTrustPolicy: + Type: AWS::IAM::ManagedPolicy + Properties: + Description: Trust policy for App Plane IdP Administration + Path: '/' + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - sts:AssumeRole + Resource: + !If + - AppPlaneSameAccount + - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/sb-${Environment}-cp-id-svc-role-${AWS::Region} + - !Sub arn:${AWS::Partition}:iam::${AppPlaneAccountId}:role/sb-${Environment}-cp-id-svc-role-${AWS::Region} + IdentityServiceProviderConfigSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Sub /saas-boost/${Environment}/IDENTITY_PROVIDER_CONFIG + SecretString: '{}' + IdentityServiceExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub sb-${Environment}-identity-svc-role-${AWS::Region} + Path: '/' + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + ManagedPolicyArns: + - !Ref IdentityServiceAppPlaneTrustPolicy + Policies: + - PolicyName: !Sub sb-${Environment}-identity-svc-policy + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - logs:PutLogEvents + Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* + - Effect: Allow + Action: + - logs:CreateLogStream + - logs:DescribeLogStreams + Resource: + - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + - secretsmanager:PutSecretValue + Resource: + - !Ref IdentityServiceProviderConfigSecret + IdentityServiceGetProvidersLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-identity-get-providers + RetentionInDays: 30 + IdentityServiceGetProviders: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub sb-${Environment}-identity-get-providers + Role: !GetAtt IdentityServiceExecutionRole.Arn + Runtime: java21 + Architectures: + - arm64 + Timeout: 300 + MemorySize: 512 + Handler: com.amazon.aws.partners.saasfactory.saasboost.IdentityService::getProviders + Code: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/IdentityService-lambda.zip + Layers: + - !Ref SaaSBoostUtilsLayer + Environment: + Variables: + SAAS_BOOST_ENV: !Ref Environment + IDENTITY_PROVIDER_CONFIG: !GetAtt IdentityServiceProviderConfigSecret.Id + IdentityServiceSetProviderLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-identity-set-provider + RetentionInDays: 30 + IdentityServiceSetProvider: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub sb-${Environment}-identity-set-provider + Role: !GetAtt IdentityServiceExecutionRole.Arn + Runtime: java21 + Architectures: + - arm64 + Timeout: 300 + MemorySize: 512 + Handler: com.amazon.aws.partners.saasfactory.saasboost.IdentityService::setProvider + Code: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/IdentityService-lambda.zip + Layers: + - !Ref SaaSBoostUtilsLayer + Environment: + Variables: + SAAS_BOOST_ENV: !Ref Environment + IDENTITY_PROVIDER_CONFIG: !GetAtt IdentityServiceProviderConfigSecret.Id + IdentityServiceGetProviderLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-identity-get-provider + RetentionInDays: 30 + IdentityServiceGetProvider: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub sb-${Environment}-identity-get-provider + Role: !GetAtt IdentityServiceExecutionRole.Arn + Runtime: java21 + Architectures: + - arm64 + Timeout: 300 + MemorySize: 512 + Handler: com.amazon.aws.partners.saasfactory.saasboost.IdentityService::getProvider + Code: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/IdentityService-lambda.zip + Layers: + - !Ref SaaSBoostUtilsLayer + Environment: + Variables: + SAAS_BOOST_ENV: !Ref Environment + IDENTITY_PROVIDER_CONFIG: !GetAtt IdentityServiceProviderConfigSecret.Id + IdentityServiceEventHandlerLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-identity-event-handler + RetentionInDays: 30 + IdentityServiceEventHandler: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub sb-${Environment}-identity-event-handler + Role: !GetAtt IdentityServiceExecutionRole.Arn + Runtime: java21 + Architectures: + - arm64 + Timeout: 300 + MemorySize: 512 + Handler: com.amazon.aws.partners.saasfactory.saasboost.IdentityService::handleEvent + Code: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/IdentityService-lambda.zip + Layers: + - !Ref SaaSBoostUtilsLayer + Environment: + Variables: + SAAS_BOOST_ENV: !Ref Environment + IDENTITY_PROVIDER_CONFIG: !GetAtt IdentityServiceProviderConfigSecret.Id + IdentityServiceEventRule: + Type: AWS::Events::Rule + Properties: + Name: !Sub sb-${Environment}-identity-events + Description: SaaS Boost Identity Service Events + EventBusName: !Ref SaaSBoostEventBus + EventPattern: + { + "source": [ + "saas-boost" + ], + "detail-type": [ + "Onboarding Tenant Assigned" + ] + } + State: ENABLED + Targets: + - Arn: !GetAtt IdentityServiceEventHandler.Arn + Id: !Sub sb-${Environment}-identity-events + IdentityServiceEventsPermission: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: !Ref IdentityServiceEventHandler + Principal: events.amazonaws.com + SourceArn: !GetAtt IdentityServiceEventRule.Arn +Outputs: + IdentityServiceGetProvidersArn: + Description: Identity Service get all providers Lambda ARN + Value: !GetAtt IdentityServiceGetProviders.Arn + IdentityServiceGetProviderArn: + Description: Identity Service get active identity provider Lambda ARN + Value: !GetAtt IdentityServiceGetProvider.Arn + IdentityServiceSetProviderArn: + Description: Identity Service set active identity provider Lambda ARN + Value: !GetAtt IdentityServiceSetProvider.Arn +... \ No newline at end of file diff --git a/resources/saas-boost-svc-metrics.yaml b/resources/saas-boost-svc-metrics.yaml index a93b80ba..84c0279d 100644 --- a/resources/saas-boost-svc-metrics.yaml +++ b/resources/saas-boost-svc-metrics.yaml @@ -30,203 +30,49 @@ Parameters: ApiGatewayHelperLayer: Description: API Gateway Helper Layer ARN Type: String - AccessLogs: - Description: ALB Access Logs Bucket - Type: String +# AccessLogs: +# Description: ALB Access Logs Bucket +# Type: String AthenaOutput: Description: Web S3 bucket Type: String - SaaSBoostPrivateApi: + SaaSBoostApi: Description: SaaS Boost Private API Type: String - PrivateApiStage: + ApiStage: Description: The API Gateway REST API stage name for the SaaS Boost private API Type: String -Mappings: - # Mappings for ELB accounts from - # https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.htm - # https://docs.amazonaws.cn/en_us/elasticloadbalancing/latest/application/enable-access-logging.htm - # Be sure to leave quote marks around the account ids or they'll get transformed to exponential notation by FindInMap - Region2ELBAccountId: - us-east-1: - AccountId: '127311923021' - us-east-2: - AccountId: '033677994240' - us-west-1: - AccountId: '027434742980' - us-west-2: - AccountId: '797873946194' - af-south-1: - AccountId: '098369216593' - ca-central-1: - AccountId: '985666609251' - eu-central-1: - AccountId: '054676820928' - eu-west-1: - AccountId: '156460612806' - eu-west-2: - AccountId: '652711504416' - eu-south-1: - AccountId: '635631232127' - eu-west-3: - AccountId: '009996457667' - eu-north-1: - AccountId: '897822967062' - ap-east-1: - AccountId: '754344448648' - ap-northeast-1: - AccountId: '582318560864' - ap-northeast-2: - AccountId: '600734575887' - ap-northeast-3: - AccountId: '383597477331' - ap-southeast-1: - AccountId: '114774131450' - ap-southeast-2: - AccountId: '783225319266' - ap-south-1: - AccountId: '718504428378' - me-south-1: - AccountId: '076674570225' - sa-east-1: - AccountId: '507241528517' - cn-north-1: - AccountId: '638102146993' - cn-northwest-1: - AccountId: '037604701340' Resources: - # Publish access logs data to S3 bucket for graphing - ALBLogsBucketPolicy: - Type: AWS::S3::BucketPolicy + MetricsQueue: + Type: AWS::SQS::Queue Properties: - Bucket: !Ref AccessLogs - PolicyDocument: - Version: 2012-10-17 - Statement: - - Sid: ELBAccessLogs - Effect: Allow - Principal: - AWS: - Fn::Join: ['', ['arn:', !Ref 'AWS::Partition', ':iam::', !FindInMap [Region2ELBAccountId, !Ref 'AWS::Region', 'AccountId'], ':root']] - Action: s3:PutObject - Resource: !Sub arn:${AWS::Partition}:s3:::${AccessLogs}/access-logs/AWSLogs/${AWS::AccountId}/* - - Sid: AWSLogDeliveryWrite - Effect: Allow - Principal: - Service: delivery.logs.amazonaws.com - Action: s3:PutObject - Resource: !Sub arn:${AWS::Partition}:s3:::${AccessLogs}/access-logs/AWSLogs/${AWS::AccountId}/* - Condition: - StringEquals: - s3:x-amz-acl: bucket-owner-full-control - - Sid: AWSLogDeliveryAclCheck - Effect: Allow - Principal: - Service: delivery.logs.amazonaws.com - Action: s3:GetBucketAcl - Resource: !Sub arn:${AWS::Partition}:s3:::${AccessLogs} - - Sid: DenyNonHttps - Effect: Deny - Action: s3:* - Principal: '*' - Resource: - - !Sub arn:${AWS::Partition}:s3:::${AccessLogs}/* - - !Sub arn:${AWS::Partition}:s3:::${AccessLogs} - Condition: - Bool: { 'aws:SecureTransport': false } - AccessLogsDatabase: - Type: AWS::Glue::Database + QueueName: !Sub sb-${Environment}-metrics + VisibilityTimeout: 900 + RedrivePolicy: + deadLetterTargetArn: !GetAtt MetricsDLQ.Arn + maxReceiveCount: 10 + SqsManagedSseEnabled: true + MetricsDLQ: + Type: AWS::SQS::Queue Properties: - CatalogId: !Ref 'AWS::AccountId' - DatabaseInput: - Description: SaaS Boost Access Logs Database - Name: !Sub sb_${Environment}_alb_access_logs - AccessLogsTable: - Type: AWS::Glue::Table + QueueName: !Sub sb-${Environment}-metrics-dlq + SqsManagedSseEnabled: true + RedriveAllowPolicy: !Sub | + { + "redrivePermission": "byQueue", + "sourceQueueArns": ["arn:${AWS::Partition}:sqs:${AWS::Region}:${AWS::AccountId}:sb-${Environment}-metrics"] + } + MetricsQueueEventMapping: + Type: AWS::Lambda::EventSourceMapping Properties: - CatalogId: !Ref 'AWS::AccountId' - DatabaseName: !Ref AccessLogsDatabase - TableInput: - Description: ALB Access Logs from Tenants - Name: !Sub sb_${Environment}_access_logs - Owner: saas-boost - TableType: EXTERNAL_TABLE - StorageDescriptor: - Columns: - - Name: type - Type: string - - Name: time - Type: string - - Name: elb - Type: string - - Name: client_ip - Type: string - - Name: client_port - Type: int - - Name: target_ip - Type: string - - Name: target_port - Type: int - - Name: request_processing_time - Type: double - - Name: target_processing_time - Type: double - - Name: response_processing_time - Type: double - - Name: elb_status_code - Type: string - - Name: target_status_code - Type: string - - Name: received_bytes - Type: bigint - - Name: sent_bytes - Type: bigint - - Name: request_verb - Type: string - - Name: request_url - Type: string - - Name: request_proto - Type: string - - Name: user_agent - Type: string - - Name: ssl_cipher - Type: string - - Name: ssl_protocol - Type: string - - Name: target_group_arn - Type: string - - Name: trace_id - Type: string - - Name: domain_name - Type: string - - Name: chosen_cert_arn - Type: string - - Name: matched_rule_priority - Type: string - - Name: request_creation_time - Type: string - - Name: actions_executed - Type: string - - Name: redirect_url - Type: string - - Name: lambda_error_reason - Type: string - - Name: target_port_list - Type: string - - Name: target_status_code_list - Type: string - - Name: new_field - Type: string - Compressed: False - Location: !Sub s3://${AccessLogs}/access-logs/AWSLogs/${AWS::AccountId}/elasticloadbalancing/${AWS::Region} - InputFormat: org.apache.hadoop.mapred.TextInputFormat - OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat - SerdeInfo: - SerializationLibrary: org.apache.hadoop.hive.serde2.RegexSerDe - Parameters: - serialization.format: 1 - input.regex: '([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*):([0-9]*) ([^ ]*)[:-]([0-9]*) ([-.0-9]*) ([-.0-9]*) ([-.0-9]*) (|[-0-9]*) (-|[-0-9]*) ([-0-9]*) ([-0-9]*) \"([^ ]*) ([^ ]*) (- |[^ ]*)\" \"([^\"]*)\" ([A-Z0-9-]+) ([A-Za-z0-9.-]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^\"]*)\" ([-.0-9]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^ ]*)\" \"([^\s]+)\" \"([^\s]+)\"(.*)' - MetricServiceExecutionRole: + BatchSize: 100 + MaximumBatchingWindowInSeconds: 30 + Enabled: true + EventSourceArn: !GetAtt MetricsQueue.Arn + FunctionName: !GetAtt MetricsQueueProcessor.Arn + FunctionResponseTypes: + - ReportBatchItemFailures + MetricsServiceExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub sb-${Environment}-metrics-svc-role-${AWS::Region} @@ -248,8 +94,7 @@ Resources: - Effect: Allow Action: - logs:PutLogEvents - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* + Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - Effect: Allow Action: - logs:CreateLogStream @@ -258,317 +103,514 @@ Resources: - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - Effect: Allow Action: - - athena:ListDataCatalogs - - athena:ListWorkGroups - - cloudwatch:GetMetricData - - cloudwatch:ListMetrics - - application-autoscaling:DescribeScalableTargets - Resource: '*' - - Effect: Allow - Action: - - s3:GetBucketLocation - - s3:ListBucket - - s3:ListBucketMultipartUploads + - sqs:SendMessage + - sqs:SendMessageBatch + - sqs:ReceiveMessage + - sqs:DeleteMessage + - sqs:GetQueueAttributes + - sqs:ChangeMessageVisibility Resource: - - !Sub arn:${AWS::Partition}:s3:::${AthenaOutput} - - !Sub arn:${AWS::Partition}:s3:::${AccessLogs} + - !GetAtt MetricsQueue.Arn - Effect: Allow Action: - - s3:AbortMultipartUpload - - s3:GetObject - - s3:ListMultipartUploadParts - - s3:PutObject + - sqs:SendMessage Resource: - - !Sub arn:${AWS::Partition}:s3:::${AthenaOutput}/* - - !Sub arn:${AWS::Partition}:s3:::${AccessLogs}/* - - Effect: Allow - Action: - - glue:GetTable - - glue:GetDatabase - - glue:GetPartitions - Resource: - - !Sub arn:${AWS::Partition}:glue:${AWS::Region}:${AWS::AccountId}:database/${AccessLogsDatabase} - - !Sub arn:${AWS::Partition}:glue:${AWS::Region}:${AWS::AccountId}:table/${AccessLogsDatabase}/${AccessLogsTable} - - !Sub arn:${AWS::Partition}:glue:${AWS::Region}:${AWS::AccountId}:catalog - - !Sub arn:${AWS::Partition}:athena:${AWS::Region}:${AWS::AccountId}:workgroup/primary - - Effect: Allow - Action: - - athena:* - Resource: - - !Sub arn:${AWS::Partition}:athena:${AWS::Region}:${AWS::AccountId}:workgroup/primary - - Effect: Allow - Action: - - sts:AssumeRole - Resource: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' - MetricsServicePublishRequestCountLogs: + - !GetAtt MetricsDLQ.Arn + MetricsQueueProcessorLogs: Type: AWS::Logs::LogGroup Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-metrics-pub-requests + LogGroupName: !Sub /aws/lambda/sb-${Environment}-metrics-queue-processor RetentionInDays: 30 - MetricsServicePublishRequestCount: + MetricsQueueProcessor: Type: AWS::Lambda::Function Properties: - FunctionName: !Sub sb-${Environment}-metrics-pub-requests - Role: !GetAtt MetricServiceExecutionRole.Arn - Runtime: java11 - Timeout: 900 - MemorySize: 384 - Handler: com.amazon.aws.partners.saasfactory.saasboost.MetricService::publishRequestCountMetrics - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/MetricsService-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - Environment: - Variables: - ATHENA_DATABASE: !Ref AccessLogsDatabase - S3_ATHENA_BUCKET: !Ref AthenaOutput - S3_ATHENA_OUTPUT_PATH: !Sub s3://${AthenaOutput}/query-results/ - ACCESS_LOGS_TABLE: !Ref AccessLogsTable - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "Metrics" - MetricsServicePublishResponseTimeLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-metrics-pub-response - RetentionInDays: 30 - MetricsServicePublishResponseTime: - Type: AWS::Lambda::Function - Properties: - FunctionName: !Sub sb-${Environment}-metrics-pub-response - Role: !GetAtt MetricServiceExecutionRole.Arn - Runtime: java11 - Timeout: 900 - MemorySize: 384 - Handler: com.amazon.aws.partners.saasfactory.saasboost.MetricService::publishResponseTimeMetrics - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/MetricsService-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - Environment: - Variables: - ATHENA_DATABASE: !Ref AccessLogsDatabase - S3_ATHENA_BUCKET: !Ref AthenaOutput - S3_ATHENA_OUTPUT_PATH: !Sub s3://${AthenaOutput}/query-results/ - ACCESS_LOGS_TABLE: !Ref AccessLogsTable - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "Metrics" - MetricsServiceAddAthenaPartitionLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-metrics-add-partition - RetentionInDays: 30 - MetricsServiceAddAthenaPartition: - Type: AWS::Lambda::Function - Properties: - FunctionName: !Sub sb-${Environment}-metrics-add-partition - Role: !GetAtt MetricServiceExecutionRole.Arn - Runtime: java11 - Timeout: 900 - MemorySize: 384 - Handler: com.amazon.aws.partners.saasfactory.saasboost.MetricService::addAthenaPartition - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/MetricsService-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - Environment: - Variables: - ATHENA_DATABASE: !Ref AccessLogsDatabase - S3_ATHENA_BUCKET: !Ref AthenaOutput - S3_ATHENA_OUTPUT_PATH: !Sub s3://${AthenaOutput}/query-results/ - ACCESS_LOGS_TABLE: !Ref AccessLogsTable - ACCESS_LOGS_PATH: !Sub s3://${AccessLogs}/access-logs/AWSLogs/${AWS::AccountId}/elasticloadbalancing/${AWS::Region} - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "Metrics" - MetricServiceQueryLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-metrics-query - RetentionInDays: 30 - MetricServiceQuery: - Type: AWS::Lambda::Function - Properties: - FunctionName: !Sub sb-${Environment}-metrics-query - Role: !GetAtt MetricServiceExecutionRole.Arn - Runtime: java11 + FunctionName: !Sub sb-${Environment}-metrics-queue-processor + Role: !GetAtt MetricsServiceExecutionRole.Arn + Runtime: java21 Timeout: 300 - MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.MetricService::queryMetrics + MemorySize: 2112 + Handler: com.amazon.aws.partners.saasfactory.saasboost.MetricsService::processMetricsQueue Code: S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/MetricsService-lambda.zip + S3Key: !Sub ${LambdaSourceFolder}MetricsService-lambda.zip Layers: - !Ref SaaSBoostUtilsLayer - - !Ref ApiGatewayHelperLayer Environment: Variables: - ATHENA_DATABASE: !Ref AccessLogsDatabase - S3_ATHENA_BUCKET: !Ref AthenaOutput - S3_ATHENA_OUTPUT_PATH: !Sub s3://${AthenaOutput}/query-results/ - ACCESS_LOGS_TABLE: !Ref AccessLogsTable - API_TRUST_ROLE: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' - API_GATEWAY_HOST: !Sub ${SaaSBoostPrivateApi}.execute-api.${AWS::Region}.${AWS::URLSuffix} - API_GATEWAY_STAGE: !Ref PrivateApiStage - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "Metrics" - MetricsServiceGetDatasetsLog: + SAAS_BOOST_ENV: !Ref Environment + METRICS_DLQ: !Ref MetricsDLQ + MetricsServicePutMetricsLogs: Type: AWS::Logs::LogGroup Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-metrics-datasets + LogGroupName: !Sub /aws/lambda/sb-${Environment}-metrics-put RetentionInDays: 30 - MetricsServiceGetDatasets: + MetricsServicePutMetrics: Type: AWS::Lambda::Function Properties: - FunctionName: !Sub sb-${Environment}-metrics-datasets - Role: !GetAtt MetricServiceExecutionRole.Arn - Runtime: java11 - Timeout: 900 - MemorySize: 384 - Handler: com.amazon.aws.partners.saasfactory.saasboost.MetricService::getAccessMetricsSignedUrls - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/MetricsService-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - Environment: - Variables: - ATHENA_DATABASE: !Ref AccessLogsDatabase - S3_ATHENA_BUCKET: !Ref AthenaOutput - S3_ATHENA_OUTPUT_PATH: !Sub s3://${AthenaOutput}/query-results/ - ACCESS_LOGS_TABLE: !Ref AccessLogsTable - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "Metrics" - MetricsServiceQueryAccessLogLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-metrics-access-log-query - RetentionInDays: 30 - MetricsServiceQueryAccessLog: - Type: AWS::Lambda::Function - Properties: - FunctionName: !Sub sb-${Environment}-metrics-access-log-query - Role: !GetAtt MetricServiceExecutionRole.Arn - Runtime: java11 + FunctionName: !Sub sb-${Environment}-metrics-put + Role: !GetAtt MetricsServiceExecutionRole.Arn + Runtime: java21 Timeout: 300 - MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.MetricService::queryAccessLogs + MemorySize: 2112 + Handler: com.amazon.aws.partners.saasfactory.saasboost.MetricsService::putMetrics Code: S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/MetricsService-lambda.zip + S3Key: !Sub ${LambdaSourceFolder}MetricsService-lambda.zip Layers: - !Ref SaaSBoostUtilsLayer - - !Ref ApiGatewayHelperLayer Environment: Variables: - ATHENA_DATABASE: !Ref AccessLogsDatabase - S3_ATHENA_BUCKET: !Ref AthenaOutput - S3_ATHENA_OUTPUT_PATH: !Sub s3://${AthenaOutput}/query-results/ - ACCESS_LOGS_TABLE: !Ref AccessLogsTable - API_TRUST_ROLE: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' - API_GATEWAY_HOST: !Sub ${SaaSBoostPrivateApi}.execute-api.${AWS::Region}.${AWS::URLSuffix} - API_GATEWAY_STAGE: !Ref PrivateApiStage - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "Metrics" - PublishRequestCountEvent: - Type: AWS::Events::Rule - Properties: - Name: !Sub sb-${Environment}-metrics-count - Description: A scheduled task to publish access log metrics to s3 web bucket - # Run this every 30 minutes - ScheduleExpression: "cron(0/30 * * * ? *)" - State: ENABLED - Targets: - - Arn: !GetAtt MetricsServicePublishRequestCount.Arn - Id: MetricsServicePublishRequestCount - PublishResponseTimeMetricsEvent: - Type: AWS::Events::Rule - Properties: - Name: !Sub sb-${Environment}-metrics-latency - Description: A scheduled task to publish access log metrics to s3 web bucket - # Run this every 30 minutes - ScheduleExpression: "cron(0/30 * * * ? *)" - State: ENABLED - Targets: - - Arn: !GetAtt MetricsServicePublishResponseTime.Arn - Id: MetricsServicePublishResponseTime - AddAthenaPartitionEvent: - Type: AWS::Events::Rule - Properties: - Name: !Sub sb-${Environment}-metrics-athena-partition - Description: A scheduled task to add partition for access logs daily - # Run at 00:15 every day - ScheduleExpression: "cron(15 0 * * ? *)" - State: DISABLED - Targets: - - Arn: !GetAtt MetricsServiceAddAthenaPartition.Arn - Id: MetricsServiceAddAthenaPartition - MetricsServicePublishRequestCountPermission: - Type: AWS::Lambda::Permission - Properties: - Action: lambda:InvokeFunction - FunctionName: !GetAtt MetricsServicePublishRequestCount.Arn - Principal: events.amazonaws.com - SourceArn: !GetAtt PublishRequestCountEvent.Arn - MetricsServicePublishResponseTimePermission: - Type: AWS::Lambda::Permission - Properties: - Action: lambda:InvokeFunction - FunctionName: !GetAtt MetricsServicePublishResponseTime.Arn - Principal: events.amazonaws.com - SourceArn: !GetAtt PublishResponseTimeMetricsEvent.Arn - MetricsServiceAddAthenaPartitionPermission: - Type: AWS::Lambda::Permission - Properties: - Action: lambda:InvokeFunction - FunctionName: !GetAtt MetricsServiceAddAthenaPartition.Arn - Principal: events.amazonaws.com - SourceArn: !GetAtt AddAthenaPartitionEvent.Arn + SAAS_BOOST_ENV: !Ref Environment + METRICS_QUEUE: !Ref MetricsQueue + METRICS_DLQ: !Ref MetricsDLQ + +# AccessLogsDatabase: +# Type: AWS::Glue::Database +# Properties: +# CatalogId: !Ref AWS::AccountId +# DatabaseInput: +# Description: SaaS Boost Access Logs Database +# Name: !Sub sb_${Environment}_alb_access_logs +# AccessLogsTable: +# Type: AWS::Glue::Table +# Properties: +# CatalogId: !Ref AWS::AccountId +# DatabaseName: !Ref AccessLogsDatabase +# TableInput: +# Description: ALB Access Logs from Tenants +# Name: !Sub sb_${Environment}_access_logs +# Owner: saas-boost +# TableType: EXTERNAL_TABLE +# StorageDescriptor: +# Columns: +# - Name: type +# Type: STRING +# - Name: time +# Type: STRING +# - Name: elb +# Type: STRING +# - Name: client_ip +# Type: STRING +# - Name: client_port +# Type: INT +# - Name: target_ip +# Type: STRING +# - Name: target_port +# Type: INT +# - Name: request_processing_time +# Type: DOUBLE +# - Name: target_processing_time +# Type: DOUBLE +# - Name: response_processing_time +# Type: DOUBLE +# - Name: elb_status_code +# Type: STRING +# - Name: target_status_code +# Type: STRING +# - Name: received_bytes +# Type: BIGINT +# - Name: sent_bytes +# Type: BIGINT +# - Name: request_verb +# Type: STRING +# - Name: request_url +# Type: STRING +# - Name: request_proto +# Type: STRING +# - Name: user_agent +# Type: STRING +# - Name: ssl_cipher +# Type: STRING +# - Name: ssl_protocol +# Type: STRING +# - Name: target_group_arn +# Type: STRING +# - Name: trace_id +# Type: STRING +# - Name: domain_name +# Type: STRING +# - Name: chosen_cert_arn +# Type: STRING +# - Name: matched_rule_priority +# Type: STRING +# - Name: request_creation_time +# Type: STRING +# - Name: actions_executed +# Type: STRING +# - Name: redirect_url +# Type: STRING +# - Name: lambda_error_reason +# Type: STRING +# - Name: target_port_list +# Type: STRING +# - Name: target_status_code_list +# Type: STRING +# - Name: new_field +# Type: STRING +# Compressed: False +# Location: !Sub s3://${AccessLogs}/access-logs/AWSLogs/${AWS::AccountId}/elasticloadbalancing/${AWS::Region} +# InputFormat: org.apache.hadoop.mapred.TextInputFormat +# OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat +# SerdeInfo: +# SerializationLibrary: org.apache.hadoop.hive.serde2.RegexSerDe +# Parameters: +# serialization.format: 1 +# input.regex: '([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*):([0-9]*) ([^ ]*)[:-]([0-9]*) ([-.0-9]*) ([-.0-9]*) ([-.0-9]*) (|[-0-9]*) (-|[-0-9]*) ([-0-9]*) ([-0-9]*) \"([^ ]*) ([^ ]*) (- |[^ ]*)\" \"([^\"]*)\" ([A-Z0-9-]+) ([A-Za-z0-9.-]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^\"]*)\" ([-.0-9]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^ ]*)\" \"([^\s]+)\" \"([^\s]+)\"(.*)' +# MetricServiceExecutionRole: +# Type: AWS::IAM::Role +# Properties: +# RoleName: !Sub sb-${Environment}-metrics-svc-role-${AWS::Region} +# Path: '/' +# AssumeRolePolicyDocument: +# Version: 2012-10-17 +# Statement: +# - Effect: Allow +# Principal: +# Service: +# - lambda.amazonaws.com +# Action: +# - sts:AssumeRole +# Policies: +# - PolicyName: !Sub sb-${Environment}-metrics-svc-policy +# PolicyDocument: +# Version: 2012-10-17 +# Statement: +# - Effect: Allow +# Action: +# - logs:PutLogEvents +# Resource: +# - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* +# - Effect: Allow +# Action: +# - logs:CreateLogStream +# - logs:DescribeLogStreams +# Resource: +# - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* +# - Effect: Allow +# Action: +# - athena:ListDataCatalogs +# - athena:ListWorkGroups +# - cloudwatch:GetMetricData +# - cloudwatch:ListMetrics +# - application-autoscaling:DescribeScalableTargets +# Resource: '*' +# - Effect: Allow +# Action: +# - s3:GetBucketLocation +# - s3:ListBucket +# - s3:ListBucketMultipartUploads +# Resource: +# - !Sub arn:${AWS::Partition}:s3:::${AthenaOutput} +## - !Sub arn:${AWS::Partition}:s3:::${AccessLogs} +# - Effect: Allow +# Action: +# - s3:AbortMultipartUpload +# - s3:GetObject +# - s3:ListMultipartUploadParts +# - s3:PutObject +# Resource: +# - !Sub arn:${AWS::Partition}:s3:::${AthenaOutput}/* +## - !Sub arn:${AWS::Partition}:s3:::${AccessLogs}/* +# - Effect: Allow +# Action: +# - glue:GetTable +# - glue:GetDatabase +# - glue:GetPartitions +# Resource: +## - !Sub arn:${AWS::Partition}:glue:${AWS::Region}:${AWS::AccountId}:database/${AccessLogsDatabase} +## - !Sub arn:${AWS::Partition}:glue:${AWS::Region}:${AWS::AccountId}:table/${AccessLogsDatabase}/${AccessLogsTable} +# - !Sub arn:${AWS::Partition}:glue:${AWS::Region}:${AWS::AccountId}:catalog +# - !Sub arn:${AWS::Partition}:athena:${AWS::Region}:${AWS::AccountId}:workgroup/primary +# - Effect: Allow +# Action: +# - athena:* +# Resource: +# - !Sub arn:${AWS::Partition}:athena:${AWS::Region}:${AWS::AccountId}:workgroup/primary +# - Effect: Allow +# Action: +# - secretsmanager:GetSecretValue +# Resource: +# - !Sub arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:/saas-boost/${Environment}/PRIVATE_API_APP_CLIENT* +# MetricsServicePublishRequestCountLogs: +# Type: AWS::Logs::LogGroup +# Properties: +# LogGroupName: !Sub /aws/lambda/sb-${Environment}-metrics-pub-requests +# RetentionInDays: 30 +# MetricsServicePublishRequestCount: +# Type: AWS::Lambda::Function +# Properties: +# FunctionName: !Sub sb-${Environment}-metrics-pub-requests +# Role: !GetAtt MetricServiceExecutionRole.Arn +# Runtime: java21 +# Architectures: +# - arm64 +# Timeout: 900 +# MemorySize: 384 +# Handler: com.amazon.aws.partners.saasfactory.saasboost.MetricService::publishRequestCountMetrics +# Code: +# S3Bucket: !Ref SaaSBoostBucket +# S3Key: !Sub ${LambdaSourceFolder}/MetricsService-lambda.zip +# Layers: +# - !Ref SaaSBoostUtilsLayer +# Environment: +# Variables: +## ATHENA_DATABASE: !Ref AccessLogsDatabase +# S3_ATHENA_BUCKET: !Ref AthenaOutput +# S3_ATHENA_OUTPUT_PATH: !Sub s3://${AthenaOutput}/query-results/ +## ACCESS_LOGS_TABLE: !Ref AccessLogsTable +# Tags: +# - Key: "Application" +# Value: "SaaSBoost" +# - Key: "Environment" +# Value: !Ref Environment +# - Key: "BoostService" +# Value: "Metrics" +# MetricsServicePublishResponseTimeLogs: +# Type: AWS::Logs::LogGroup +# Properties: +# LogGroupName: !Sub /aws/lambda/sb-${Environment}-metrics-pub-response +# RetentionInDays: 30 +# MetricsServicePublishResponseTime: +# Type: AWS::Lambda::Function +# Properties: +# FunctionName: !Sub sb-${Environment}-metrics-pub-response +# Role: !GetAtt MetricServiceExecutionRole.Arn +# Runtime: java21 +# Architectures: +# - arm64 +# Timeout: 900 +# MemorySize: 384 +# Handler: com.amazon.aws.partners.saasfactory.saasboost.MetricService::publishResponseTimeMetrics +# Code: +# S3Bucket: !Ref SaaSBoostBucket +# S3Key: !Sub ${LambdaSourceFolder}/MetricsService-lambda.zip +# Layers: +# - !Ref SaaSBoostUtilsLayer +# Environment: +# Variables: +## ATHENA_DATABASE: !Ref AccessLogsDatabase +# S3_ATHENA_BUCKET: !Ref AthenaOutput +# S3_ATHENA_OUTPUT_PATH: !Sub s3://${AthenaOutput}/query-results/ +## ACCESS_LOGS_TABLE: !Ref AccessLogsTable +# Tags: +# - Key: "Application" +# Value: "SaaSBoost" +# - Key: "Environment" +# Value: !Ref Environment +# - Key: "BoostService" +# Value: "Metrics" +# MetricsServiceAddAthenaPartitionLogs: +# Type: AWS::Logs::LogGroup +# Properties: +# LogGroupName: !Sub /aws/lambda/sb-${Environment}-metrics-add-partition +# RetentionInDays: 30 +# MetricsServiceAddAthenaPartition: +# Type: AWS::Lambda::Function +# Properties: +# FunctionName: !Sub sb-${Environment}-metrics-add-partition +# Role: !GetAtt MetricServiceExecutionRole.Arn +# Runtime: java21 +# Architectures: +# - arm64 +# Timeout: 900 +# MemorySize: 384 +# Handler: com.amazon.aws.partners.saasfactory.saasboost.MetricService::addAthenaPartition +# Code: +# S3Bucket: !Ref SaaSBoostBucket +# S3Key: !Sub ${LambdaSourceFolder}/MetricsService-lambda.zip +# Layers: +# - !Ref SaaSBoostUtilsLayer +# Environment: +# Variables: +## ATHENA_DATABASE: !Ref AccessLogsDatabase +# S3_ATHENA_BUCKET: !Ref AthenaOutput +# S3_ATHENA_OUTPUT_PATH: !Sub s3://${AthenaOutput}/query-results/ +## ACCESS_LOGS_TABLE: !Ref AccessLogsTable +## ACCESS_LOGS_PATH: !Sub s3://${AccessLogs}/access-logs/AWSLogs/${AWS::AccountId}/elasticloadbalancing/${AWS::Region} +# Tags: +# - Key: "Application" +# Value: "SaaSBoost" +# - Key: "Environment" +# Value: !Ref Environment +# - Key: "BoostService" +# Value: "Metrics" +# MetricServiceQueryLogs: +# Type: AWS::Logs::LogGroup +# Properties: +# LogGroupName: !Sub /aws/lambda/sb-${Environment}-metrics-query +# RetentionInDays: 30 +# MetricServiceQuery: +# Type: AWS::Lambda::Function +# Properties: +# FunctionName: !Sub sb-${Environment}-metrics-query +# Role: !GetAtt MetricServiceExecutionRole.Arn +# Runtime: java21 +# Architectures: +# - arm64 +# Timeout: 300 +# MemorySize: 1024 +# Handler: com.amazon.aws.partners.saasfactory.saasboost.MetricService::queryMetrics +# Code: +# S3Bucket: !Ref SaaSBoostBucket +# S3Key: !Sub ${LambdaSourceFolder}/MetricsService-lambda.zip +# Layers: +# - !Ref SaaSBoostUtilsLayer +# - !Ref ApiGatewayHelperLayer +# Environment: +# Variables: +## ATHENA_DATABASE: !Ref AccessLogsDatabase +# S3_ATHENA_BUCKET: !Ref AthenaOutput +# S3_ATHENA_OUTPUT_PATH: !Sub s3://${AthenaOutput}/query-results/ +## ACCESS_LOGS_TABLE: !Ref AccessLogsTable +# API_APP_CLIENT: !Sub /saas-boost/${Environment}/PRIVATE_API_APP_CLIENT +# Tags: +# - Key: "Application" +# Value: "SaaSBoost" +# - Key: "Environment" +# Value: !Ref Environment +# - Key: "BoostService" +# Value: "Metrics" +# MetricsServiceGetDatasetsLog: +# Type: AWS::Logs::LogGroup +# Properties: +# LogGroupName: !Sub /aws/lambda/sb-${Environment}-metrics-datasets +# RetentionInDays: 30 +# MetricsServiceGetDatasets: +# Type: AWS::Lambda::Function +# Properties: +# FunctionName: !Sub sb-${Environment}-metrics-datasets +# Role: !GetAtt MetricServiceExecutionRole.Arn +# Runtime: java21 +# Architectures: +# - arm64 +# Timeout: 900 +# MemorySize: 384 +# Handler: com.amazon.aws.partners.saasfactory.saasboost.MetricService::getAccessMetricsSignedUrls +# Code: +# S3Bucket: !Ref SaaSBoostBucket +# S3Key: !Sub ${LambdaSourceFolder}/MetricsService-lambda.zip +# Layers: +# - !Ref SaaSBoostUtilsLayer +# Environment: +# Variables: +## ATHENA_DATABASE: !Ref AccessLogsDatabase +# S3_ATHENA_BUCKET: !Ref AthenaOutput +# S3_ATHENA_OUTPUT_PATH: !Sub s3://${AthenaOutput}/query-results/ +## ACCESS_LOGS_TABLE: !Ref AccessLogsTable +# Tags: +# - Key: "Application" +# Value: "SaaSBoost" +# - Key: "Environment" +# Value: !Ref Environment +# - Key: "BoostService" +# Value: "Metrics" +# MetricsServiceQueryAccessLogLogs: +# Type: AWS::Logs::LogGroup +# Properties: +# LogGroupName: !Sub /aws/lambda/sb-${Environment}-metrics-access-log-query +# RetentionInDays: 30 +# MetricsServiceQueryAccessLog: +# Type: AWS::Lambda::Function +# Properties: +# FunctionName: !Sub sb-${Environment}-metrics-access-log-query +# Role: !GetAtt MetricServiceExecutionRole.Arn +# Runtime: java21 +# Architectures: +# - arm64 +# Timeout: 300 +# MemorySize: 1024 +# Handler: com.amazon.aws.partners.saasfactory.saasboost.MetricService::queryAccessLogs +# Code: +# S3Bucket: !Ref SaaSBoostBucket +# S3Key: !Sub ${LambdaSourceFolder}/MetricsService-lambda.zip +# Layers: +# - !Ref SaaSBoostUtilsLayer +# - !Ref ApiGatewayHelperLayer +# Environment: +# Variables: +## ATHENA_DATABASE: !Ref AccessLogsDatabase +# S3_ATHENA_BUCKET: !Ref AthenaOutput +# S3_ATHENA_OUTPUT_PATH: !Sub s3://${AthenaOutput}/query-results/ +## ACCESS_LOGS_TABLE: !Ref AccessLogsTable +# API_APP_CLIENT: !Sub /saas-boost/${Environment}/PRIVATE_API_APP_CLIENT +# Tags: +# - Key: "Application" +# Value: "SaaSBoost" +# - Key: "Environment" +# Value: !Ref Environment +# - Key: "BoostService" +# Value: "Metrics" +# PublishRequestCountEvent: +# Type: AWS::Events::Rule +# Properties: +# Name: !Sub sb-${Environment}-metrics-count +# Description: A scheduled task to publish access log metrics to s3 web bucket +# # Run this every 30 minutes +# ScheduleExpression: "cron(0/30 * * * ? *)" +# State: DISABLED +# Targets: +# - Arn: !GetAtt MetricsServicePublishRequestCount.Arn +# Id: MetricsServicePublishRequestCount +# PublishResponseTimeMetricsEvent: +# Type: AWS::Events::Rule +# Properties: +# Name: !Sub sb-${Environment}-metrics-latency +# Description: A scheduled task to publish access log metrics to s3 web bucket +# # Run this every 30 minutes +# ScheduleExpression: "cron(0/30 * * * ? *)" +# State: DISABLED +# Targets: +# - Arn: !GetAtt MetricsServicePublishResponseTime.Arn +# Id: MetricsServicePublishResponseTime +# AddAthenaPartitionEvent: +# Type: AWS::Events::Rule +# Properties: +# Name: !Sub sb-${Environment}-metrics-athena-partition +# Description: A scheduled task to add partition for access logs daily +# # Run at 00:15 every day +# ScheduleExpression: "cron(15 0 * * ? *)" +# State: DISABLED +# Targets: +# - Arn: !GetAtt MetricsServiceAddAthenaPartition.Arn +# Id: MetricsServiceAddAthenaPartition +# MetricsServicePublishRequestCountPermission: +# Type: AWS::Lambda::Permission +# Properties: +# Action: lambda:InvokeFunction +# FunctionName: !GetAtt MetricsServicePublishRequestCount.Arn +# Principal: events.amazonaws.com +# SourceArn: !GetAtt PublishRequestCountEvent.Arn +# MetricsServicePublishResponseTimePermission: +# Type: AWS::Lambda::Permission +# Properties: +# Action: lambda:InvokeFunction +# FunctionName: !GetAtt MetricsServicePublishResponseTime.Arn +# Principal: events.amazonaws.com +# SourceArn: !GetAtt PublishResponseTimeMetricsEvent.Arn +# MetricsServiceAddAthenaPartitionPermission: +# Type: AWS::Lambda::Permission +# Properties: +# Action: lambda:InvokeFunction +# FunctionName: !GetAtt MetricsServiceAddAthenaPartition.Arn +# Principal: events.amazonaws.com +# SourceArn: !GetAtt AddAthenaPartitionEvent.Arn Outputs: - AccessLogsDatabase: - Description: Athena Database for ALB Access Logs - Value: !Ref AccessLogsDatabase - AccessLogsTable: - Description: Athena Access Logs Table - Value: !Ref AccessLogsTable - QueryArn: - Description: Metrics Service query Lambda ARN - Value: !GetAtt MetricServiceQuery.Arn - DatasetsArn: - Description: Metrics Service datasets Lambda ARN - Value: !GetAtt MetricsServiceGetDatasets.Arn - AlbQueryArn: - Description: Metrics Service ALB metric query Lambda ARN - Value: !GetAtt MetricsServiceQueryAccessLog.Arn + MetricsServicePutArn: + Description: Metrics Service put Lambda ARN + Value: !GetAtt MetricsServicePutMetrics.Arn +# AccessLogsDatabase: +# Description: Athena Database for ALB Access Logs +# Value: !Ref AccessLogsDatabase +# AccessLogsTable: +# Description: Athena Access Logs Table +# Value: !Ref AccessLogsTable +# QueryArn: +# Description: Metrics Service query Lambda ARN +# Value: !GetAtt MetricServiceQuery.Arn +# DatasetsArn: +# Description: Metrics Service datasets Lambda ARN +# Value: !GetAtt MetricsServiceGetDatasets.Arn +# AlbQueryArn: +# Description: Metrics Service ALB metric query Lambda ARN +# Value: !GetAtt MetricsServiceQueryAccessLog.Arn ... \ No newline at end of file diff --git a/resources/saas-boost-svc-onboarding.yaml b/resources/saas-boost-svc-onboarding.yaml index d7fa2861..cc10f0ae 100644 --- a/resources/saas-boost-svc-onboarding.yaml +++ b/resources/saas-boost-svc-onboarding.yaml @@ -36,12 +36,6 @@ Parameters: SaaSBoostEventBus: Description: SaaS Boost Eventbridge Bus Type: String - SaaSBoostPrivateApi: - Description: SaaS Boost Private API - Type: String - PrivateApiStage: - Description: The API Gateway REST API stage name for the SaaS Boost private API - Type: String ResourcesBucket: Description: S3 bucket containing tenant custom config files (zip archive) Type: String @@ -62,36 +56,6 @@ Resources: Tags: - Key: SaaS Boost Value: !Ref Environment - CidrBlockTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub sb-${Environment}-cidr-mapping - AttributeDefinitions: - - AttributeName: cidr_block - AttributeType: S - KeySchema: - - AttributeName: cidr_block - KeyType: HASH - ProvisionedThroughput: - ReadCapacityUnits: 5 - WriteCapacityUnits: 5 - Tags: - - Key: SaaS Boost - Value: !Ref Environment - OnboardingValidationQueue: - Type: AWS::SQS::Queue - Properties: - QueueName: !Sub sb-${Environment}-onboarding-validation - VisibilityTimeout: 60 # Must be greater than the Timeout setting on the Lambda - RedrivePolicy: - deadLetterTargetArn: !GetAtt OnboardingValidationDLQ.Arn - maxReceiveCount: 10 - SqsManagedSseEnabled: true - OnboardingValidationDLQ: - Type: AWS::SQS::Queue - Properties: - QueueName: !Sub sb-${Environment}-onboarding-validation-dlq - SqsManagedSseEnabled: true OnboardingTenantConfigQueue: Type: AWS::SQS::Queue Properties: @@ -153,70 +117,6 @@ Resources: Targets: - Arn: !GetAtt OnboardingTenantConfigQueue.Arn Id: !Sub sb-${Environment}-onboarding-tenant-config - PopulateDynamoDBExecRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub sb-${Environment}-populate-ddb-role-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: !Sub sb-${Environment}-populate-ddb-policy - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:CreateLogStream - - logs:DescribeLogStreams - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - dynamodb:Scan - - dynamodb:BatchWriteItem - Resource: - - !Sub arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${CidrBlockTable} - PopulateDynamoDBLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-populate-ddb - RetentionInDays: 30 - # Fills out the CIDR block lookup table used during onboarding - PopulateDynamoDB: - Type: AWS::Lambda::Function - Properties: - FunctionName: !Sub sb-${Environment}-populate-ddb - Role: !GetAtt PopulateDynamoDBExecRole.Arn - Runtime: java11 - Timeout: 60 - MemorySize: 512 - Handler: com.amazon.aws.partners.saasfactory.saasboost.CidrDynamoDB - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref CloudFormationUtilsLayer - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/CidrDynamoDB-lambda.zip - Environment: - Variables: - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - InvokePopulateDynamoDB: - Type: Custom::CustomResource - Properties: - ServiceToken: !GetAtt PopulateDynamoDB.Arn - Table: !Ref CidrBlockTable OnboardingServiceBasePolicy: Type: AWS::IAM::ManagedPolicy Properties: @@ -248,7 +148,6 @@ Resources: - dynamodb:UpdateItem Resource: - !Sub arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${OnboardingTable} - - !Sub arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${CidrBlockTable} - Effect: Allow Action: - s3:ListBucket @@ -268,15 +167,15 @@ Resources: - events:PutEvents Resource: - !Sub arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/${SaaSBoostEventBus} - - Effect: Allow - Action: - - events:DescribeRule - - events:DeleteRule - - events:PutRule - - events:PutTargets - - events:RemoveTargets - Resource: - - !Sub arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/* +# - Effect: Allow +# Action: +# - events:DescribeRule +# - events:DeleteRule +# - events:PutRule +# - events:PutTargets +# - events:RemoveTargets +# Resource: +# - !Sub arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/* - Effect: Allow Action: - sqs:SendMessage @@ -285,581 +184,46 @@ Resources: - sqs:GetQueueAttributes - sqs:ChangeMessageVisibility Resource: - - !GetAtt OnboardingValidationQueue.Arn - !GetAtt OnboardingTenantConfigQueue.Arn - Effect: Allow Action: - sqs:SendMessage Resource: - - !GetAtt OnboardingValidationDLQ.Arn - !GetAtt OnboardingTenantConfigDLQ.Arn - Effect: Allow Action: - - sts:AssumeRole - Resource: - - !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' - - Effect: Allow - Action: - - ecr:ListImages - - ecr:CreateRepository - - ecr:DeleteRepository - - ecr:TagResource - - ecr:UntagResource - - ecr:ListTagsForResource - Resource: - - !Sub arn:${AWS::Partition}:ecr:${AWS::Region}:${AWS::AccountId}:repository/* - - Effect: Allow - Action: - - cloudfront:GetDistribution - Resource: - - !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/* - - Effect: Allow - Action: - - apigateway:GET - Resource: - - !Sub arn:${AWS::Partition}:apigateway:*::/restapis/* - - Effect: Allow - Action: - - lambda:AddPermission - - lambda:RemovePermission - - lambda:InvokeFunction - - lambda:GetFunction - - lambda:GetFunctionConfiguration - Resource: - - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:*deploy* - - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:saas-boost-app-services-macro - - !Sub '{{resolve:ssm:/saas-boost/${Environment}/CLEAR_BUCKET_ARN}}' - OnboardingServiceTenantProvisionPolicy: - Type: AWS::IAM::ManagedPolicy - Properties: - Description: Onboarding Service policy for base tenant environment provisioning - Path: '/' - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - cloudformation:CreateStack - - cloudformation:UpdateStack - - cloudformation:CreateChangeSet - - cloudformation:DeleteStack - - cloudformation:DescribeStacks - - cloudformation:DescribeStackResource - Resource: - - !Sub arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/sb-${Environment}* - - !Sub arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:transform/saas-boost-app-services-macro - - Effect: Allow - Action: - - sns:Publish - Resource: - - !Sub arn:${AWS::Partition}:sns:${AWS::Region}:${AWS::AccountId}:sb-${Environment}-onboarding* - - !Sub arn:${AWS::Partition}:sns:${AWS::Region}:${AWS::AccountId}:sb-${Environment}-core-stack-listener - - Effect: Allow - Action: - - ssm:GetParameter* - - ssm:PutParameter - - ssm:DeleteParameter - Resource: - - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/saas-boost/${Environment}/* - - Effect: Allow - Action: - - secretsmanager:CreateSecret - - secretsmanager:DeleteSecret - secretsmanager:GetSecretValue Resource: - - !Sub arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:/saas-boost/${Environment}/* - - Effect: Allow - Action: - - ds:CreateMicrosoftAD - - ds:DescribeDirectories - - secretsmanager:GetRandomPassword - Resource: - - '*' - - Effect: Allow - Action: - - ssm:GetParameters - Resource: - - !Sub arn:${AWS::Partition}:ssm:*:*:parameter/aws/service/ami-windows-latest/* - - !Sub arn:${AWS::Partition}:ssm:*:*:parameter/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/* - - Effect: Allow - Action: - - ssm:AddTagsToResource - Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/* - - Effect: Allow - Action: - - ecs:DescribeClusters - - ecs:DeleteCluster - Resource: - - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/sb-${Environment}-tenant* - - Effect: Allow - Action: - - codepipeline:CreatePipeline - - codepipeline:DeletePipeline - - codepipeline:GetPipeline - - codepipeline:GetPipelineState - - codepipeline:UpdatePipeline - - codepipeline:TagResource - - codepipeline:UntagResource - - codepipeline:ListTagsForResource - Resource: - - !Sub arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:sb-${Environment}-tenant* - - Effect: Allow - Action: - - kms:TagResource - - kms:PutKeyPolicy - - kms:CreateAlias - - kms:UpdateAlias - - kms:DeleteAlias - - kms:UntagResource - - kms:DescribeKey - - kms:ScheduleKeyDeletion - - kms:Get* - Resource: - - !Sub arn:${AWS::Partition}:kms:*:${AWS::AccountId}:alias/* - - !Sub arn:${AWS::Partition}:kms:*:${AWS::AccountId}:key/* - - Effect: Allow - Action: - - route53:GetHostedZone - - route53:ChangeResourceRecordSets - - route53:ListResourceRecordSets - - route53:ChangeTagsForResource - - route53:ListQueryLoggingConfigs - - route53:DeleteHostedZone - Resource: - - !Sub arn:${AWS::Partition}:route53:::hostedzone/* - - Effect: Allow - Action: - - route53:GetChange - Resource: - - !Sub arn:${AWS::Partition}:route53:::change/* - - Effect: Allow - Action: - - route53:ListHostedZonesByName - - route53:CreateHostedZone - - ec2:CreateInternetGateway - - ec2:DeleteInternetGateway - - ec2:CreateVpc - - ec2:DeleteVpc - - ec2:ModifyVpcAttribute - - ec2:AttachInternetGateway - - ec2:DetachInternetGateway - - ec2:CreateSubnet - - ec2:DeleteSubnet - - ec2:CreateSecurityGroup - - ec2:DeleteSecurityGroup - - ec2:CreateRouteTable - - ec2:DeleteRouteTable - - ec2:AssociateRouteTable - - ec2:DisassociateRouteTable - - ec2:Describe* - - ec2:CreateNetworkInterface - - ec2:DeleteNetworkInterface - - elasticloadbalancing:Describe* - - elasticloadbalancing:AddTags - - elasticloadbalancing:RemoveTags - - kms:List* - - kms:CreateKey - Resource: - - '*' - OnboardingServiceEC2ComputeProvisionPolicy: - Type: AWS::IAM::ManagedPolicy - Properties: - Description: Onboarding Service policy for tenant environment compute provisioning - Path: '/' - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - ec2:CreateTags - - ec2:DeleteTags - Resource: - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/* - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:launch-template/* - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:internet-gateway/* - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:vpc/* - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:route-table/* - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:subnet/* - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:transit-gateway/* - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:transit-gateway-attachment/* - - Effect: Allow - Action: - - ec2:CreateLaunchTemplate - - ec2:DeleteLaunchTemplate - Resource: - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:launch-template/* - - Effect: Allow - Action: - - ec2:AuthorizeSecurityGroupIngress - - ec2:RevokeSecurityGroupIngress - - ec2:AuthorizeSecurityGroupEgress - - ec2:RevokeSecurityGroupEgress - Resource: - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:security-group/* - - Effect: Allow - Action: - - ec2:CreateRoute - - ec2:DeleteRoute - Resource: - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:route-table/* - - Effect: Allow - Action: - - ec2:CreateTransitGatewayVpcAttachment - - ec2:DeleteTransitGatewayVpcAttachment - Resource: - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:vpc/* - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:subnet/* - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:transit-gateway/* - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:transit-gateway-attachment/* - - Effect: Allow - Action: - - ec2:CreateTransitGatewayRoute - - ec2:DeleteTransitGatewayRoute - - ec2:AssociateTransitGatewayRouteTable - - ec2:DisassociateTransitGatewayRouteTable - - ec2:SearchTransitGatewayRoutes - Resource: - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:transit-gateway-route-table/* - - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:transit-gateway-attachment/* - OnboardingServiceComputeProvisionPolicy: - Type: AWS::IAM::ManagedPolicy - Properties: - Description: Onboarding Service policy for tenant environment compute provisioning - Path: '/' - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:CreateLogGroup - - logs:PutRetentionPolicy - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/ecs/sb-${Environment}-tenant* - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/sb-${Environment}-tenant* - - Effect: Allow - Action: - - elasticloadbalancing:CreateTargetGroup - - elasticloadbalancing:ModifyTargetGroupAttributes - - elasticloadbalancing:DeleteTargetGroup - Resource: - - !Sub arn:${AWS::Partition}:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:targetgroup/*/* - - Effect: Allow - Action: - - elasticloadbalancing:CreateLoadBalancer - - elasticloadbalancing:DeleteLoadBalancer - - elasticloadbalancing:ModifyLoadBalancerAttributes - - elasticloadbalancing:CreateListener - Resource: - - !Sub arn:${AWS::Partition}:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:loadbalancer/app/*/* - - !Sub arn:${AWS::Partition}:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:loadbalancer/net/*/* - - Effect: Allow - Action: - - elasticloadbalancing:CreateRule - - elasticloadbalancing:ModifyRule - - elasticloadbalancing:SetRulePriorities - - elasticloadbalancing:DeleteRule - - elasticloadbalancing:DeleteListener - Resource: - - !Sub arn:${AWS::Partition}:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:listener/app/*/* - - !Sub arn:${AWS::Partition}:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:listener-rule/app/* - - !Sub arn:${AWS::Partition}:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:listener/net/*/* - - !Sub arn:${AWS::Partition}:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:listener-rule/net/* - - Effect: Allow - Action: - - iam:CreateServiceLinkedRole - - iam:DeleteServiceLinkedRole - Resource: - - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService - Condition: - StringLike: - iam:AWSServiceName: - - ecs.application-autoscaling.amazonaws.com - - Effect: Allow - Action: - - iam:PassRole - Resource: - - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService - - Effect: Allow - Action: - - iam:GetRole - - iam:CreateRole - - iam:DeleteRole - - iam:AttachRolePolicy - - iam:DetachRolePolicy - - iam:GetRolePolicy - - iam:PutRolePolicy - - iam:DeleteRolePolicy - - iam:PassRole - Resource: - - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/sb-${Environment}-*tenant* - - Effect: Allow - Action: - - iam:GetInstanceProfile - - iam:CreateInstanceProfile - - iam:DeleteInstanceProfile - - iam:AddRoleToInstanceProfile - - iam:RemoveRoleFromInstanceProfile - Resource: - - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:instance-profile/sb-${Environment}-tenant* - - Effect: Allow - Action: - - ecs:PutClusterCapacityProviders - - ecs:DescribeCapacityProviders - - ecs:DeleteCapacityProvider - Resource: - - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:capacity-provider/sb-${Environment}-tenant* - - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/sb-${Environment}-tenant* - - Effect: Allow - Action: - - ecs:CreateService - - ecs:DescribeServices - - ecs:UpdateService - - ecs:DeleteService - Resource: - - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:service/sb-${Environment}-tenant* - - Effect: Allow - Action: - - servicediscovery:CreateService - - servicediscovery:DeleteNamespace - Resource: - - !Sub arn:${AWS::Partition}:servicediscovery:${AWS::Region}:${AWS::AccountId}:namespace/* - - Effect: Allow - Action: - - servicediscovery:GetService - - servicediscovery:DeleteService - Resource: - - !Sub arn:${AWS::Partition}:servicediscovery:${AWS::Region}:${AWS::AccountId}:service/* - - Effect: Allow - Action: - - autoscaling:CreateAutoScalingGroup - - autoscaling:DeleteAutoScalingGroup - - autoscaling:UpdateAutoScalingGroup - - autoscaling:SuspendProcesses - - autoscaling:ResumeProcesses - - autoscaling:CreateOrUpdateTags - - autoscaling:DeleteTags - Resource: - - !Sub arn:${AWS::Partition}:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/sb-${Environment}-tenant* - - Effect: Allow - Action: - - lambda:InvokeFunction - Resource: - - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:sb-${Environment}-set-instance-protection - - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:sb-${Environment}-attach-capacity-provider - - Effect: Allow - Action: - - servicediscovery:CreatePrivateDnsNamespace - - servicediscovery:GetOperation - - application-autoscaling:Describe* - - application-autoscaling:RegisterScalableTarget - - application-autoscaling:DeregisterScalableTarget - - application-autoscaling:DeleteScalingPolicy - - application-autoscaling:PutScalingPolicy - - autoscaling:Describe* - - ecs:CreateCluster - - ecs:CreateCapacityProvider - - ecs:DescribeTaskDefinition - - ecs:RegisterTaskDefinition - - ecs:DeregisterTaskDefinition - - ecs:TagResource - - ecs:UntagResource - - ec2:RunInstances - Resource: - - '*' - OnboardingServiceDatabaseProvisionPolicy: - Type: AWS::IAM::ManagedPolicy - Properties: - Description: Onboarding Service policy for tenant environment database provisioning - Path: '/' - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:CreateLogGroup - - logs:PutRetentionPolicy - - logs:DeleteLogGroup - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - rds:AddTagsToResource - - rds:RemoveTagsFromResource - - rds:CreateDBCluster - - rds:CreateDBInstance - Resource: - - !Sub arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:cluster:sb-${Environment}-*tenant* - - !Sub arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:cluster-pg:* - - !Sub arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:subgrp:sb-${Environment}-*tenant* - - !Sub arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:db:* - - Effect: Allow - Action: - - rds:CreateDBSnapshot - - rds:CreateDBClusterSnapshot - - rds:DescribeDBClusterSnapshots - Resource: - - !Sub arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:db:* - - !Sub arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:cluster:sb-${Environment}-*tenant* - - !Sub arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:cluster-snapshot:* - - Effect: Allow - Action: - - rds:DescribeDBClusters - - rds:DeleteDBCluster - Resource: - - !Sub arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:cluster:sb-${Environment}-*tenant* - - !Sub arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:cluster-snapshot:* - - Effect: Allow - Action: - - rds:DescribeDBSubnetGroups - - rds:CreateDBSubnetGroup - - rds:DeleteDBSubnetGroup - - rds:ListTagsForResource - Resource: - - !Sub arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:subgrp:sb-${Environment}-*tenant* - - Effect: Allow - Action: - - rds:DescribeDBInstances - - rds:DeleteDBInstance - Resource: - - !Sub arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:db:* - - Effect: Allow - Action: - - iam:GetRole - - iam:CreateRole - - iam:DeleteRole - - iam:GetRolePolicy - - iam:PutRolePolicy - - iam:DeleteRolePolicy - - iam:PassRole - Resource: - - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/sb-${Environment}-rds-bootstrap-tenant* - - Effect: Allow - Action: - - lambda:GetFunction - - lambda:CreateFunction - - lambda:InvokeFunction - - lambda:DeleteFunction - - lambda:TagResource - - lambda:UntagResource - Resource: - - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:sb-${Environment}-rds-bootstrap-tenant-* - - Effect: Allow - Action: - - lambda:GetLayerVersion - Resource: - - !Ref SaaSBoostUtilsLayer - - !Ref CloudFormationUtilsLayer - OnboardingServiceStorageProvisionPolicy: - Type: AWS::IAM::ManagedPolicy - Properties: - Description: Onboarding Service policy for tenant environment storage provisioning - Path: '/' - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - elasticfilesystem:Describe* - - elasticfilesystem:CreateMountTarget - - elasticfilesystem:DeleteMountTarget - - elasticfilesystem:CreateTags - - elasticfilesystem:TagResource - - elasticfilesystem:PutFileSystemPolicy - - elasticfilesystem:DeleteFileSystemPolicy - - elasticfilesystem:PutLifecycleConfiguration - Resource: - - !Sub arn:${AWS::Partition}:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:file-system/* - - Effect: Allow - Action: - - fsx:CreateFileSystem - - fsx:DeleteFileSystem - - fsx:TagResource - - fsx:UntagResource - Resource: - - !Sub arn:${AWS::Partition}:fsx:${AWS::Region}:${AWS::AccountId}:file-system/* - - Effect: Allow - Action: - - fsx:CreateStorageVirtualMachine - Resource: - - !Sub arn:${AWS::Partition}:fsx:${AWS::Region}:${AWS::AccountId}:storage-virtual-machine/* - - !Sub arn:${AWS::Partition}:fsx:${AWS::Region}:${AWS::AccountId}:file-system/* - - Effect: Allow - Action: - - fsx:TagResource - - fsx:UntagResource - - fsx:DeleteStorageVirtualMachine - Resource: - - !Sub arn:${AWS::Partition}:fsx:${AWS::Region}:${AWS::AccountId}:storage-virtual-machine/* - - Effect: Allow - Action: - - fsx:TagResource - - fsx:UntagResource - - fsx:CreateVolume - - fsx:DeleteVolume - Resource: - - !Sub arn:${AWS::Partition}:fsx:${AWS::Region}:${AWS::AccountId}:volume/*/* - - !Sub arn:${AWS::Partition}:fsx:${AWS::Region}:${AWS::AccountId}:storage-virtual-machine/*/* - - Effect: Allow - Action: - - elasticfilesystem:CreateFileSystem - - elasticfilesystem:DeleteFileSystem - - ds:DescribeDirectories - - fsx:DescribeFileSystems - - fsx:DescribeStorageVirtualMachines - - fsx:DescribeVolumes - Resource: - - '*' - - Effect: Allow - Action: - - iam:GetRole - - iam:CreateRole - - iam:DeleteRole - - iam:GetRolePolicy - - iam:PutRolePolicy - - iam:DeleteRolePolicy - - iam:PassRole - Resource: - - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/sb-${Environment}-fsx-dns-tenant* - - Effect: Allow - Action: - - lambda:GetFunction - - lambda:CreateFunction - - lambda:InvokeFunction - - lambda:DeleteFunction - - lambda:TagResource - - lambda:UntagResource - Resource: - - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:sb-${Environment}-fsx-dns-tenant-* - - Effect: Allow - Action: - - lambda:GetLayerVersion - Resource: - - !Ref SaaSBoostUtilsLayer - - !Ref CloudFormationUtilsLayer - - Effect: Allow - Action: - # This is for the AD stack -- need to figure out why update DNS wants to delete its SSM params - - ssm:DeleteParameter - Resource: - - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/saas-boost/${Environment}/* - # This is for the AD stack -- need to figure out why update DNS wants to delete the directory - - Effect: Allow - Action: - - ds:DeleteDirectory - Resource: - - !Sub arn:${AWS::Partition}:ds:${AWS::Region}:${AWS::AccountId}:directory/* - # Covers the creation/deletion of S3Extension bucket - - Effect: Allow - Action: - - s3:CreateBucket - - s3:DeleteBucket - - s3:PutBucketLogging - - s3:PutBucketOwnershipControls - - s3:PutBucketPublicAccessBlock - - s3:PutBucketTagging - - s3:PutEncryptionConfiguration - Resource: - - !Sub arn:${AWS::Partition}:s3:::sb-${Environment}-core-* + - !Sub arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:/saas-boost/${Environment}/PRIVATE_API_APP_CLIENT* +# - Effect: Allow +# Action: +# - ecr:ListImages +# - ecr:CreateRepository +# - ecr:DeleteRepository +# - ecr:TagResource +# - ecr:UntagResource +# - ecr:ListTagsForResource +# Resource: +# - !Sub arn:${AWS::Partition}:ecr:${AWS::Region}:${AWS::AccountId}:repository/* +# - Effect: Allow +# Action: +# - cloudfront:GetDistribution +# Resource: +# - !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/* +# - Effect: Allow +# Action: +# - apigateway:GET +# Resource: +# - !Sub arn:${AWS::Partition}:apigateway:*::/restapis/* +# - Effect: Allow +# Action: +# - lambda:AddPermission +# - lambda:RemovePermission +# - lambda:InvokeFunction +# - lambda:GetFunction +# - lambda:GetFunctionConfiguration +# Resource: +# - !Sub '{{resolve:ssm:/saas-boost/${Environment}/CLEAR_BUCKET_ARN}}' OnboardingServiceExecutionRole: Type: AWS::IAM::Role Properties: @@ -876,11 +240,6 @@ Resources: - sts:AssumeRole ManagedPolicyArns: - !Ref OnboardingServiceBasePolicy - - !Ref OnboardingServiceTenantProvisionPolicy - - !Ref OnboardingServiceComputeProvisionPolicy - - !Ref OnboardingServiceEC2ComputeProvisionPolicy - - !Ref OnboardingServiceDatabaseProvisionPolicy - - !Ref OnboardingServiceStorageProvisionPolicy OnboardingServiceGetAllLogs: Type: AWS::Logs::LogGroup Properties: @@ -891,7 +250,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-onboarding-get-all Role: !GetAtt OnboardingServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.OnboardingService::getOnboardings @@ -903,7 +264,6 @@ Resources: Environment: Variables: ONBOARDING_TABLE: !Ref OnboardingTable - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: "Application" Value: "SaaSBoost" @@ -921,7 +281,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-onboarding-get-by-id Role: !GetAtt OnboardingServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.OnboardingService::getOnboarding @@ -933,7 +295,6 @@ Resources: Environment: Variables: ONBOARDING_TABLE: !Ref OnboardingTable - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: "Application" Value: "SaaSBoost" @@ -941,17 +302,19 @@ Resources: Value: !Ref Environment - Key: "BoostService" Value: "Onboarding" - OnboardingServiceStartLogs: + OnboardingServiceInsertLogs: Type: AWS::Logs::LogGroup Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-onboarding-start + LogGroupName: !Sub /aws/lambda/sb-${Environment}-onboarding-insert RetentionInDays: 30 - OnboardingServiceStart: + OnboardingServiceInsert: Type: AWS::Lambda::Function Properties: - FunctionName: !Sub sb-${Environment}-onboarding-start + FunctionName: !Sub sb-${Environment}-onboarding-insert Role: !GetAtt OnboardingServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.OnboardingService::insertOnboarding @@ -967,7 +330,6 @@ Resources: SAAS_BOOST_BUCKET: !Ref SaaSBoostBucket SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus RESOURCES_BUCKET: !Ref ResourcesBucket - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: "Application" Value: "SaaSBoost" @@ -985,7 +347,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-onboarding-update Role: !GetAtt OnboardingServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.OnboardingService::updateOnboarding @@ -1000,7 +364,6 @@ Resources: ONBOARDING_TABLE: !Ref OnboardingTable SAAS_BOOST_BUCKET: !Ref SaaSBoostBucket SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: Application Value: SaaSBoost @@ -1016,7 +379,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-onboarding-delete Role: !GetAtt OnboardingServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.OnboardingService::deleteOnboarding @@ -1031,7 +396,6 @@ Resources: ONBOARDING_TABLE: !Ref OnboardingTable SAAS_BOOST_BUCKET: !Ref SaaSBoostBucket SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: Application Value: SaaSBoost @@ -1047,8 +411,10 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-onboarding-events Role: !GetAtt OnboardingServiceExecutionRole.Arn - Runtime: java11 - Timeout: 45 + Runtime: java21 + Architectures: + - arm64 + Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.OnboardingService::handleOnboardingEvent Code: @@ -1061,16 +427,9 @@ Resources: Variables: SAAS_BOOST_ENV: !Ref Environment ONBOARDING_TABLE: !Ref OnboardingTable - CIDR_BLOCK_TABLE: !Ref CidrBlockTable - API_TRUST_ROLE: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' - API_GATEWAY_HOST: !Sub ${SaaSBoostPrivateApi}.execute-api.${AWS::Region}.${AWS::URLSuffix} - API_GATEWAY_STAGE: !Ref PrivateApiStage + API_APP_CLIENT: !Sub /saas-boost/${Environment}/PRIVATE_API_APP_CLIENT SAAS_BOOST_BUCKET: !Ref SaaSBoostBucket SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus - ONBOARDING_STACK_SNS: !Sub '{{resolve:ssm:/saas-boost/${Environment}/ONBOARDING_STACK_SNS}}' - ONBOARDING_APP_STACK_SNS: !Sub '{{resolve:ssm:/saas-boost/${Environment}/ONBOARDING_APP_STACK_SNS}}' - ONBOARDING_VALIDATION_QUEUE: !Ref OnboardingValidationQueue - ONBOARDING_VALIDATION_DLQ: !Ref OnboardingValidationDLQ RESOURCES_BUCKET: !Ref ResourcesBucket TENANT_CONFIG_DLQ: !Ref OnboardingTenantConfigDLQ Tags: @@ -1078,52 +437,6 @@ Resources: Value: SaaSBoost - Key: Environment Value: !Ref Environment - OnboardingServiceValidationLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-onboarding-validation - RetentionInDays: 30 - OnboardingServiceValidation: - Type: AWS::Lambda::Function - Properties: - FunctionName: !Sub sb-${Environment}-onboarding-validation - Role: !GetAtt OnboardingServiceExecutionRole.Arn - Runtime: java11 - Timeout: 59 # Must be less than the VisibilityTimeout setting on the queue - MemorySize: 512 - Handler: com.amazon.aws.partners.saasfactory.saasboost.OnboardingService::processValidateOnboardingQueue - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/OnboardingService-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref ApiGatewayHelperLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - ONBOARDING_TABLE: !Ref OnboardingTable - CIDR_BLOCK_TABLE: !Ref CidrBlockTable - API_TRUST_ROLE: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' - API_GATEWAY_HOST: !Sub ${SaaSBoostPrivateApi}.execute-api.${AWS::Region}.${AWS::URLSuffix} - API_GATEWAY_STAGE: !Ref PrivateApiStage - SAAS_BOOST_BUCKET: !Ref SaaSBoostBucket - SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus - ONBOARDING_VALIDATION_DLQ: !Ref OnboardingValidationDLQ - Tags: - - Key: Application - Value: SaaSBoost - - Key: Environment - Value: !Ref Environment - ValidationEventMapping: - Type: AWS::Lambda::EventSourceMapping - Properties: - BatchSize: 10 - MaximumBatchingWindowInSeconds: 0 - Enabled: true - EventSourceArn: !GetAtt OnboardingValidationQueue.Arn - FunctionName: !GetAtt OnboardingServiceValidation.Arn - FunctionResponseTypes: - - ReportBatchItemFailures OnboardingTenantConfigLogs: Type: AWS::Logs::LogGroup Properties: @@ -1134,7 +447,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-onboarding-tenant-config-event Role: !GetAtt OnboardingServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 89 # Must be less than the VisibilityTimeout setting on the queue MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.OnboardingService::processTenantConfigQueue @@ -1148,10 +463,7 @@ Resources: Variables: SAAS_BOOST_ENV: !Ref Environment ONBOARDING_TABLE: !Ref OnboardingTable - CIDR_BLOCK_TABLE: !Ref CidrBlockTable - API_TRUST_ROLE: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' - API_GATEWAY_HOST: !Sub ${SaaSBoostPrivateApi}.execute-api.${AWS::Region}.${AWS::URLSuffix} - API_GATEWAY_STAGE: !Ref PrivateApiStage + API_APP_CLIENT: !Sub /saas-boost/${Environment}/PRIVATE_API_APP_CLIENT SAAS_BOOST_BUCKET: !Ref SaaSBoostBucket SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus TENANT_CONFIG_DLQ: !Ref OnboardingTenantConfigDLQ @@ -1171,6 +483,7 @@ Resources: FunctionName: !GetAtt OnboardingTenantConfig.Arn FunctionResponseTypes: - ReportBatchItemFailures + # Onboarding events _consumed_ by the control plane onboarding service OnboardingServiceEventRule: Type: AWS::Events::Rule Properties: @@ -1183,11 +496,12 @@ Resources: "saas-boost" ], "detail-type": [ - { - "prefix": "Onboarding " - }, - "Application Configuration Changed", - "Application Configuration Update Completed", + "Onboarding Validated", + "Onboarding Provisioning", + "Onboarding Provisioned", + "Onboarding Deploying", + "Onboarding Deployed", + "Onboarding Failed", "Tenant Deleted", "Tenant Enabled", "Tenant Disabled" @@ -1204,42 +518,13 @@ Resources: FunctionName: !Ref OnboardingServiceEventHandler Principal: events.amazonaws.com SourceArn: !GetAtt OnboardingServiceEventRule.Arn - # CodePipeline sends its events to the default EventBridge bus, so we need a 2nd rule to match it - OnboardingDeploymentPipelineEventRule: - Type: AWS::Events::Rule - Properties: - Name: !Sub sb-${Environment}-codepipeline-state - Description: CodePipeline state changes for SaaS Boost onboarding deployment events - EventPattern: # Skip STOPPING events - { - "source": [ - "aws.codepipeline" - ], - "detail-type": [ - "CodePipeline Pipeline Execution State Change" - ], - "detail": { - "state": ["STARTED", "SUCCEEDED", "FAILED"] - } - } - State: ENABLED - Targets: - - Arn: !GetAtt OnboardingServiceEventHandler.Arn - Id: !Sub sb-${Environment}-codepipeline-state - OnboardingDeploymentPipelineEventPermission: - Type: AWS::Lambda::Permission - Properties: - Action: lambda:InvokeFunction - FunctionName: !Ref OnboardingServiceEventHandler - Principal: events.amazonaws.com - SourceArn: !GetAtt OnboardingDeploymentPipelineEventRule.Arn Outputs: OnboardingServiceGetAllArn: Description: Onboarding Service get all onboarding requests Lambda ARN Value: !GetAtt OnboardingServiceGetAll.Arn - OnboardingServiceStartArn: - Description: Onboarding Service start onboarding Lambda ARN - Value: !GetAtt OnboardingServiceStart.Arn + OnboardingServiceInsertArn: + Description: Onboarding Service insert onboarding request Lambda ARN + Value: !GetAtt OnboardingServiceInsert.Arn OnboardingServiceByIdArn: Description: Onboarding Service get onboarding request by id Lambda ARN Value: !GetAtt OnboardingServiceGetById.Arn diff --git a/resources/saas-boost-svc-quota.yaml b/resources/saas-boost-svc-quota.yaml deleted file mode 100644 index eeed3f0b..00000000 --- a/resources/saas-boost-svc-quota.yaml +++ /dev/null @@ -1,115 +0,0 @@ ---- -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -AWSTemplateFormatVersion: 2010-09-09 -Description: AWS SaaS Boost Quota Service -Parameters: - Environment: - Description: Environment name - Type: String - SaaSBoostBucket: - Description: SaaS Boost assets S3 bucket - Type: String - LambdaSourceFolder: - Description: Folder for lambda source code to change on each deployment - Type: String - SaaSBoostUtilsLayer: - Description: Utils Layer ARN - Type: String - ApiGatewayHelperLayer: - Description: API Gateway Helper Layer ARN - Type: String -Resources: - QuotasServiceExecutionRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub sb-${Environment}-quotas-service-role-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: !Sub sb-${Environment}-quotas-service-policy - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:CreateLogStream - - logs:DescribeLogStreams - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - ec2:DescribeInternetGateways - - ec2:DescribeNatGateways - - ec2:DescribeVpcs - - ec2:DescribeAccountAttributes - - ec2:DescribeInstances - - elasticloadbalancing:DescribeLoadBalancers - - elasticloadbalancing:DescribeAccountLimits - - rds:DescribeDBInstances - - rds:DescribeDBClusters - - rds:DescribeAccountAttributes - - servicequotas:ListServiceQuotas - Resource: '*' - - Effect: Allow - Action: - - cloudwatch:GetMetricData - Resource: '*' - QuotasServiceCheckLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-quotas-check - RetentionInDays: 30 - QuotasServiceCheckFunction: - Type: AWS::Lambda::Function - Properties: - FunctionName: !Sub sb-${Environment}-quotas-check - Role: !GetAtt QuotasServiceExecutionRole.Arn - Runtime: java11 - Timeout: 300 - MemorySize: 512 - Handler: com.amazon.aws.partners.saasfactory.saasboost.QuotasService::checkQuotas - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/QuotasService-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "Onboarding" -Outputs: - QuotasServiceCheckArn: - Description: Quota Service check limits Lambda ARN - Value: !GetAtt QuotasServiceCheckFunction.Arn -... \ No newline at end of file diff --git a/resources/saas-boost-svc-settings.yaml b/resources/saas-boost-svc-settings.yaml index a43b88ef..0c95d0fd 100644 --- a/resources/saas-boost-svc-settings.yaml +++ b/resources/saas-boost-svc-settings.yaml @@ -27,121 +27,7 @@ Parameters: SaaSBoostUtilsLayer: Description: Utils Layer ARN Type: String - ApiGatewayHelperLayer: - Description: API Gateway Helper Layer ARN - Type: String - CloudFormationUtilsLayer: - Description: CloudFormation Utils Layer ARN - Type: String - SaaSBoostEventBus: - Description: SaaS Boost Eventbridge Bus - Type: String - SaaSBoostPrivateApi: - Description: SaaS Boost Private API - Type: String - PrivateApiStage: - Description: The API Gateway REST API stage name for the SaaS Boost private API - Type: String - ResourcesBucket: - Description: S3 bucket containing tenant custom config files (zip archive) - Type: String Resources: - RdsOptionsTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub sb-${Environment}-rds-options - AttributeDefinitions: - - AttributeName: region - AttributeType: S - - AttributeName: engine - AttributeType: S - KeySchema: - - AttributeName: region - KeyType: HASH - - AttributeName: engine - KeyType: RANGE - ProvisionedThroughput: - ReadCapacityUnits: 5 - WriteCapacityUnits: 5 - RdsOptionsExecutionRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub sb-${Environment}-rds-options-role-${AWS::Region} - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: !Sub sb-${Environment}-rds-options-policy - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:DescribeLogGroups - - logs:DescribeLogStreams - - logs:CreateLogStream - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - dynamodb:DescribeTable - - dynamodb:PutItem - - dynamodb:Scan - - dynamodb:UpdateItem - Resource: - - !Sub arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${RdsOptionsTable} - - Effect: Allow - Action: - - rds:DescribeOrderableDBInstanceOptions - - rds:DescribeDBEngineVersions - Resource: '*' - RdsOptionsLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-rds-options - RetentionInDays: 30 - RdsOptions: - Type: AWS::Lambda::Function - Properties: - FunctionName: !Sub sb-${Environment}-rds-options - Role: !GetAtt RdsOptionsExecutionRole.Arn - Runtime: java11 - Timeout: 720 - MemorySize: 768 - Handler: com.amazon.aws.partners.saasfactory.saasboost.RdsOptions - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref CloudFormationUtilsLayer - Environment: - Variables: - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/RdsOptions-lambda.zip - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "Settings" - InvokeRdsOptions: - Type: Custom::CustomResource - Properties: - ServiceToken: !GetAtt RdsOptions.Arn - Table: !Ref RdsOptionsTable SettingsServiceExecutionRole: Type: AWS::IAM::Role Properties: @@ -181,36 +67,6 @@ Resources: - ssm:DeleteParameters Resource: - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/* - - Effect: Allow - Action: - - dynamodb:DescribeTable - - dynamodb:GetItem - - dynamodb:Scan - - dynamodb:Query - Resource: - - !Sub arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${RdsOptionsTable} - - Effect: Allow - Action: - - events:PutEvents - Resource: - - !Sub arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/${SaaSBoostEventBus} - - Effect: Allow - Action: - - s3:PutObject - Resource: - - !Sub arn:${AWS::Partition}:s3:::${ResourcesBucket}/* - - Effect: Allow - Action: - - sts:AssumeRole - Resource: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' - - Effect: Allow - Action: - - acm:ListCertificates - Resource: '*' - - Effect: Allow - Action: - - route53:ListHostedZones - Resource: '*' SettingsServiceGetAllLogs: Type: AWS::Logs::LogGroup Properties: @@ -221,9 +77,11 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-settings-get-all Role: !GetAtt SettingsServiceExecutionRole.Arn - Runtime: java11 - Timeout: 300 - MemorySize: 1024 + Runtime: java21 + Architectures: + - arm64 + Timeout: 30 + MemorySize: 2112 # AWS Lambda Power Tuning balanced strategy Handler: com.amazon.aws.partners.saasfactory.saasboost.SettingsService::getSettings Code: S3Bucket: !Ref SaaSBoostBucket @@ -233,14 +91,13 @@ Resources: Environment: Variables: SAAS_BOOST_ENV: !Ref Environment - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" + - Key: Application + Value: SaaSBoost + - Key: Environment Value: !Ref Environment - - Key: "BoostService" - Value: "Settings" + - Key: BoostService + Value: Settings SettingsServiceGetByIdLogs: Type: AWS::Logs::LogGroup Properties: @@ -251,9 +108,11 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-settings-get-by-id Role: !GetAtt SettingsServiceExecutionRole.Arn - Runtime: java11 - Timeout: 300 - MemorySize: 512 + Runtime: java21 + Architectures: + - arm64 + Timeout: 30 + MemorySize: 2112 # AWS Lambda Power Tuning balanced strategy Handler: com.amazon.aws.partners.saasfactory.saasboost.SettingsService::getSetting Code: S3Bucket: !Ref SaaSBoostBucket @@ -263,14 +122,13 @@ Resources: Environment: Variables: SAAS_BOOST_ENV: !Ref Environment - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" + - Key: Application + Value: SaaSBoost + - Key: Environment Value: !Ref Environment - - Key: "BoostService" - Value: "Settings" + - Key: BoostService + Value: Settings SettingsServiceGetSecretLogs: Type: AWS::Logs::LogGroup Properties: @@ -281,9 +139,11 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-settings-get-secret Role: !GetAtt SettingsServiceExecutionRole.Arn - Runtime: java11 - Timeout: 300 - MemorySize: 512 + Runtime: java21 + Architectures: + - arm64 + Timeout: 30 + MemorySize: 2112 # AWS Lambda Power Tuning balanced strategy Handler: com.amazon.aws.partners.saasfactory.saasboost.SettingsService::getSecret Code: S3Bucket: !Ref SaaSBoostBucket @@ -293,14 +153,13 @@ Resources: Environment: Variables: SAAS_BOOST_ENV: !Ref Environment - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" + - Key: Application + Value: SaaSBoost + - Key: Environment Value: !Ref Environment - - Key: "BoostService" - Value: "Settings" + - Key: BoostService + Value: Settings SettingsServiceUpdateLogs: Type: AWS::Logs::LogGroup Properties: @@ -311,9 +170,11 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-settings-update Role: !GetAtt SettingsServiceExecutionRole.Arn - Runtime: java11 - Timeout: 300 - MemorySize: 512 + Runtime: java21 + Architectures: + - arm64 + Timeout: 30 + MemorySize: 3072 # AWS Lambda Power Tuning balanced strategy Handler: com.amazon.aws.partners.saasfactory.saasboost.SettingsService::updateSetting Code: S3Bucket: !Ref SaaSBoostBucket @@ -323,242 +184,13 @@ Resources: Environment: Variables: SAAS_BOOST_ENV: !Ref Environment - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" + - Key: Application + Value: SaaSBoost + - Key: Environment Value: !Ref Environment - - Key: "BoostService" - Value: "Settings" - SettingsServiceUpdateAppConfigLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-settings-update-config - RetentionInDays: 30 - SettingsServiceUpdateAppConfig: - Type: AWS::Lambda::Function - Properties: - FunctionName: !Sub sb-${Environment}-settings-update-config - Role: !GetAtt SettingsServiceExecutionRole.Arn - Runtime: java11 - Timeout: 300 - MemorySize: 512 - Handler: com.amazon.aws.partners.saasfactory.saasboost.SettingsService::updateAppConfig - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/SettingsService-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref ApiGatewayHelperLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus - API_TRUST_ROLE: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' - API_GATEWAY_HOST: !Sub ${SaaSBoostPrivateApi}.execute-api.${AWS::Region}.${AWS::URLSuffix} - API_GATEWAY_STAGE: !Ref PrivateApiStage - RESOURCES_BUCKET: !Ref ResourcesBucket - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "Settings" - SettingsServiceGetAppConfigLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-settings-get-config - RetentionInDays: 30 - SettingsServiceGetAppConfig: - Type: AWS::Lambda::Function - Properties: - FunctionName: !Sub sb-${Environment}-settings-get-config - Role: !GetAtt SettingsServiceExecutionRole.Arn - Runtime: java11 - Timeout: 300 - MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.SettingsService::getAppConfig - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/SettingsService-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "Settings" - SettingsServiceDeleteAppConfigLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-settings-delete-config - RetentionInDays: 30 - SettingsServiceDeleteAppConfig: - Type: AWS::Lambda::Function - Properties: - FunctionName: !Sub sb-${Environment}-settings-delete-config - Role: !GetAtt SettingsServiceExecutionRole.Arn - Runtime: java11 - Timeout: 300 - MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.SettingsService::deleteAppConfig - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/SettingsService-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "Settings" - AppConfigEventHandlerLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-settings-app-config-events - RetentionInDays: 30 - AppConfigEventHandler: - Type: AWS::Lambda::Function - Properties: - FunctionName: !Sub sb-${Environment}-settings-app-config-events - Role: !GetAtt SettingsServiceExecutionRole.Arn - Runtime: java11 - Timeout: 300 - MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.SettingsService::handleAppConfigEvent - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/SettingsService-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - - !Ref ApiGatewayHelperLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus - API_TRUST_ROLE: !Sub '{{resolve:ssm:/saas-boost/${Environment}/PRIVATE_API_TRUST_ROLE}}' - API_GATEWAY_HOST: !Sub ${SaaSBoostPrivateApi}.execute-api.${AWS::Region}.${AWS::URLSuffix} - API_GATEWAY_STAGE: !Ref PrivateApiStage - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "Settings" - SettingsServiceOptionsLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-settings-options - RetentionInDays: 30 - SettingsServiceOptions: - Type: AWS::Lambda::Function - Properties: - FunctionName: !Sub sb-${Environment}-settings-options - Role: !GetAtt SettingsServiceExecutionRole.Arn - Runtime: java11 - Timeout: 300 - MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.SettingsService::options - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/SettingsService-lambda.zip - Layers: - - !Ref SaaSBoostUtilsLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - OPTIONS_TABLE: !Ref RdsOptionsTable - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" - Value: !Ref Environment - - Key: "BoostService" - Value: "Settings" - AppConfigEventRule: - Type: AWS::Events::Rule - Properties: - Name: !Sub sb-${Environment}-app-config-events - Description: SaaS Boost application config events - EventBusName: !Ref SaaSBoostEventBus - EventPattern: - { - "source": [ - "saas-boost" - ], - "detail-type": [{ - "prefix": "Application Configuration " - }] - } - State: ENABLED - Targets: - - Arn: !GetAtt AppConfigEventHandler.Arn - Id: !Sub sb-${Environment}-app-config-events - AppConfigEventsPermission: - Type: AWS::Lambda::Permission - Properties: - Action: lambda:InvokeFunction - FunctionName: !Ref AppConfigEventHandler - Principal: events.amazonaws.com - SourceArn: !GetAtt AppConfigEventRule.Arn - AppConfigResourceFilesEventRule: - Type: AWS::Events::Rule - Properties: - Name: !Sub sb-${Environment}-settings-app-config - Description: SaaS Boost application config resources bucket events - EventPattern: !Sub | - { - "source": [ - "aws.s3" - ], - "detail-type": [ - "Object Created" - ], - "detail": { - "reason": [ - "PutObject" - ], - "bucket": { - "name": [ - "${ResourcesBucket}" - ] - }, - "object": { - "key": [{ - "prefix": "services" - }] - } - } - } - State: ENABLED - Targets: - - Arn: !GetAtt AppConfigEventHandler.Arn - Id: !Sub sb-${Environment}-settings-app-config-file-event - AppConfigResourceFilesEventPermission: - Type: AWS::Lambda::Permission - Properties: - Action: lambda:InvokeFunction - FunctionName: !Ref AppConfigEventHandler - Principal: events.amazonaws.com - SourceArn: !GetAtt AppConfigResourceFilesEventRule.Arn + - Key: BoostService + Value: Settings Outputs: SettingsServiceGetAllArn: Description: Settings Service get all settings Lambda ARN @@ -566,19 +198,10 @@ Outputs: SettingsServiceByIdArn: Description: Settings Service get setting Lambda ARN Value: !GetAtt SettingsServiceGetById.Arn - SettingsServiceOptionsArn: - Description: Settings Service get options Lambda ARN - Value: !GetAtt SettingsServiceOptions.Arn - SettingsServiceGetAppConfigArn: - Description: Settings Service get application configuration Lambda ARN - Value: !GetAtt SettingsServiceGetAppConfig.Arn - SettingsServiceUpdateAppConfigArn: - Description: Settings Service update application configuration Lambda ARN - Value: !GetAtt SettingsServiceUpdateAppConfig.Arn - SettingsServiceDeleteAppConfigArn: - Description: Settings Service delete application configuration Lambda ARN - Value: !GetAtt SettingsServiceDeleteAppConfig.Arn SettingsServiceGetSecretArn: Description: Settings Service get decrypted secret setting Lambda ARN Value: !GetAtt SettingsServiceGetSecret.Arn -... \ No newline at end of file + SettingsServiceUpdateArn: + Description: Settings Service update setting Lambda ARN + Value: !GetAtt SettingsServiceUpdate.Arn +... diff --git a/resources/saas-boost-svc-system-user.yaml b/resources/saas-boost-svc-system-user.yaml index b053b057..6cc20c7e 100644 --- a/resources/saas-boost-svc-system-user.yaml +++ b/resources/saas-boost-svc-system-user.yaml @@ -24,6 +24,9 @@ Parameters: SaaSBoostUtilsLayer: Description: Utils Layer ARN Type: String + KeycloakHelperLayer: + Description: Keycloak Helper Layer ARN + Type: String Environment: Description: Environment name Type: String @@ -65,6 +68,9 @@ Resources: - cognito-idp:ListUsers - cognito-idp:AdminInitiateAuth - cognito-idp:ListUserPoolClients + - cognito-idp:AdminAddUserToGroup + - cognito-idp:AdminListGroupsForUser + - cognito-idp:AdminRemoveUserFromGroup Resource: - !Sub arn:${AWS::Partition}:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${CognitoUserPoolId} SystemUserServiceExecutionRole: @@ -111,7 +117,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-sys-user-get-by-id Role: !GetAtt SystemUserServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.SystemUserService::getUser @@ -120,6 +128,7 @@ Resources: S3Key: !Sub ${LambdaSourceFolder}/SystemUserService-lambda.zip Layers: - !Ref SaaSBoostUtilsLayer + - !If [UseKeycloak, !Ref KeycloakHelperLayer, !Ref 'AWS::NoValue'] Environment: Variables: SAAS_BOOST_ENV: !Ref Environment @@ -127,7 +136,6 @@ Resources: COGNITO_USER_POOL: !If [UseCognito, !Ref CognitoUserPoolId, !Ref 'AWS::NoValue'] KEYCLOAK_HOST: !If [UseKeycloak, !Ref KeycloakHost, !Ref 'AWS::NoValue'] KEYCLOAK_REALM: !If [UseKeycloak, !Ref KeycloakRealm, !Ref 'AWS::NoValue'] - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: Application Value: SaaSBoost @@ -145,7 +153,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-sys-user-get-all Role: !GetAtt SystemUserServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.SystemUserService::getUsers @@ -154,6 +164,7 @@ Resources: S3Key: !Sub ${LambdaSourceFolder}/SystemUserService-lambda.zip Layers: - !Ref SaaSBoostUtilsLayer + - !If [UseKeycloak, !Ref KeycloakHelperLayer, !Ref 'AWS::NoValue'] Environment: Variables: SAAS_BOOST_ENV: !Ref Environment @@ -161,7 +172,6 @@ Resources: COGNITO_USER_POOL: !If [UseCognito, !Ref CognitoUserPoolId, !Ref 'AWS::NoValue'] KEYCLOAK_HOST: !If [UseKeycloak, !Ref KeycloakHost, !Ref 'AWS::NoValue'] KEYCLOAK_REALM: !If [UseKeycloak, !Ref KeycloakRealm, !Ref 'AWS::NoValue'] - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: Application Value: SaaSBoost @@ -179,7 +189,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-sys-user-update Role: !GetAtt SystemUserServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.SystemUserService::updateUser @@ -188,6 +200,7 @@ Resources: S3Key: !Sub ${LambdaSourceFolder}/SystemUserService-lambda.zip Layers: - !Ref SaaSBoostUtilsLayer + - !If [UseKeycloak, !Ref KeycloakHelperLayer, !Ref 'AWS::NoValue'] Environment: Variables: SAAS_BOOST_ENV: !Ref Environment @@ -195,7 +208,6 @@ Resources: COGNITO_USER_POOL: !If [UseCognito, !Ref CognitoUserPoolId, !Ref 'AWS::NoValue'] KEYCLOAK_HOST: !If [UseKeycloak, !Ref KeycloakHost, !Ref 'AWS::NoValue'] KEYCLOAK_REALM: !If [UseKeycloak, !Ref KeycloakRealm, !Ref 'AWS::NoValue'] - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: Application Value: SaaSBoost @@ -213,7 +225,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-sys-user-insert Role: !GetAtt SystemUserServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.SystemUserService::insertUser @@ -222,6 +236,7 @@ Resources: S3Key: !Sub ${LambdaSourceFolder}/SystemUserService-lambda.zip Layers: - !Ref SaaSBoostUtilsLayer + - !If [UseKeycloak, !Ref KeycloakHelperLayer, !Ref 'AWS::NoValue'] Environment: Variables: SAAS_BOOST_ENV: !Ref Environment @@ -229,7 +244,6 @@ Resources: COGNITO_USER_POOL: !If [UseCognito, !Ref CognitoUserPoolId, !Ref 'AWS::NoValue'] KEYCLOAK_HOST: !If [UseKeycloak, !Ref KeycloakHost, !Ref 'AWS::NoValue'] KEYCLOAK_REALM: !If [UseKeycloak, !Ref KeycloakRealm, !Ref 'AWS::NoValue'] - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: Application Value: SaaSBoost @@ -247,7 +261,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-sys-user-delete Role: !GetAtt SystemUserServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.SystemUserService::deleteUser @@ -256,6 +272,7 @@ Resources: S3Key: !Sub ${LambdaSourceFolder}/SystemUserService-lambda.zip Layers: - !Ref SaaSBoostUtilsLayer + - !If [UseKeycloak, !Ref KeycloakHelperLayer, !Ref 'AWS::NoValue'] Environment: Variables: SAAS_BOOST_ENV: !Ref Environment @@ -263,7 +280,6 @@ Resources: COGNITO_USER_POOL: !If [UseCognito, !Ref CognitoUserPoolId, !Ref 'AWS::NoValue'] KEYCLOAK_HOST: !If [UseKeycloak, !Ref KeycloakHost, !Ref 'AWS::NoValue'] KEYCLOAK_REALM: !If [UseKeycloak, !Ref KeycloakRealm, !Ref 'AWS::NoValue'] - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: Application Value: SaaSBoost @@ -281,7 +297,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-sys-user-enable Role: !GetAtt SystemUserServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.SystemUserService::enableUser @@ -290,6 +308,7 @@ Resources: S3Key: !Sub ${LambdaSourceFolder}/SystemUserService-lambda.zip Layers: - !Ref SaaSBoostUtilsLayer + - !If [UseKeycloak, !Ref KeycloakHelperLayer, !Ref 'AWS::NoValue'] Environment: Variables: SAAS_BOOST_ENV: !Ref Environment @@ -297,7 +316,6 @@ Resources: COGNITO_USER_POOL: !If [UseCognito, !Ref CognitoUserPoolId, !Ref 'AWS::NoValue'] KEYCLOAK_HOST: !If [UseKeycloak, !Ref KeycloakHost, !Ref 'AWS::NoValue'] KEYCLOAK_REALM: !If [UseKeycloak, !Ref KeycloakRealm, !Ref 'AWS::NoValue'] - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: Application Value: SaaSBoost @@ -315,7 +333,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-sys-user-disable Role: !GetAtt SystemUserServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.SystemUserService::disableUser @@ -324,6 +344,7 @@ Resources: S3Key: !Sub ${LambdaSourceFolder}/SystemUserService-lambda.zip Layers: - !Ref SaaSBoostUtilsLayer + - !If [UseKeycloak, !Ref KeycloakHelperLayer, !Ref 'AWS::NoValue'] Environment: Variables: SAAS_BOOST_ENV: !Ref Environment @@ -331,7 +352,6 @@ Resources: COGNITO_USER_POOL: !If [UseCognito, !Ref CognitoUserPoolId, !Ref 'AWS::NoValue'] KEYCLOAK_HOST: !If [UseKeycloak, !Ref KeycloakHost, !Ref 'AWS::NoValue'] KEYCLOAK_REALM: !If [UseKeycloak, !Ref KeycloakRealm, !Ref 'AWS::NoValue'] - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: Application Value: SaaSBoost diff --git a/resources/saas-boost-svc-tenant.yaml b/resources/saas-boost-svc-tenant.yaml index d1d45a29..ba31a7bb 100644 --- a/resources/saas-boost-svc-tenant.yaml +++ b/resources/saas-boost-svc-tenant.yaml @@ -107,7 +107,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-tenants-get-by-id Role: !GetAtt TenantServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.TenantService::getTenant @@ -121,7 +123,6 @@ Resources: SAAS_BOOST_ENV: !Ref Environment SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus TENANTS_TABLE: !Ref TenantsTable - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: "Application" Value: "SaaSBoost" @@ -139,7 +140,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-tenants-get-all Role: !GetAtt TenantServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 1024 Handler: com.amazon.aws.partners.saasfactory.saasboost.TenantService::getTenants @@ -160,7 +163,6 @@ Resources: SAAS_BOOST_ENV: !Ref Environment SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus TENANTS_TABLE: !Ref TenantsTable - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' TenantServiceUpdateLogs: Type: AWS::Logs::LogGroup Properties: @@ -171,7 +173,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-tenants-update Role: !GetAtt TenantServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.TenantService::updateTenant @@ -185,7 +189,6 @@ Resources: SAAS_BOOST_ENV: !Ref Environment SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus TENANTS_TABLE: !Ref TenantsTable - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: "Application" Value: "SaaSBoost" @@ -203,7 +206,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-tenants-insert Role: !GetAtt TenantServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.TenantService::insertTenant @@ -217,7 +222,6 @@ Resources: SAAS_BOOST_ENV: !Ref Environment SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus TENANTS_TABLE: !Ref TenantsTable - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: "Application" Value: "SaaSBoost" @@ -235,7 +239,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-tenants-delete Role: !GetAtt TenantServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.TenantService::deleteTenant @@ -249,7 +255,6 @@ Resources: SAAS_BOOST_ENV: !Ref Environment SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus TENANTS_TABLE: !Ref TenantsTable - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: "Application" Value: "SaaSBoost" @@ -267,7 +272,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-tenants-enable Role: !GetAtt TenantServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.TenantService::enableTenant @@ -281,7 +288,6 @@ Resources: SAAS_BOOST_ENV: !Ref Environment SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus TENANTS_TABLE: !Ref TenantsTable - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: "Application" Value: "SaaSBoost" @@ -299,7 +305,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-tenants-disable Role: !GetAtt TenantServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.TenantService::disableTenant @@ -313,7 +321,6 @@ Resources: SAAS_BOOST_ENV: !Ref Environment SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus TENANTS_TABLE: !Ref TenantsTable - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: "Application" Value: "SaaSBoost" @@ -331,7 +338,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-tenants-events Role: !GetAtt TenantServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 45 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.TenantService::handleTenantEvent @@ -346,7 +355,6 @@ Resources: SAAS_BOOST_ENV: !Ref Environment TENANTS_TABLE: !Ref TenantsTable SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: "Application" Value: "SaaSBoost" diff --git a/resources/saas-boost-svc-tier.yaml b/resources/saas-boost-svc-tier.yaml index c2cb3fce..1e07d507 100644 --- a/resources/saas-boost-svc-tier.yaml +++ b/resources/saas-boost-svc-tier.yaml @@ -78,6 +78,7 @@ Resources: - dynamodb:DescribeTable - dynamodb:GetItem - dynamodb:Scan + - dynamodb:Query - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:UpdateItem @@ -93,7 +94,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-tier-get-all Role: !GetAtt TierServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 1024 Handler: com.amazon.aws.partners.saasfactory.saasboost.TierService::getTiers @@ -105,14 +108,13 @@ Resources: Environment: Variables: TIERS_TABLE: !Ref TiersTable - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" + - Key: Application + Value: SaaSBoost + - Key: Environment Value: !Ref Environment - - Key: "BoostService" - Value: "Tier" + - Key: BoostService + Value: Tier TierServiceGetByIdLogs: Type: AWS::Logs::LogGroup Properties: @@ -123,7 +125,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-tier-get-by-id Role: !GetAtt TierServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.TierService::getTier @@ -135,14 +139,13 @@ Resources: Environment: Variables: TIERS_TABLE: !Ref TiersTable - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" + - Key: Application + Value: SaaSBoost + - Key: Environment Value: !Ref Environment - - Key: "BoostService" - Value: "Tier" + - Key: BoostService + Value: Tier TierServiceUpdateLogs: Type: AWS::Logs::LogGroup Properties: @@ -153,7 +156,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-tier-update Role: !GetAtt TierServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 512 Handler: com.amazon.aws.partners.saasfactory.saasboost.TierService::updateTier @@ -165,7 +170,6 @@ Resources: Environment: Variables: TIERS_TABLE: !Ref TiersTable - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: "Application" Value: "SaaSBoost" @@ -173,20 +177,22 @@ Resources: Value: !Ref Environment - Key: "BoostService" Value: "Tier" - TierServiceCreateLogs: + TierServiceInsertLogs: Type: AWS::Logs::LogGroup Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-tier-create + LogGroupName: !Sub /aws/lambda/sb-${Environment}-tier-insert RetentionInDays: 30 - TierServiceCreate: + TierServiceInsert: Type: AWS::Lambda::Function Properties: - FunctionName: !Sub sb-${Environment}-tier-create + FunctionName: !Sub sb-${Environment}-tier-insert Role: !GetAtt TierServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.TierService::createTier + Handler: com.amazon.aws.partners.saasfactory.saasboost.TierService::insertTier Code: S3Bucket: !Ref SaaSBoostBucket S3Key: !Sub ${LambdaSourceFolder}/TierService-lambda.zip @@ -195,14 +201,13 @@ Resources: Environment: Variables: TIERS_TABLE: !Ref TiersTable - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" + - Key: Application + Value: SaaSBoost + - Key: Environment Value: !Ref Environment - - Key: "BoostService" - Value: "Tier" + - Key: BoostService + Value: Tier TierServiceDeleteLogs: Type: AWS::Logs::LogGroup Properties: @@ -213,7 +218,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-tier-delete Role: !GetAtt TierServiceExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 300 MemorySize: 1024 Handler: com.amazon.aws.partners.saasfactory.saasboost.TierService::deleteTier @@ -225,14 +232,13 @@ Resources: Environment: Variables: TIERS_TABLE: !Ref TiersTable - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - - Key: "Application" - Value: "SaaSBoost" - - Key: "Environment" + - Key: Application + Value: SaaSBoost + - Key: Environment Value: !Ref Environment - - Key: "BoostService" - Value: "Tier" + - Key: BoostService + Value: Tier Outputs: TierServiceGetAllArn: Description: Tier Service get all tiers Lambda ARN @@ -243,9 +249,9 @@ Outputs: TierServiceUpdateArn: Description: Tier Service update tier Lambda ARN Value: !GetAtt TierServiceUpdate.Arn - TierServiceCreateArn: - Description: Tier Service create tier Lambda ARN - Value: !GetAtt TierServiceCreate.Arn + TierServiceInsertArn: + Description: Tier Service insert tier Lambda ARN + Value: !GetAtt TierServiceInsert.Arn TierServiceDeleteArn: Description: Tier Service delete tier Lambda ARN Value: !GetAtt TierServiceDelete.Arn diff --git a/resources/saas-boost-web.yaml b/resources/saas-boost-web.yaml index 105d715e..fd049b4f 100644 --- a/resources/saas-boost-web.yaml +++ b/resources/saas-boost-web.yaml @@ -194,12 +194,10 @@ Resources: "aws.s3" ], "detail-type": [ - "Object Created" + "Object Created", + "Object Deleted" ], "detail": { - "reason": [ - "PutObject", "PostObject", "CopyObject", "CompleteMultipartUpload" - ], "bucket": { "name": [ "${SaaSBoostBucket}" @@ -217,4 +215,4 @@ Resources: - Arn: !GetAtt AdminWebCodeBuildProject.Arn RoleArn: !GetAtt AdminWebBuildEventRole.Arn Id: !Sub sb-${Environment}-admin-web-build -... \ No newline at end of file +... diff --git a/resources/saas-boost.yaml b/resources/saas-boost.yaml index 98c3fe74..6fd6e150 100644 --- a/resources/saas-boost.yaml +++ b/resources/saas-boost.yaml @@ -63,33 +63,19 @@ Parameters: Type: String AllowedPattern: ^[^\s@]+@[^\s@]+\.[^\s@]+$ ConstraintDescription: Must be a valid email address. - PublicApiStage: - Description: The API Gateway REST API stage name for the SaaS Boost public API - Type: String - Default: v1 - PrivateApiStage: - Description: The API Gateway REST API stage name for the SaaS Boost private API + ApiStage: + Description: The API Gateway REST API stage name for the SaaS Boost API Type: String Default: v1 Version: Description: Version of SaaS Boost Type: String Default: 1.0 - ApplicationServices: - Description: Comma separated list of application service names to create ECR repositories for - Type: String - Default: '' - AppExtensions: - Description: Comma separated list of extension names to apply to the entire application + AppPlaneAccountId: + Description: Application Plane account to communicate with this Control Plane. Leave blank to use the same AWS account for your application plane resources. Type: String Default: '' - CreateMacroResources: - Description: Whether to create the Lambda, ExecutionRole, LogGroup, and CloudFormation macro for SaaS Boost environments - Type: String - AllowedValues: ['true', 'false'] - Default: 'false' Conditions: - ShouldCreateMacroResources: !Equals [!Ref CreateMacroResources, 'true'] UseCognito: !Equals [!Ref SystemIdentityProvider, 'COGNITO'] UseKeycloak: !Equals [!Ref SystemIdentityProvider, 'KEYCLOAK'] InChinaRegion: !Equals [!Ref AWS::Partition, 'aws-cn'] @@ -98,31 +84,13 @@ Conditions: HasSystemIdPCustomDomain: !Not [!Equals [!Ref SystemIdentityProviderDomain, '']] AdminWebAppCustomDomainInChina: !And [!Condition InChinaRegion, !Condition HasAdminWebAppCustomDomain] AdminWebAppCustomDomainNotInChina: !And [!Condition NotInChinaRegion, !Condition HasAdminWebAppCustomDomain] + AppPlaneSameAccount: !Equals ['', !Ref AppPlaneAccountId] Resources: - SSMSaaSBoostEnvironment: - Type: AWS::SSM::Parameter - Properties: - Name: !Sub /saas-boost/${Environment}/SAAS_BOOST_ENVIRONMENT - Type: String - Value: !Ref Environment - SSMParamSaaSBoostBucket: - Type: AWS::SSM::Parameter - Properties: - Name: !Sub /saas-boost/${Environment}/SAAS_BOOST_BUCKET - Type: String - Value: !Ref SaaSBoostBucket - SSMParamLambdaSourceFolder: - Type: AWS::SSM::Parameter - Properties: - Name: !Sub /saas-boost/${Environment}/SAAS_BOOST_LAMBDAS_FOLDER - Type: String - Value: !Ref LambdaSourceFolder # Create all the S3 buckets SaaS Boost needs up front so we can create # a single Lambda IAM policy to clean up the buckets on stack delete Logging: Type: AWS::S3::Bucket Properties: - AccessControl: Private PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true @@ -165,11 +133,9 @@ Resources: - logging.s3.amazonaws.com Resource: - !Sub arn:${AWS::Partition}:s3:::${Logging}/* - # Bucket needed for CodePipeline to drive tenant deployment workflow Pipelines: Type: AWS::S3::Bucket Properties: - AccessControl: Private PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true @@ -190,57 +156,15 @@ Resources: Tags: - Key: SaaS Boost Value: !Ref Environment - CodePipelineBucketPolicy: - Type: AWS::S3::BucketPolicy - Properties: - Bucket: !Ref Pipelines - PolicyDocument: - Statement: - - Effect: Deny - Action: s3:* - Principal: '*' - Resource: - - !Sub arn:${AWS::Partition}:s3:::${Pipelines}/* - - !Sub arn:${AWS::Partition}:s3:::${Pipelines} - Condition: - Bool: { 'aws:SecureTransport': false } - # Bucket for Access Logs for ALBs for Tenants - AccessLogs: - Type: AWS::S3::Bucket - Properties: - AccessControl: Private - LifecycleConfiguration: - Rules: - - Id: DeleteContentAfter30Day - Status: 'Enabled' - ExpirationInDays: 30 - PublicAccessBlockConfiguration: - BlockPublicAcls: true - BlockPublicPolicy: true - IgnorePublicAcls: true - RestrictPublicBuckets: true - BucketEncryption: - ServerSideEncryptionConfiguration: - - ServerSideEncryptionByDefault: - SSEAlgorithm: AES256 - Tags: - - Key: SaaS Boost - Value: !Ref Environment # Bucket for Athena output for access log queries AthenaOutput: Type: AWS::S3::Bucket Properties: - AccessControl: Private LifecycleConfiguration: Rules: - Id: DeleteContentAfter5Day Status: 'Enabled' ExpirationInDays: 5 - PublicAccessBlockConfiguration: - BlockPublicAcls: true - BlockPublicPolicy: true - IgnorePublicAcls: true - RestrictPublicBuckets: true BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: @@ -273,7 +197,6 @@ Resources: Resources: Type: AWS::S3::Bucket Properties: - AccessControl: Private PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true @@ -301,7 +224,6 @@ Resources: AdminWeb: Type: AWS::S3::Bucket Properties: - AccessControl: Private PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true @@ -424,15 +346,35 @@ Resources: Type: AWS::Lambda::LayerVersion Properties: LayerName: !Sub sb-${Environment}-utils - CompatibleRuntimes: [java11] + CompatibleRuntimes: + - java17 + - java21 + CompatibleArchitectures: + - arm64 + - x86_64 Content: S3Bucket: !Ref SaaSBoostBucket S3Key: !Sub ${LambdaSourceFolder}/Utils-lambda.zip + SaaSBoostUtilsLayerPermissions: + Type: AWS::Lambda::LayerVersionPermission + Properties: + Action: lambda:GetLayerVersion + LayerVersionArn: !Ref SaaSBoostUtilsLayer + Principal: + !If + - AppPlaneSameAccount + - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root + - !Sub arn:${AWS::Partition}:iam::${AppPlaneAccountId}:root ApiGatewayHelperLayer: Type: AWS::Lambda::LayerVersion Properties: LayerName: !Sub sb-${Environment}-apigw-helper - CompatibleRuntimes: [java11] + CompatibleRuntimes: + - java17 + - java21 + CompatibleArchitectures: + - arm64 + - x86_64 Content: S3Bucket: !Ref SaaSBoostBucket S3Key: !Sub ${LambdaSourceFolder}/ApiGatewayHelper-lambda.zip @@ -440,10 +382,68 @@ Resources: Type: AWS::Lambda::LayerVersion Properties: LayerName: !Sub sb-${Environment}-cloudformation-utils - CompatibleRuntimes: [java11] + CompatibleRuntimes: + - java17 + - java21 + CompatibleArchitectures: + - arm64 + - x86_64 Content: S3Bucket: !Ref SaaSBoostBucket S3Key: !Sub ${LambdaSourceFolder}/CloudFormationUtils-lambda.zip + CloudFormationUtilsLayerPermissions: + Type: AWS::Lambda::LayerVersionPermission + Properties: + Action: lambda:GetLayerVersion + LayerVersionArn: !Ref CloudFormationUtilsLayer + Principal: + !If + - AppPlaneSameAccount + - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root + - !Sub arn:${AWS::Partition}:iam::${AppPlaneAccountId}:root + KeycloakHelperLayer: + Type: AWS::Lambda::LayerVersion + Condition: UseKeycloak + Properties: + LayerName: !Sub sb-${Environment}-keycloak-helper + CompatibleRuntimes: + - java17 + - java21 + CompatibleArchitectures: + - arm64 + - x86_64 + Content: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/KeycloakHelper-lambda.zip + SaaSBoostApiClientHelperLayer: + Type: AWS::Lambda::LayerVersion + Properties: + LayerName: !Sub sb-${Environment}-api-client-helper + CompatibleRuntimes: + - java21 + - java17 + - java11 + - python3.12 + - python3.11 + - python3.10 + - python3.9 + - python3.8 + CompatibleArchitectures: + - arm64 + - x86_64 + Content: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/SaaSBoostApiClientHelper-lambda.zip + SaaSBoostApiClientHelperLayerPermissions: + Type: AWS::Lambda::LayerVersionPermission + Properties: + Action: lambda:GetLayerVersion + LayerVersionArn: !Ref SaaSBoostApiClientHelperLayer + Principal: + !If + - AppPlaneSameAccount + - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root + - !Sub arn:${AWS::Partition}:iam::${AppPlaneAccountId}:root ClearBucketExecutionRole: Type: AWS::IAM::Role Properties: @@ -499,7 +499,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-clear-bucket Role: !GetAtt ClearBucketExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 900 MemorySize: 1024 Handler: com.amazon.aws.partners.saasfactory.saasboost.ClearS3Bucket @@ -509,9 +511,6 @@ Resources: Layers: - !Ref SaaSBoostUtilsLayer - !Ref CloudFormationUtilsLayer - Environment: - Variables: - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: Application Value: SaaSBoost @@ -533,11 +532,6 @@ Resources: Properties: ServiceToken: !GetAtt ClearBucket.Arn Bucket: !Ref AthenaOutput - InvokeClearAccessLogsBucket: - Type: Custom::CustomResource - Properties: - ServiceToken: !GetAtt ClearBucket.Arn - Bucket: !Ref AccessLogs InvokeClearWebsiteBucket: Type: Custom::CustomResource Properties: @@ -599,7 +593,9 @@ Resources: Properties: FunctionName: !Sub sb-${Environment}-clear-ecr-repo Role: !GetAtt ClearEcrRepoExecutionRole.Arn - Runtime: java11 + Runtime: java21 + Architectures: + - arm64 Timeout: 900 MemorySize: 1024 Handler: com.amazon.aws.partners.saasfactory.saasboost.ClearEcrRepo @@ -609,21 +605,90 @@ Resources: Layers: - !Ref SaaSBoostUtilsLayer - !Ref CloudFormationUtilsLayer - Environment: - Variables: - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - Key: Application Value: SaaSBoost - Key: Environment Value: !Ref Environment - ApplicationServicesMacroExecutionRole: + SaaSBoostEventBus: + Type: AWS::Events::EventBus + Properties: + Name: !Sub sb-${Environment}-events + SaaSBoostEventBusDlq: + Type: AWS::SQS::Queue + SaaSBoostEventBusDlqPolicy: + Type: AWS::SQS::QueuePolicy + Properties: + Queues: + - Ref: SaaSBoostEventBusDlq + PolicyDocument: + Statement: + - Effect: Allow + Action: + - SQS:SendMessage + Resource: !GetAtt SaaSBoostEventBusDlq.Arn + Principal: + Service: events.amazonaws.com + # Allow App Plane to publish certain SaaS Boost events to the Control Plane EventBus + AppPlanePublishStatement: + Type: AWS::Events::EventBusPolicy + Properties: + EventBusName: !Ref SaaSBoostEventBus + StatementId: !Sub sb-${Environment}-app-plane-publish + Statement: + Effect: Allow + Principal: + AWS: !Sub arn:${AWS::Partition}:iam::${AppPlaneAccountId}:root + Action: + - events:PutEvents + Resource: + - !GetAtt SaaSBoostEventBus.Arn + Condition: + ForAnyValue:StringEquals: + "events:detail-type": + - Onboarding Validated + - Onboarding Provisioning + - Onboarding Provisioned + - Onboarding Deploying + - Onboarding Deployed + - Onboarding Failed + - Tenant Onboarding Status Changed + - Tenant Resources Changed + StringEquals: + events:source: saas-boost + # Allow App Plane to create rules and targets for Control Plane events + AppPlaneRuleCreationStatement: + Type: AWS::Events::EventBusPolicy + Properties: + EventBusName: !Ref SaaSBoostEventBus + StatementId: !Sub sb-${Environment}-app-plane-rule-creation + Statement: + Effect: Allow + Principal: + AWS: !Sub arn:${AWS::Partition}:iam::${AppPlaneAccountId}:root + Action: + - events:PutRule + - events:DeleteRule + - events:DescribeRule + - events:DisableRule + - events:EnableRule + - events:PutTargets + - events:RemoveTargets + Resource: + - !Sub arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${SaaSBoostEventBus.Name}/* + Condition: + StringEqualsIfExists: + "events:creatorAccount": "${aws:PrincipalAccount}" + "events:source": saas-boost + CodeBuildStartLogs: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-start-build + RetentionInDays: 30 + CodeBuildStartExecRole: Type: AWS::IAM::Role - Condition: ShouldCreateMacroResources - DependsOn: - - ApplicationServicesMacroLogs Properties: - RoleName: !Sub saas-boost-app-services-macro-${AWS::Region} + RoleName: !Sub sb-${Environment}-start-build-role-${AWS::Region} Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 @@ -635,83 +700,61 @@ Resources: Action: - sts:AssumeRole Policies: - - PolicyName: saas-boost-app-services-macro + - PolicyName: !Sub sb-${Environment}-start-build-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:PutLogEvents - Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* + Resource: + - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - Effect: Allow Action: - logs:CreateLogStream - logs:DescribeLogStreams Resource: - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - ApplicationServicesMacroLogs: - Type: AWS::Logs::LogGroup - Condition: ShouldCreateMacroResources - Properties: - LogGroupName: /aws/lambda/saas-boost-app-services-macro - RetentionInDays: 30 - ApplicationServicesMacroFunction: + - Effect: Allow + Action: + - codebuild:StartBuild + - codebuild:BatchGetBuilds + Resource: !Sub arn:${AWS::Partition}:codebuild:${AWS::Region}:${AWS::AccountId}:project/* + CodeBuildStartLambda: Type: AWS::Lambda::Function - Condition: ShouldCreateMacroResources - DependsOn: ApplicationServicesMacroLogs + DependsOn: CodeBuildStartLogs Properties: - FunctionName: saas-boost-app-services-macro - Role: !GetAtt ApplicationServicesMacroExecutionRole.Arn - Runtime: java11 - Timeout: 900 + FunctionName: !Sub sb-${Environment}-start-build + Role: !GetAtt CodeBuildStartExecRole.Arn + Runtime: java21 + Architectures: + - arm64 + Timeout: 600 MemorySize: 1024 - Handler: com.amazon.aws.partners.saasfactory.saasboost.ApplicationServicesMacro + Handler: com.amazon.aws.partners.saasfactory.saasboost.StartCodeBuild Code: S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/ApplicationServicesMacro-lambda.zip + S3Key: !Sub ${LambdaSourceFolder}/StartCodeBuild-lambda.zip Layers: - - !Ref SaaSBoostUtilsLayer + - !Ref SaaSBoostUtilsLayer + - !Ref CloudFormationUtilsLayer Environment: Variables: - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' + SAAS_BOOST_ENV: !Ref Environment Tags: - - Key: Application - Value: SaaSBoost - - Key: Environment + - Key: "Application" + Value: "SaaSBoost" + - Key: "Environment" Value: !Ref Environment - ApplicationServicesMacro: - Type: AWS::CloudFormation::Macro - Condition: ShouldCreateMacroResources - Properties: - # Can't use a parameter as part of a macro name when you include it in another template - Name: saas-boost-app-services-macro - FunctionName: !GetAtt ApplicationServicesMacroFunction.Arn - CreateMacroWaitHandle: - Condition: ShouldCreateMacroResources - DependsOn: ApplicationServicesMacro - Type: AWS::CloudFormation::WaitConditionHandle - NoCreateMacroWaitHandle: - Type: AWS::CloudFormation::WaitConditionHandle - MacroWaitCondition: - Type: AWS::CloudFormation::WaitCondition - Properties: - Handle: !If [ShouldCreateMacroResources, !Ref CreateMacroWaitHandle, !Ref NoCreateMacroWaitHandle] - Timeout: '1' - Count: 0 - SaaSBoostEventBus: - Type: AWS::Events::EventBus - Properties: - Name: !Sub sb-${Environment}-events - SaaSBoostEventBusParam: - Type: AWS::SSM::Parameter + CodePipelineWaitHandlerLogs: + Type: AWS::Logs::LogGroup Properties: - Name: !Sub /saas-boost/${Environment}/EVENT_BUS - Type: String - Value: !Ref SaaSBoostEventBus - CoreStackListenerExecRole: + LogGroupName: !Sub /aws/lambda/sb-${Environment}-pipeline-waithandler + RetentionInDays: 30 + CodePipelineWaitHandlerExecRole: Type: AWS::IAM::Role Properties: - RoleName: !Sub sb-${Environment}-core-stack-listener-${AWS::Region} + RoleName: !Sub sb-${Environment}-pipeline-waithandler-role-${AWS::Region} Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 @@ -723,7 +766,7 @@ Resources: Action: - sts:AssumeRole Policies: - - PolicyName: !Sub sb-${Environment}-core-stack-listener + - PolicyName: !Sub sb-${Environment}-pipeline-waithandler-policy PolicyDocument: Version: 2012-10-17 Statement: @@ -740,72 +783,121 @@ Resources: - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - Effect: Allow Action: - - cloudformation:DescribeStacks - - cloudformation:ListStackResources - Resource: - - !Sub arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/* + - codepipeline:PutJobSuccessResult + - codepipeline:PutJobFailureResult + Resource: '*' + CodePipelineWaitHandlerLambda: + Type: AWS::Lambda::Function + DependsOn: CodePipelineWaitHandlerLogs + Properties: + FunctionName: !Sub sb-${Environment}-pipeline-waithandler + Role: !GetAtt CodePipelineWaitHandlerExecRole.Arn + Runtime: java21 + Architectures: + - arm64 + Timeout: 600 + MemorySize: 1024 + Handler: com.amazon.aws.partners.saasfactory.saasboost.CodePipelineWaitHandler + Code: + S3Bucket: !Ref SaaSBoostBucket + S3Key: !Sub ${LambdaSourceFolder}/CodePipelineWaitHandler-lambda.zip + Layers: + - !Ref SaaSBoostUtilsLayer + - !Ref CloudFormationUtilsLayer + Environment: + Variables: + SAAS_BOOST_ENV: !Ref Environment + Tags: + - Key: "Application" + Value: "SaaSBoost" + - Key: "Environment" + Value: !Ref Environment + CodePipelineUpdateEcsServiceExecRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub sb-${Environment}-update-ecs-${AWS::Region} + Path: '/' + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyName: !Sub sb-${Environment}-update-ecs + PolicyDocument: + Version: 2012-10-17 + Statement: - Effect: Allow Action: - - events:PutEvents + - logs:PutLogEvents + Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* + - Effect: Allow + Action: + - logs:CreateLogStream + - logs:DescribeLogStreams Resource: - - !Sub arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/${SaaSBoostEventBus} + - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - Effect: Allow Action: - - ecr:ListTagsForResource + - codepipeline:PutJobSuccessResult + - codepipeline:PutJobFailureResult + Resource: '*' + - Effect: Allow + Action: + - ecs:DescribeServices + - ecs:UpdateService Resource: - - !Sub arn:${AWS::Partition}:ecr:${AWS::Region}:${AWS::AccountId}:repository/sb-${Environment}-core-* - CoreStackListenerLogs: + - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:service/* + Condition: + StringLike: + ecs:cluster: + - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/sb-${Environment}-tenant* + - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/sb-${Environment}-keycloak + CodePipelineUpdateEcsServiceLogs: Type: AWS::Logs::LogGroup Properties: - LogGroupName: !Sub /aws/lambda/sb-${Environment}-core-stack-listener + LogGroupName: !Sub /aws/lambda/sb-${Environment}-update-ecs RetentionInDays: 30 - CoreStackListener: + CodePipelineUpdateEcsServiceLambda: Type: AWS::Lambda::Function - DependsOn: CoreStackListenerLogs - Properties: - FunctionName: !Sub sb-${Environment}-core-stack-listener - Role: !GetAtt CoreStackListenerExecRole.Arn - Runtime: java11 - Timeout: 600 + DependsOn: CodePipelineUpdateEcsServiceLogs + Properties: + FunctionName: !Sub sb-${Environment}-update-ecs + Role: !GetAtt CodePipelineUpdateEcsServiceExecRole.Arn + Runtime: java21 + Architectures: + - arm64 + Timeout: 300 MemorySize: 512 - Handler: com.amazon.aws.partners.saasfactory.saasboost.CoreStackListener + Handler: com.amazon.aws.partners.saasfactory.saasboost.EcsServiceUpdate Code: S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub ${LambdaSourceFolder}/CoreStackListener-lambda.zip + S3Key: !Sub ${LambdaSourceFolder}/EcsServiceUpdate-lambda.zip Layers: - !Ref SaaSBoostUtilsLayer - - !Ref CloudFormationUtilsLayer - Environment: - Variables: - SAAS_BOOST_ENV: !Ref Environment - SAAS_BOOST_EVENT_BUS: !Ref SaaSBoostEventBus - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' Tags: - - Key: Application - Value: SaaSBoost - - Key: Environment + - Key: "Application" + Value: "SaaSBoost" + - Key: "Environment" Value: !Ref Environment - CoreStackListenerTopic: - Type: AWS::SNS::Topic - Properties: - DisplayName: SaaS Boost Provisioning Notifications - TopicName: !Sub sb-${Environment}-core-stack-listener - KmsMasterKeyId: alias/aws/sns - CoreStackListenerSubscription: - Type: AWS::SNS::Subscription - Properties: - Protocol: lambda - Endpoint: !GetAtt CoreStackListener.Arn - TopicArn: !Ref CoreStackListenerTopic - CoreStackListenerPermission: - Type: AWS::Lambda::Permission - Properties: - FunctionName: !Ref CoreStackListener - Principal: sns.amazonaws.com - Action: lambda:InvokeFunction - SourceArn: !Ref CoreStackListenerTopic + - Key: "BoostService" + Value: "ECSDeploy" + SaaSBoostApi: + Type: AWS::ApiGateway::RestApi + Properties: + Name: !Sub sb-${Environment}-api + EndpointConfiguration: + Types: + - REGIONAL + MinimumCompressionSize: 2000 + # We only need a VPC for the control plane if we're self-hosting Keycloak network: Type: AWS::CloudFormation::Stack + Condition: UseKeycloak Properties: TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-network.yaml Parameters: @@ -821,10 +913,11 @@ Resources: LambdaSourceFolder: !Ref LambdaSourceFolder SaaSBoostUtilsLayer: !Ref SaaSBoostUtilsLayer CloudFormationUtilsLayer: !Ref CloudFormationUtilsLayer + KeycloakHelperLayer: !If [UseKeycloak, !Ref KeycloakHelperLayer, ''] CodePipelineBucket: !Ref Pipelines - CodePipelineUpdateEcsService: !GetAtt core.Outputs.CodePipelineUpdateEcsService - StartCodeBuildLambda: !GetAtt core.Outputs.StartCodeBuildLambda - CloudFormationWaitHandleCallback: !GetAtt core.Outputs.CodePipelineWaitHandler + CodePipelineUpdateEcsService: !Ref CodePipelineUpdateEcsServiceLambda + StartCodeBuildLambda: !GetAtt CodeBuildStartLambda.Arn + CloudFormationWaitHandleCallback: !Ref CodePipelineWaitHandlerLambda AdminUsername: !Ref AdminUsername AdminEmailAddress: !Ref AdminEmailAddress AdminWebUrl: !If @@ -834,19 +927,28 @@ Resources: CustomDomainName: !If [HasSystemIdPCustomDomain, !Ref SystemIdentityProviderDomain, ''] CustomDomainHostedZone: !If [HasSystemIdPCustomDomain, !Ref SystemIdentityProviderHostedZone, ''] CustomDomainCertificate: !If [HasSystemIdPCustomDomain, !Ref SystemIdentityProviderCertificate, ''] - ApiGatewayUrl: !Sub https://${core.Outputs.SaaSBoostPublicApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${PublicApiStage} #!GetAtt publicapi.Outputs.PublicApiGatewayEndpoint - VPC: !GetAtt network.Outputs.EgressVpc + ApiGatewayUrl: !Sub https://${SaaSBoostApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${ApiStage} #!GetAtt publicapi.Outputs.PublicApiGatewayEndpoint + VPC: !If [UseKeycloak, !GetAtt network.Outputs.Vpc, ''] ClearEcrRepoArn: !GetAtt ClearEcrRepo.Arn PrivateSubnets: - !Join - - ',' - - - !GetAtt network.Outputs.PrivateSubnet1 - - !GetAtt network.Outputs.PrivateSubnet2 + !If + - UseKeycloak + - + !Join + - ',' + - - !GetAtt network.Outputs.PrivateSubnet1 + - !GetAtt network.Outputs.PrivateSubnet2 + - '' PublicSubnets: - !Join - - ',' - - - !GetAtt network.Outputs.PublicSubnet1 - - !GetAtt network.Outputs.PublicSubnet2 + !If + - UseKeycloak + - + !Join + - ',' + - - !GetAtt network.Outputs.PublicSubnet1 + - !GetAtt network.Outputs.PublicSubnet2 + - '' + AppPlaneAccountId: !Ref AppPlaneAccountId web: Type: AWS::CloudFormation::Stack Properties: @@ -854,43 +956,19 @@ Resources: Parameters: Environment: !Ref Environment SaaSBoostBucket: !Ref SaaSBoostBucket - StartCodeBuildLambda: !GetAtt core.Outputs.StartCodeBuildLambda + StartCodeBuildLambda: !GetAtt CodeBuildStartLambda.Arn AdminWebBucket: !Ref AdminWeb AdminWebUrl: !If - HasAdminWebAppCustomDomain - !Sub 'https://${AdminWebAppDomain}' - !Sub 'https://${AdminWebCloudFrontDistribution.DomainName}' - ApiGatewayUrl: !Sub https://${core.Outputs.SaaSBoostPublicApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${PublicApiStage} #!GetAtt publicapi.Outputs.PublicApiGatewayEndpoint + ApiGatewayUrl: !Sub https://${SaaSBoostApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${ApiStage} #!GetAtt publicapi.Outputs.PublicApiGatewayEndpoint AdminWebClientId: !GetAtt idp.Outputs.AdminWebAppClient OidcIssuerUrl: !GetAtt idp.Outputs.OidcIssuerUrl OidcDomainUrl: !GetAtt idp.Outputs.OidcDomainUrl SystemIdentityProvider: !Ref SystemIdentityProvider - core: - Type: AWS::CloudFormation::Stack - DependsOn: - - network - - MacroWaitCondition - Properties: - TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-core.yaml - Parameters: - Environment: !Ref Environment - SaaSBoostBucket: !Ref SaaSBoostBucket - LambdaSourceFolder: !Ref LambdaSourceFolder - SaaSBoostUtilsLayer: !Ref SaaSBoostUtilsLayer - ApiGatewayHelperLayer: !Ref ApiGatewayHelperLayer - CloudFormationUtilsLayer: !Ref CloudFormationUtilsLayer - CodePipelineBucket: !Ref Pipelines - LoggingBucket: !Ref Logging - PublicApiStage: !Ref PublicApiStage - PrivateApiStage: !Ref PrivateApiStage - ApplicationServices: !Ref ApplicationServices - EventBus: !Ref SaaSBoostEventBus - AppExtensions: !Ref AppExtensions - NotificationARNs: - - !Ref CoreStackListenerTopic billing: Type: AWS::CloudFormation::Stack - DependsOn: core Properties: TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-svc-billing.yaml Parameters: @@ -899,29 +977,28 @@ Resources: LambdaSourceFolder: !Ref LambdaSourceFolder SaaSBoostUtilsLayer: !Ref SaaSBoostUtilsLayer ApiGatewayHelperLayer: !Ref ApiGatewayHelperLayer - SaaSBoostPrivateApi: !GetAtt core.Outputs.SaaSBoostPrivateApi - PrivateApiStage: !Ref PrivateApiStage + SaaSBoostEventBus: !Ref SaaSBoostEventBus # To Do - merge this stuff into the billing template - metering: - Type: AWS::CloudFormation::Stack - Properties: - TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-metering-billing.yaml - Parameters: - Environment: !Ref Environment - SaaSBoostBucket: !Ref SaaSBoostBucket - LambdaSourceFolder: !Ref LambdaSourceFolder - SaaSBoostUtilsLayer: !Ref SaaSBoostUtilsLayer - ApiGatewayHelperLayer: !Ref ApiGatewayHelperLayer - EventBus: !Ref SaaSBoostEventBus - SaaSBoostPrivateApi: !GetAtt core.Outputs.SaaSBoostPrivateApi - PrivateApiStage: !Ref PrivateApiStage + #stripe: + # Type: AWS::CloudFormation::Stack + # Properties: + # TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-svc-billing-stripe.yaml + # Parameters: + # Environment: !Ref Environment + # SaaSBoostBucket: !Ref SaaSBoostBucket + # LambdaSourceFolder: !Ref LambdaSourceFolder + # SaaSBoostUtilsLayer: !Ref SaaSBoostUtilsLayer + # ApiGatewayHelperLayer: !Ref ApiGatewayHelperLayer + # EventBus: !Ref SaaSBoostEventBus + # SaaSBoostApi: !Ref SaaSBoostApi + # ApiStage: !Ref ApiStage metrics: Type: AWS::CloudFormation::Stack # Delete the metrics stack before clearing the S3 buckets because the metrics stack defines # EventBridge timers that could write to the bucket after clearing but before deleting DependsOn: - InvokeClearAthenaBucket - - InvokeClearAccessLogsBucket + #- InvokeClearAccessLogsBucket Properties: TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-svc-metrics.yaml Parameters: @@ -930,10 +1007,10 @@ Resources: LambdaSourceFolder: !Ref LambdaSourceFolder SaaSBoostUtilsLayer: !Ref SaaSBoostUtilsLayer ApiGatewayHelperLayer: !Ref ApiGatewayHelperLayer - AccessLogs: !Ref AccessLogs + #AccessLogs: !Ref AccessLogs AthenaOutput: !Ref AthenaOutput - SaaSBoostPrivateApi: !GetAtt core.Outputs.SaaSBoostPrivateApi - PrivateApiStage: !Ref PrivateApiStage + SaaSBoostApi: !Ref SaaSBoostApi + ApiStage: !Ref ApiStage onboarding: Type: AWS::CloudFormation::Stack Properties: @@ -946,24 +1023,20 @@ Resources: ApiGatewayHelperLayer: !Ref ApiGatewayHelperLayer CloudFormationUtilsLayer: !Ref CloudFormationUtilsLayer SaaSBoostEventBus: !Ref SaaSBoostEventBus - SaaSBoostPrivateApi: !GetAtt core.Outputs.SaaSBoostPrivateApi - PrivateApiStage: !Ref PrivateApiStage ResourcesBucket: !Ref Resources - quota: + settings: Type: AWS::CloudFormation::Stack - DependsOn: core Properties: - TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-svc-quota.yaml + TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-svc-settings.yaml Parameters: Environment: !Ref Environment SaaSBoostBucket: !Ref SaaSBoostBucket LambdaSourceFolder: !Ref LambdaSourceFolder SaaSBoostUtilsLayer: !Ref SaaSBoostUtilsLayer - ApiGatewayHelperLayer: !Ref ApiGatewayHelperLayer - settings: + appconfig: Type: AWS::CloudFormation::Stack Properties: - TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-svc-settings.yaml + TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-svc-app-config.yaml Parameters: Environment: !Ref Environment SaaSBoostBucket: !Ref SaaSBoostBucket @@ -972,8 +1045,6 @@ Resources: ApiGatewayHelperLayer: !Ref ApiGatewayHelperLayer CloudFormationUtilsLayer: !Ref CloudFormationUtilsLayer SaaSBoostEventBus: !Ref SaaSBoostEventBus - SaaSBoostPrivateApi: !GetAtt core.Outputs.SaaSBoostPrivateApi - PrivateApiStage: !Ref PrivateApiStage ResourcesBucket: !Ref Resources tenant: Type: AWS::CloudFormation::Stack @@ -999,8 +1070,6 @@ Resources: SaaSBoostEventBus: !Ref SaaSBoostEventBus sysuser: Type: AWS::CloudFormation::Stack - DependsOn: - - core Properties: TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-svc-system-user.yaml Parameters: @@ -1008,39 +1077,63 @@ Resources: SaaSBoostBucket: !Ref SaaSBoostBucket LambdaSourceFolder: !Ref LambdaSourceFolder SaaSBoostUtilsLayer: !Ref SaaSBoostUtilsLayer + KeycloakHelperLayer: !If [UseKeycloak, !Ref KeycloakHelperLayer, ''] IdentityProvider: !Ref SystemIdentityProvider CognitoUserPoolId: !If [UseCognito, !GetAtt idp.Outputs.CognitoUserPool, ''] KeycloakHost: !If [UseKeycloak, !GetAtt idp.Outputs.KeycloakHost, ''] KeycloakRealm: !If [UseKeycloak, !GetAtt idp.Outputs.KeycloakRealm, ''] - publicapi: + identity: Type: AWS::CloudFormation::Stack Properties: - TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-public-api.yaml + TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-svc-identity.yaml Parameters: Environment: !Ref Environment SaaSBoostBucket: !Ref SaaSBoostBucket LambdaSourceFolder: !Ref LambdaSourceFolder SaaSBoostUtilsLayer: !Ref SaaSBoostUtilsLayer - PublicApi: !GetAtt core.Outputs.SaaSBoostPublicApi - RootResourceId: !GetAtt core.Outputs.SaaSBoostPublicApiRootResourceId - PublicApiStage: !Ref PublicApiStage + SaaSBoostEventBus: !Ref SaaSBoostEventBus + AppPlaneAccountId: !If [AppPlaneSameAccount, !Ref AWS::AccountId, !Ref AppPlaneAccountId] + api: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-api.yaml + Parameters: + Environment: !Ref Environment + SaaSBoostBucket: !Ref SaaSBoostBucket + LambdaSourceFolder: !Ref LambdaSourceFolder + SaaSBoostUtilsLayer: !Ref SaaSBoostUtilsLayer + CodePipelineBucket: !Ref Pipelines + StartCodeBuildLambda: !GetAtt CodeBuildStartLambda.Arn + SSMParamSaaSBoostBucket: !Ref SSMParamSaaSBoostBucket + SSMParamLambdaSourceFolder: !Ref SSMParamLambdaSourceFolder + SaaSBoostApi: !Ref SaaSBoostApi + RootResourceId: !GetAtt SaaSBoostApi.RootResourceId + ApiStage: !Ref ApiStage IdentityProvider: !Ref SystemIdentityProvider CognitoUserPoolId: !If [UseCognito, !GetAtt idp.Outputs.CognitoUserPool, ''] KeycloakHost: !If [UseKeycloak, !GetAtt idp.Outputs.KeycloakHost, ''] KeycloakRealm: !If [UseKeycloak, !GetAtt idp.Outputs.KeycloakRealm, ''] - BillingServiceGetPlans: !GetAtt billing.Outputs.BillingServiceGetPlansArn - MetricsServiceQuery: !GetAtt metrics.Outputs.QueryArn - MetricsServiceDatasets: !GetAtt metrics.Outputs.DatasetsArn - MetricsServiceAlbQuery: !GetAtt metrics.Outputs.AlbQueryArn + AdminWebAppClientId: !GetAtt idp.Outputs.AdminWebAppClient + ApiAppClientId: !GetAtt idp.Outputs.ApiAppClient + PrivateApiAppClientId: !GetAtt idp.Outputs.PrivateApiAppClient + #BillingServiceGetPlans: !GetAtt billing.Outputs.BillingServiceGetPlansArn + #MetricsServiceQuery: !GetAtt metrics.Outputs.QueryArn + #MetricsServiceDatasets: !GetAtt metrics.Outputs.DatasetsArn + #MetricsServiceAlbQuery: !GetAtt metrics.Outputs.AlbQueryArn + MetricsServicePut: !GetAtt metrics.Outputs.MetricsServicePutArn OnboardingServiceGetAll: !GetAtt onboarding.Outputs.OnboardingServiceGetAllArn - OnboardingServiceStart: !GetAtt onboarding.Outputs.OnboardingServiceStartArn + OnboardingServiceInsert: !GetAtt onboarding.Outputs.OnboardingServiceInsertArn OnboardingServiceById: !GetAtt onboarding.Outputs.OnboardingServiceByIdArn SettingsServiceGetAll: !GetAtt settings.Outputs.SettingsServiceGetAllArn SettingsServiceById: !GetAtt settings.Outputs.SettingsServiceByIdArn - SettingsServiceOptions: !GetAtt settings.Outputs.SettingsServiceOptionsArn - SettingsServiceGetAppConfig: !GetAtt settings.Outputs.SettingsServiceGetAppConfigArn - SettingsServiceUpdateAppConfig: !GetAtt settings.Outputs.SettingsServiceUpdateAppConfigArn + SettingsServiceGetSecret: !GetAtt settings.Outputs.SettingsServiceGetSecretArn + SettingsServiceUpdate: !GetAtt settings.Outputs.SettingsServiceUpdateArn + AppConfigServiceOptions: !GetAtt appconfig.Outputs.AppConfigServiceOptionsArn + AppConfigServiceGet: !GetAtt appconfig.Outputs.AppConfigServiceGetArn + AppConfigServiceUpdate: !GetAtt appconfig.Outputs.AppConfigServiceUpdateArn + AppConfigServiceDelete: !GetAtt appconfig.Outputs.AppConfigServiceDeleteArn TenantServiceGetAll: !GetAtt tenant.Outputs.TenantServiceGetAllArn + TenantServiceInsert: !GetAtt tenant.Outputs.TenantServiceInsertArn TenantServiceById: !GetAtt tenant.Outputs.TenantServiceByIdArn TenantServiceUpdate: !GetAtt tenant.Outputs.TenantServiceUpdateArn TenantServiceDelete: !GetAtt tenant.Outputs.TenantServiceDeleteArn @@ -1049,7 +1142,7 @@ Resources: TierServiceGetAll: !GetAtt tier.Outputs.TierServiceGetAllArn TierServiceGetById: !GetAtt tier.Outputs.TierServiceGetByIdArn TierServiceUpdate: !GetAtt tier.Outputs.TierServiceUpdateArn - TierServiceCreate: !GetAtt tier.Outputs.TierServiceCreateArn + TierServiceInsert: !GetAtt tier.Outputs.TierServiceInsertArn TierServiceDelete: !GetAtt tier.Outputs.TierServiceDeleteArn SystemUserServiceGetAll: !GetAtt sysuser.Outputs.SystemUserServiceGetAllArn SystemUserServiceInsert: !GetAtt sysuser.Outputs.SystemUserServiceInsertArn @@ -1058,31 +1151,15 @@ Resources: SystemUserServiceDelete: !GetAtt sysuser.Outputs.SystemUserServiceDeleteArn SystemUserServiceEnable: !GetAtt sysuser.Outputs.SystemUserServiceEnableArn SystemUserServiceDisable: !GetAtt sysuser.Outputs.SystemUserServiceDisableArn - privateapi: - Type: AWS::CloudFormation::Stack - Properties: - TemplateURL: !Sub https://${SaaSBoostBucket}.s3.${AWS::Region}.${AWS::URLSuffix}/saas-boost-private-api.yaml - Parameters: - Environment: !Ref Environment - PrivateApi: !GetAtt core.Outputs.SaaSBoostPrivateApi - RootResourceId: !GetAtt core.Outputs.SaaSBoostPrivateApiRootResourceId - PrivateApiStage: !Ref PrivateApiStage - QuotasServiceCheck: !GetAtt quota.Outputs.QuotasServiceCheckArn - TenantServiceById: !GetAtt tenant.Outputs.TenantServiceByIdArn - TenantServiceInsert: !GetAtt tenant.Outputs.TenantServiceInsertArn - TenantServiceGetAll: !GetAtt tenant.Outputs.TenantServiceGetAllArn - TenantServiceDelete: !GetAtt tenant.Outputs.TenantServiceDeleteArn - SettingsServiceGetAll: !GetAtt settings.Outputs.SettingsServiceGetAllArn - SettingsServiceGetSecret: !GetAtt settings.Outputs.SettingsServiceGetSecretArn - SettingsServiceDeleteAppConfig: !GetAtt settings.Outputs.SettingsServiceDeleteAppConfigArn - SettingsServiceGetAppConfig: !GetAtt settings.Outputs.SettingsServiceGetAppConfigArn - SSMParamMetricsAnalyticsDeployed: + IdentityServiceGetProviders: !GetAtt identity.Outputs.IdentityServiceGetProvidersArn + IdentityServiceGetProvider: !GetAtt identity.Outputs.IdentityServiceGetProviderArn + IdentityServiceSetProvider: !GetAtt identity.Outputs.IdentityServiceSetProviderArn + SSMParamEnvironment: Type: AWS::SSM::Parameter Properties: - Name: !Sub /saas-boost/${Environment}/METRICS_ANALYTICS_DEPLOYED + Name: !Sub /saas-boost/${Environment}/ENVIRONMENT Type: String - # start out as false and will update when stack is deployed separately - Value: 'false' + Value: !Ref Environment SSMParamVersion: Type: AWS::SSM::Parameter Properties: @@ -1092,15 +1169,27 @@ Resources: SSMParamSaaSBoostStack: Type: AWS::SSM::Parameter Properties: - Name: !Sub /saas-boost/${Environment}/SAAS_BOOST_STACK + Name: !Sub /saas-boost/${Environment}/STACK_NAME Type: String Value: !Ref AWS::StackName - SSMParamALBOutputBucket: + SSMParamSaaSBoostBucket: + Type: AWS::SSM::Parameter + Properties: + Name: !Sub /saas-boost/${Environment}/SAAS_BOOST_BUCKET + Type: String + Value: !Ref SaaSBoostBucket + SSMParamLambdaSourceFolder: + Type: AWS::SSM::Parameter + Properties: + Name: !Sub /saas-boost/${Environment}/LAMBDAS_FOLDER + Type: String + Value: !Ref LambdaSourceFolder + SSMParamSaaSBoostEventBus: Type: AWS::SSM::Parameter Properties: - Name: !Sub /saas-boost/${Environment}/ACCESS_LOGS_BUCKET + Name: !Sub /saas-boost/${Environment}/EVENT_BUS Type: String - Value: !Ref AccessLogs + Value: !Ref SaaSBoostEventBus SSMParamCodePipelineBucket: Type: AWS::SSM::Parameter Properties: @@ -1131,6 +1220,24 @@ Resources: Name: !Sub /saas-boost/${Environment}/CFN_UTILS_LAYER Type: String Value: !Ref CloudFormationUtilsLayer + SSMParamApiClientHelperLayer: + Type: AWS::SSM::Parameter + Properties: + Name: !Sub /saas-boost/${Environment}/API_CLIENT_HELPER_LAYER + Type: String + Value: !Ref SaaSBoostApiClientHelperLayer + SSMParamApiClientSecretArn: + Type: AWS::SSM::Parameter + Properties: + Name: !Sub /saas-boost/${Environment}/API_APP_CLIENT_SECRET + Type: String + Value: !GetAtt idp.Outputs.ApiAppClientSecret + SSMParamApiClientSecretKeyArn: + Type: AWS::SSM::Parameter + Properties: + Name: !Sub /saas-boost/${Environment}/API_APP_CLIENT_KEY + Type: String + Value: !GetAtt idp.Outputs.ApiAppClientEncryptionKey Outputs: SaaSBoostBucket: Description: S3 bucket with Saas Boost resources @@ -1150,9 +1257,6 @@ Outputs: AthenaOutputBucket: Description: S3 bucket for Athena queries output Value: !Ref AthenaOutput - AccessLogsBucket: - Description: S3 bucket for ALB access logs - Value: !Ref AccessLogs LoggingBucket: Description: S3 bucket for s3 access logging Value: !Ref Logging @@ -1160,36 +1264,27 @@ Outputs: Description: SaaS Boost Utils Layer Value: !Ref SaaSBoostUtilsLayer ApiGatewayHelperLayer: - Description: SaaS Boost Private API Layer + Description: SaaS Boost API Gateway Helper Layer Value: !Ref ApiGatewayHelperLayer CloudFormationUtilsLayer: Description: SaaS Boost CloudFormation Utils Layer Value: !Ref CloudFormationUtilsLayer + KeycloakHelperLayer: + Description: SaaS Boost Keycloak Helper Layer + Value: !If [UseKeycloak, !Ref KeycloakHelperLayer, ''] EventBus: Description: SaaS Boost Eventbridge Bus Value: !Ref SaaSBoostEventBus - EgressVpc: - Description: Egress VPC Id - Value: !GetAtt network.Outputs.EgressVpc - TransitGateway: - Description: Transit Gateway for Egress to Public Internet - Value: !GetAtt network.Outputs.TransitGateway - TenantTransitGatewayRouteTable: - Description: Transit Gateway Route table for tenant - Value: !GetAtt network.Outputs.TenantTransitGatewayRouteTable - EgressTransitGatewayRouteTable: - Description: Transit Gateway Route table for egress - Value: !GetAtt network.Outputs.EgressTransitGatewayRouteTable - PublicSubnet1: - Description: Public Subnet AZ 1 - Value: !GetAtt network.Outputs.PublicSubnet1 - PublicSubnet2: - Description: Public Subnet AZ 2 - Value: !GetAtt network.Outputs.PublicSubnet2 - PrivateSubnet1: - Description: Private Subnet AZ 1 - Value: !GetAtt network.Outputs.PrivateSubnet1 - PrivateSubnet2: - Description: Private Subnet AZ 2 - Value: !GetAtt network.Outputs.PrivateSubnet2 + CodePipelineUpdateEcsService: + Description: Lambda to update ECS desired count + Value: !Ref CodePipelineUpdateEcsServiceLambda + StartCodeBuildLambda: + Description: Lambda ARN to trigger a CodeBuild project + Value: !GetAtt CodeBuildStartLambda.Arn + CodePipelineWaitHandler: + Description: Lambda to process CloudFormation wait condition as part of a CodePipeline + Value: !Ref CodePipelineWaitHandlerLambda + SaaSBoostApiRootResourceId: + Description: SaaS Boost API root resource id + Value: !GetAtt SaaSBoostApi.RootResourceId ... diff --git a/resources/tenant-onboarding-ad.yaml b/resources/tenant-onboarding-ad.yaml deleted file mode 100644 index 4ffef03a..00000000 --- a/resources/tenant-onboarding-ad.yaml +++ /dev/null @@ -1,78 +0,0 @@ ---- -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -AWSTemplateFormatVersion: 2010-09-09 -Description: AWS SaaS Boost Managed AD -Parameters: - Edition: - Type: String - Default: Standard - Description: AWS Managed Microsoft AD is available in two editions, Standard and Enterprise. - AllowedValues: - - Standard - - Enterprise - Subnets: - Type: List - Description: The subnets to launch the Active Directory. Should be maximum of two subnets. - VpcId: - Type: String - Description: The SaaS Boost VPC ID. - Environment: - Description: SaaS Boost Environment - MaxLength: 30 - MinLength: 1 - Type: String - TenantId: - Description: The GUID for the tenant - Type: String -Resources: - ADCredentials: - Type: AWS::SecretsManager::Secret - Properties: - Name: !Sub /saas-boost/${Environment}/${TenantId}/ACTIVE_DIRECTORY_ADMIN - GenerateSecretString: - IncludeSpace: false - ExcludePunctuation: true - PasswordLength: 12 - GenerateStringKey: password - SecretStringTemplate: !Sub '{"username": "admin"}' - ADDirectory: - Type: AWS::DirectoryService::MicrosoftAD - Properties: - Name: - !Join - - '' - - - 'tenant-' - - !Select [0, !Split ['-', !Ref TenantId]] - - '.' - - !Ref AWS::Region - - '.sb-' - - !Ref Environment - Edition: !Ref Edition - Password: !Sub '{{resolve:secretsmanager:${ADCredentials}:SecretString:password}}' - ShortName: - !Join - - '' - - - 'tenant-' - - !Select [0, !Split ['-', !Ref TenantId]] - VpcSettings: - SubnetIds: !Ref Subnets - VpcId: !Ref VpcId -Outputs: - ActiveDirectoryCredentials: - Description: SecretsManager reference to Active Directory username and password - Value: !Ref ADCredentials - ActiveDirectoryId: - Description: AWS Managed Active Directory ID - Value: !Ref ADDirectory \ No newline at end of file diff --git a/resources/tenant-onboarding-app.yaml b/resources/tenant-onboarding-app.yaml deleted file mode 100644 index 2551bd52..00000000 --- a/resources/tenant-onboarding-app.yaml +++ /dev/null @@ -1,1327 +0,0 @@ ---- -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -AWSTemplateFormatVersion: 2010-09-09 -Description: AWS SaaS Boost Tenant Onboarding Application Service -Parameters: - Environment: - Description: Environment (test, uat, prod, etc.) - Type: String - TenantId: - Description: The GUID for the tenant - Type: String - Tier: - Description: The tier this tenant is onboading into - Type: String - Default: '' - ServiceName: - Description: Name for this application service - Type: String - ServiceResourceName: - Description: CloudFormation friendly version of the service name - Type: String - ContainerRepository: - Description: The name of the ECR repository hosting the container image - Type: String - ContainerRepositoryTag: - Description: The container image tag in the ECR repository - Type: String - Default: latest - ECSCluster: - Description: This tenant's container cluster - Type: String - EnableECSExec: - Description: Enable ECS Exec for this ECS Service? - Type: String - AllowedValues: ['true', 'false'] - Default: 'false' - PubliclyAddressable: - Description: Is this service publicly accessible from the Internet? - Type: String - AllowedValues: ['true', 'false'] - Default: 'true' - PublicPathRoute: - Description: If this service is public, what path part routes to it? - Type: String - Default: '/*' - PublicPathRulePriority: - Description: The ALB listener rule priority for this path route - Type: Number - Default: 1 - VPC: - Description: This tenant's VPC - Type: String - SubnetPrivateA: - Description: Private subnet in this tenant's VPC - Type: String - SubnetPrivateB: - Description: Private subnet in this tenant's VPC - Type: String - PrivateRouteTable: - Description: Route table for the private subnets in this tenant's VPC - Type: String - ServiceDiscoveryNamespace: - Description: ID of the existing Private Service Discovery Namespace for this tenant's VPC - Type: String - ECSLoadBalancerHttpListener: - Description: The ARN of the ALB listener for HTTP traffic - Type: String - ECSLoadBalancerHttpsListener: - Description: The ARN of the ALB listener for HTTPS traffic - Type: String - ECSSecurityGroup: - Description: Source security group to grant access to the container cluster - Type: String - ContainerOS: - Description: Operating System to use for the Docker host - Type: String - # Can't have dashes or underscores in Mappings keys :( - AllowedValues: [WIN2019FULL, WIN2019CORE, WIN2022FULL, WIN2022CORE, WIN2016FULL, LINUX] - ClusterInstanceType: - Description: EC2 instance type to use for non Fargate Docker hosts - Type: String - Default: t2.xlarge - TaskLaunchType: - Description: ECS launch type. Defaults to Fargate. - Type: String - AllowedValues: [FARGATE, EC2] - Default: FARGATE - TaskMemory: - Description: Fargate memory setting - Type: Number - AllowedValues: [512, 1024, 2048, 3072, 4096, 5120, 6144, 7168, 8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384, 17408, 18432, 19456, 20480, 21504, 22528, 23552, 24576, 25600, 26624, 27648, 28672, 29696, 30720] - Default: 1024 - TaskCPU: - Description: Fargate CPU setting - Type: Number - AllowedValues: [256, 512, 1024, 2048, 4096] - Default: 512 - MinTaskCount: - Description: Desired count of concurrent tasks for this tenant - Type: Number - Default: 1 - MaxTaskCount: - Description: Maximum count of concurrent tasks for this tenant (max size we can auto scale up to) - Type: Number - Default: 1 - MinAutoScalingGroupSize: - Description: Minimum count of EC2 instances running in AutoScaling group - Type: Number - Default: 0 - MaxAutoScalingGroupSize: - Description: Maximum count of EC2 instances running in AutoScaling group - Type: Number - Default: 1 - ContainerPort: - Description: The TCP port the container is listening on via EXPOSE in the Dockerfile - Type: Number - ContainerHealthCheckPath: - Description: The destination on the Container for the Load Balancer to use for health checks - Type: String - UseRDS: - Description: Deploy the RDS nested stack? - Type: String - AllowedValues: ['true', 'false'] - Default: 'false' - RDSInstanceClass: - Description: The compute and memory capacity of the DB instance - Type: String - RDSEngine: - Description: The database engine - Type: String - RDSEngineVersion: - Description: The version number of the database engine to use - Type: String - RDSParameterGroupFamily: - Description: The database parameter group family supporting the engine and engine version. Only used for Aurora clusters. - Type: String - RDSUsername: - Description: The master username for the database - Type: String - RDSPasswordParam: - Description: The Parameter Store secure string parameter and version containing the master database password - Type: String - RDSPort: - Description: The TCP port to connect to the database on - Type: String - RDSDatabase: - Description: Optional. The name of the database to create. - Type: String - RDSBootstrap: - Description: Optional. The filename of the SQL bootstrap file. - Type: String - MetricsStream: - Description: Optional. The name of the Firehose delivery stream for the Analytics system. - Type: String - EventBus: - Description: Optional. SaaS boost Metering and Billing EventBridge bus. - Type: String - FileSystemMountPoint: - Description: Container mount point for the file system - Type: String - Default: '' - UseEFS: - Description: Deploy the EFS nested stack? - Type: String - AllowedValues: ['true', 'false'] - Default: 'false' - EncryptEFS: - Description: Turn on EFS encryption at rest? - Type: String - AllowedValues: ['true', 'false'] - Default: 'true' - EFSLifecyclePolicy: - Description: Enable EFS IA lifecycle? - Type: String - AllowedValues: - - NEVER - - AFTER_7_DAYS - - AFTER_14_DAYS - - AFTER_30_DAYS - - AFTER_60_DAYS - - AFTER_90_DAYS - Default: NEVER - UseFSx: - Description: Deploy the FSX nested stack? - Type: String - AllowedValues: ['true', 'false'] - Default: 'false' - ActiveDirectoryId: - Description: AWS Managed Active Directory ID - Type: String - Default: '' - ActiveDirectoryDnsIps: - Description: List of Active Directory DNS IP addresses to add to Windows cluster hosts for FSx - Type: String - Default: '' - ActiveDirectoryDnsName: - Description: Active Directory controller hostname to use when joining Windows cluster hosts to domain for FSx - Type: String - Default: '' - ActiveDirectoryCredentials: - Description: Secrets Manager ARN for active directory username and password - Type: String - Default: '' - FSxFileSystemType: - Description: Type of FSx file system to provision - Type: String - AllowedValues: [FSX_WINDOWS, FSX_ONTAP] - Default: FSX_WINDOWS - FSxWindowsMountDrive: - Description: Windows drive to mount attach the file system to - Type: String - Default: 'G:' - FileSystemStorage: - Description: Storage capacity for the file system in GB - Type: Number - Default: 32 - FileSystemThroughput: - Description: Throughput capacity for the file system in MB/s - Type: Number - Default: 16 - FSxBackupRetention: - Description: Number of days to retain automatic backups - MinValue: 0 - MaxValue: 90 - Default: 0 - Type: Number - FSxDailyBackupTime: - Description: Preferred time to take daily automatic backups, formatted HH:MM in the UTC time zone. - Default: '02:00' - Type: String - FSxWeeklyMaintenanceTime: - Description: Preferred start time to perform weekly maintenance, formatted d:HH:MM in the UTC time zone - Default: '7:01:00' - Type: String - OntapVolumeSize: - Description: Specify the size of the ONTAP volume to create inside the Storage Virtual Machine in MB - Type: Number - Default: 40 - MinValue: 20 - MaxValue: 104857600 - Disable: - Description: Disable the tenant's access to the application - Type: String - AllowedValues: ['true', 'false'] - Default: 'false' - OnboardingDdbTable: - Description: Internal DDB table used to store Onboarding Metadata - Type: String - TenantStorageBucket: - Description: Name of bucket provisioned as part of the S3 Storage extension - Type: String - Default: '' - # These params are here to read the image values from the public SSM. Leave the defaults. - WIN2022FULL: - Type: AWS::SSM::Parameter::Value - Default: /aws/service/ami-windows-latest/Windows_Server-2022-English-Full-ECS_Optimized/image_id - WIN2022CORE: - Type: AWS::SSM::Parameter::Value - Default: /aws/service/ami-windows-latest/Windows_Server-2022-English-Core-ECS_Optimized/image_id - WIN2019FULL: - Type: AWS::SSM::Parameter::Value - Default: /aws/service/ami-windows-latest/Windows_Server-2019-English-Full-ECS_Optimized/image_id - WIN2019CORE: - Type: AWS::SSM::Parameter::Value - Default: /aws/service/ami-windows-latest/Windows_Server-2019-English-Core-ECS_Optimized/image_id - WIN2016FULL: - Type: AWS::SSM::Parameter::Value - Default: /aws/service/ami-windows-latest/Windows_Server-2016-English-Full-ECS_Optimized/image_id - AMZNLINUX2: - Type: AWS::SSM::Parameter::Value - Default: /aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id -Conditions: - IsPublic: !Equals [!Ref PubliclyAddressable, 'true'] - IsPrivate: !Not [Condition: IsPublic] - HasHttpsListener: !Not [!Equals [!Ref ECSLoadBalancerHttpsListener, '']] - IsPublicHttps: !And - - Condition: IsPublic - - Condition: HasHttpsListener - IsPublicHttp: !And - - Condition: IsPublic - - !Not [Condition: HasHttpsListener] - DisableAccess: !Equals [!Ref Disable, 'true'] - ProvisionEFS: !Equals [!Ref UseEFS, 'true'] - ProvisionRDS: !Equals [!Ref UseRDS, 'true'] - Ec2LaunchType: !Equals [!Ref TaskLaunchType, 'EC2'] - FargateLaunchType: !Equals [!Ref TaskLaunchType, 'FARGATE'] - WindowsOS: !Not [!Equals [!Ref ContainerOS, 'LINUX']] - LinuxOS: !Equals [!Ref ContainerOS, 'LINUX'] - Ec2Windows: !And - - Condition: Ec2LaunchType - - Condition: WindowsOS - Ec2Linux: !And - - Condition: Ec2LaunchType - - Condition: LinuxOS - ProvisionFSx: !And - - !Equals [!Ref UseFSx, 'true'] - - Condition: Ec2LaunchType - IsWin2019Full: !Equals [WIN2019FULL, !Ref ContainerOS] - IsWin2019Core: !Equals [WIN2019CORE, !Ref ContainerOS] - IsWin2022Full: !Equals [WIN2022FULL, !Ref ContainerOS] - IsWin2022Core: !Equals [WIN2022CORE, !Ref ContainerOS] - UseS3: !Not [!Equals [!Ref TenantStorageBucket, '']] - EcsExec: !Equals [!Ref EnableECSExec, 'true'] -Resources: - CapacityProvider: - Type: AWS::ECS::CapacityProvider - Condition: Ec2LaunchType - Properties: - Name: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - AutoScalingGroupProvider: - AutoScalingGroupArn: !Ref ECSAutoScalingGroup - ManagedScaling: - TargetCapacity: 100 - Status: ENABLED - ManagedTerminationProtection: ENABLED - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - AttachEcsCapacityProvider: - Type: Custom::CustomResource - Condition: Ec2LaunchType - Properties: - ServiceToken: !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:sb-${Environment}-attach-capacity-provider - CapacityProvider: !Ref CapacityProvider - ECSCluster: !Ref ECSCluster - OnboardingDdbTable: !Ref OnboardingDdbTable - TenantId: !Ref TenantId - CapacityProviderWaitHandle: - Type: AWS::CloudFormation::WaitConditionHandle - Condition: Ec2LaunchType - DependsOn: AttachEcsCapacityProvider - ClusterCapacityAssociationWaitCondition: - Type: AWS::CloudFormation::WaitCondition - Properties: - Handle: !If [Ec2LaunchType, !Ref CapacityProviderWaitHandle, !Ref WaitHandle] - Timeout: '1' - Count: 0 - InvokeDisableInstanceProtection: - Type: Custom::CustomResource - Condition: Ec2LaunchType - DependsOn: - - ECSService - Properties: - ServiceToken: !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:sb-${Environment}-set-instance-protection - AutoScalingGroup: !Ref ECSAutoScalingGroup - Enable: 'false' - ECSTaskExecutionRole: - Type: AWS::IAM::Role - Properties: - RoleName: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-exec-', !Ref ServiceResourceName]] - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - ecs-tasks.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-exec-', !Ref ServiceResourceName]] - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:CreateLogStream - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - ecr:BatchCheckLayerAvailability - - ecr:GetDownloadUrlForLayer - - ecr:BatchGetImage - Resource: - - !Sub arn:${AWS::Partition}:ecr:${AWS::Region}:${AWS::AccountId}:repository/${ContainerRepository} - - Effect: Allow - Action: - - ecr:GetAuthorizationToken - Resource: '*' - - Effect: Allow - Action: - - ssm:GetParameters - Resource: - - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${RDSPasswordParam} - - Effect: Allow - Action: - - s3:GetObject - Resource: - - !Sub 'arn:${AWS::Partition}:s3:::{{resolve:ssm:/saas-boost/${Environment}/RESOURCES_BUCKET}}/tenants/${TenantId}/ServiceDiscovery.env' - - Effect: Allow - Action: - - s3:GetBucketLocation - Resource: - - !Sub 'arn:${AWS::Partition}:s3:::{{resolve:ssm:/saas-boost/${Environment}/RESOURCES_BUCKET}}' - - Effect: Allow - Action: - - fsx:DescribeFileSystems - Resource: - - !Sub arn:${AWS::Partition}:fsx:${AWS::Region}:${AWS::AccountId}:file-system/* - ECSLogGroup: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: - Fn::Join: ['', ['/ecs/sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - RetentionInDays: 30 - ECSTaskRole: - Type: AWS::IAM::Role - Properties: - RoleName: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-task-', !Ref ServiceResourceName]] - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - ecs-tasks.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-task-', !Ref ServiceResourceName]] - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - firehose:PutRecord - - firehose:PutRecordBatch - Resource: - - !Sub arn:${AWS::Partition}:firehose:${AWS::Region}:${AWS::AccountId}:deliverystream/${MetricsStream} - - Effect: Allow - Action: - - events:DescribeEventBus - - events:PutEvents - Resource: - - !Sub arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/${EventBus} - - Effect: Allow - Action: - - s3:GetObject - - s3:GetObjectVersion - Resource: - - !Sub arn:${AWS::Partition}:s3:::{{resolve:ssm:/saas-boost/${Environment}/RESOURCES_BUCKET}}/tenants/${TenantId}/* - - Effect: Allow - Action: - - s3:ListBucket - - s3:ListBucketVersions - Resource: - - !Sub arn:${AWS::Partition}:s3:::${TenantStorageBucket} - Condition: - StringLike: - s3:prefix: - - !Sub ${ServiceResourceName}/${TenantId}/* - - Effect: Allow - Action: - - s3:GetBucket* - - s3:GetAccelerateConfiguration - - s3:GetAnalyticsConfiguration - - s3:GetEncryptionConfiguration - - s3:GetIntelligentTieringConfiguration - - s3:GetInventoryConfiguration - - s3:GetLifecycleConfiguration - - s3:GetMetricsConfiguration - Resource: - - !Sub arn:${AWS::Partition}:s3:::${TenantStorageBucket} - - Effect: Allow - Action: - - s3:GetObject* - - s3:PutObject* - - s3:DeleteObject* - - s3:AbortMultipartUpload - - s3:ListMultipartUploadParts - Resource: - - !Sub arn:${AWS::Partition}:s3:::${TenantStorageBucket}/${ServiceResourceName}/${TenantId}/* - - Effect: !If [EcsExec, 'Allow', 'Deny'] - Action: - - ssmmessages:CreateControlChannel - - ssmmessages:CreateDataChannel - - ssmmessages:OpenControlChannel - - ssmmessages:OpenDataChannel - Resource: - # ssmmessages does not support providing ARNs as resource access restrictors - - '*' - ECSTaskDefinition: - Type: AWS::ECS::TaskDefinition - Properties: - Family: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn - TaskRoleArn: !GetAtt ECSTaskRole.Arn - RequiresCompatibilities: - Fn::If: - - Ec2LaunchType - - - EC2 - - - FARGATE - Memory: !If [FargateLaunchType, !Ref TaskMemory, !Ref 'AWS::NoValue'] - Cpu: !If [FargateLaunchType, !Ref TaskCPU, !Ref 'AWS::NoValue'] - NetworkMode: - Fn::If: - - Ec2Windows - - !Ref 'AWS::NoValue' - - awsvpc - Volumes: - Fn::If: - - ProvisionFSx - - !If - - LinuxOS - - - Name: !GetAtt fsx.Outputs.FileSystemId - Host: - SourcePath: !Sub '/${fsx.Outputs.FileSystemId}' - - - Name: !GetAtt fsx.Outputs.FileSystemId - Host: - SourcePath: !Sub '${FSxWindowsMountDrive}\' - - !If - - ProvisionEFS - - - Name: !GetAtt efs.Outputs.FileSystemId - EFSVolumeConfiguration: - FilesystemId: !GetAtt efs.Outputs.FileSystemId - TransitEncryption: ENABLED - - !Ref 'AWS::NoValue' - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - ContainerDefinitions: - - Name: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/${ContainerRepository}:${ContainerRepositoryTag} - Cpu: !If [Ec2LaunchType, !Ref TaskCPU, !Ref 'AWS::NoValue'] - Memory: !If [Ec2LaunchType, !Ref TaskMemory, !Ref 'AWS::NoValue'] - PortMappings: - - ContainerPort: !Ref ContainerPort - LogConfiguration: - LogDriver: awslogs - Options: - awslogs-group: !Ref ECSLogGroup - awslogs-region: !Ref AWS::Region - awslogs-stream-prefix: ecs - Environment: - - Name: AWS_REGION - Value: !Ref AWS::Region - - Name: SAAS_BOOST_ENV - Value: !Ref Environment - - Name: TENANT_ID - Value: !Ref TenantId - - Name: SAAS_BOOST_RESOURCES_BUCKET - Value: !Sub '{{resolve:ssm:/saas-boost/${Environment}/RESOURCES_BUCKET}}' - - Name: SAAS_BOOST_EVENT_BUS - Value: !Ref EventBus - - Name: METRICS_STREAM - Value: !Ref MetricsStream - - Name: DB_HOST - Value: !If [ProvisionRDS, !GetAtt rds.Outputs.RdsEndpoint, !Ref AWS::NoValue] - - Name: DB_PORT - Value: !If [ProvisionRDS, !Ref RDSPort, !Ref AWS::NoValue] - - Name: DB_NAME - Value: !If [ProvisionRDS, !Ref RDSDatabase, !Ref AWS::NoValue] - - Name: DB_USER - Value: !If [ProvisionRDS, !Ref RDSUsername, !Ref AWS::NoValue] - - Name: S3_BUCKET - Value: !If [UseS3, !Ref TenantStorageBucket, !Ref AWS::NoValue] - - Name: S3_ROOT_PREFIX - Value: !If [UseS3, !Sub '${ServiceResourceName}/', !Ref AWS::NoValue] - EnvironmentFiles: - - Type: s3 - Value: !Sub 'arn:${AWS::Partition}:s3:::{{resolve:ssm:/saas-boost/${Environment}/RESOURCES_BUCKET}}/tenants/${TenantId}/ServiceDiscovery.env' - MountPoints: - Fn::If: - - ProvisionEFS - - - ContainerPath: !Ref FileSystemMountPoint - SourceVolume: !GetAtt efs.Outputs.FileSystemId - - !If - - ProvisionFSx - - - ContainerPath: !Ref FileSystemMountPoint - SourceVolume: !GetAtt fsx.Outputs.FileSystemId - - !Ref 'AWS::NoValue' - LinuxParameters: - Fn::If: - - WindowsOS - - !Ref 'AWS::NoValue' - - InitProcessEnabled: true - Secrets: - Fn::If: - - ProvisionRDS - - - Name: DB_PASSWORD - ValueFrom: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${RDSPasswordParam} - - !Ref 'AWS::NoValue' - InvokeClearTenantStorageBucketPrefix: - Condition: UseS3 - Type: Custom::CustomResource - Properties: - ServiceToken: !Sub '{{resolve:ssm:/saas-boost/${Environment}/CLEAR_BUCKET_ARN}}' - Bucket: !Ref TenantStorageBucket - Prefix: !Sub '${ServiceResourceName}/${TenantId}/' - CodePipeline: - Type: AWS::CodePipeline::Pipeline - Properties: - Name: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - RoleArn: !Sub '{{resolve:ssm:/saas-boost/${Environment}/CODE_PIPELINE_ROLE}}' - ArtifactStore: - Location: !Sub '{{resolve:ssm:/saas-boost/${Environment}/CODE_PIPELINE_BUCKET}}' - Type: S3 - RestartExecutionOnUpdate: false - Stages: - - Name: Source - Actions: - - Name: SourceAction - ActionTypeId: - Category: Source - Owner: AWS - Provider: S3 - Version: '1' - Configuration: - S3Bucket: !Sub '{{resolve:ssm:/saas-boost/${Environment}/CODE_PIPELINE_BUCKET}}' - S3ObjectKey: - Fn::Join: ['', [!Ref TenantId, '/', 'sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - PollForSourceChanges: false - OutputArtifacts: - - Name: imgdef - - Name: Deploy - Actions: - - Name: PreDeployAction - ActionTypeId: - Category: Invoke - Owner: AWS - Provider: Lambda - Version: '1' - RunOrder: 1 - Configuration: - FunctionName: !Sub sb-${Environment}-update-ecs - UserParameters: !Sub '{"cluster":"${ECSCluster}","service":"${ECSService.Name}","desiredCount":${MinTaskCount}}' - - Name: DeployAction - ActionTypeId: - Category: Deploy - Owner: AWS - Provider: ECS - Version: '1' - RunOrder: 2 - Configuration: - ClusterName: !Ref ECSCluster - ServiceName: !GetAtt ECSService.Name - FileName: imagedefinitions.json - InputArtifacts: - - Name: imgdef - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - FsxWaitHandle: - Condition: ProvisionFSx - DependsOn: fsx - Type: AWS::CloudFormation::WaitConditionHandle - WaitHandle: - Type: AWS::CloudFormation::WaitConditionHandle - FsxWaitCondition: - Type: AWS::CloudFormation::WaitCondition - Properties: - Handle: !If [ProvisionFSx, !Ref FsxWaitHandle, !Ref WaitHandle] - Timeout: '1' - Count: 0 - # We either need an Auto Scaling Group, Instance Profile, and Launch Template (Windows/EC2) - # Or we need an Target Group (Linux/Fargate) - # per https://aws.amazon.com/blogs/containers/managing-compute-for-amazon-ecs-clusters-with-capacity-providers/ - ECSAutoScalingGroup: - Type: AWS::AutoScaling::AutoScalingGroup - Condition: Ec2LaunchType - Properties: - AutoScalingGroupName: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - VPCZoneIdentifier: - - !Ref SubnetPrivateA - - !Ref SubnetPrivateB - LaunchTemplate: - LaunchTemplateId: !If [Ec2Windows, !Ref WindowsLaunchTemplate, !Ref LinuxLaunchTemplate] - Version: !If [Ec2Windows, !GetAtt WindowsLaunchTemplate.LatestVersionNumber, !GetAtt LinuxLaunchTemplate.LatestVersionNumber] - NewInstancesProtectedFromScaleIn: true - MinSize: !Ref MinAutoScalingGroupSize - MaxSize: !Ref MaxAutoScalingGroupSize - HealthCheckGracePeriod: 30 - UpdatePolicy: - AutoScalingRollingUpdate: - MinInstancesInService: 1 - MaxBatchSize: 1 - PauseTime: PT5M - WaitOnResourceSignals: false - SuspendProcesses: - - HealthCheck - - ReplaceUnhealthy - - AZRebalance - - AlarmNotification - - ScheduledActions - ECSInstanceRole: - Type: AWS::IAM::Role - Condition: Ec2LaunchType - Properties: - RoleName: - Fn::Join: ['', ['sb-', !Ref Environment, '-ecs-instance-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - ec2.amazonaws.com - Action: - - sts:AssumeRole - ManagedPolicyArns: - - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore - - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMDirectoryServiceAccess - Policies: - - PolicyName: - Fn::Join: ['', ['sb-', !Ref Environment, '-ecs-instance-tenant-', !Select [0, !Split ['-', !Ref TenantId]]]] - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:CreateLogStream - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - ec2:DescribeTags - Resource: - - '*' - - Effect: Allow - Action: - - ecr:BatchCheckLayerAvailability - - ecr:GetDownloadUrlForLayer - - ecr:BatchGetImage - Resource: - - !Sub arn:${AWS::Partition}:ecr:${AWS::Region}:${AWS::AccountId}:repository/${ContainerRepository} - - Effect: Allow - Action: - - ecr:GetAuthorizationToken - Resource: '*' - - Effect: Allow - Action: - - ecs:DeregisterContainerInstance - - ecs:RegisterContainerInstance - - ecs:SubmitAttachmentStateChanges - - ecs:SubmitContainerStateChange - - ecs:SubmitTaskStateChange - Resource: - - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/${ECSCluster} - - Effect: Allow - Action: - - ecs:Poll - - ecs:StartTelemetrySession - Resource: - - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:container-instance/* - Condition: - StringLike: - ecs:cluster: - - !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/${ECSCluster} - - Effect: Allow - Action: - - ecs:DiscoverPollEndpoint - Resource: '*' - - Effect: Allow - Action: - - secretsmanager:GetSecretValue - Resource: - - !Sub arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:/saas-boost/${Environment}/${TenantId}/ACTIVE_DIRECTORY_ADMIN* - - Effect: Allow - Action: - - kms:Decrypt - Resource: !Sub arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/alias/aws/ssm - ECSInstanceProfile: - Type: AWS::IAM::InstanceProfile - Condition: Ec2LaunchType - Properties: - Path: '/' - Roles: - - !Ref ECSInstanceRole - WindowsLaunchTemplate: - Type: AWS::EC2::LaunchTemplate - Condition: Ec2Windows - DependsOn: FsxWaitCondition - Metadata: - AWS::CloudFormation::Init: - config: - files: - c:\cfn\cfn-hup.conf: - content: !Sub | - [main] - stack=${AWS::StackId} - region=${AWS::Region} - c:\cfn\hooks.d\cfn-auto-reloader.conf: - content: !Sub | - [cfn-auto-reloader-hook] - triggers=post.update - path=Resources.WindowsLaunchTemplate.Metadata.AWS::CloudFormation::Init - action=cfn-init.exe -v -s ${AWS::StackName} -r WindowsLaunchTemplate --region ${AWS::Region} - services: - windows: - cfn-hup: - enabled: true - ensureRunning: true - files: - - c:\\cfn\\cfn-hup.conf - - c:\\etc\\cfn\\hooks.d\\cfn-auto-reloader.conf - Properties: - TagSpecifications: - - ResourceType: launch-template - Tags: - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - - Key: Tenant - Value: - !Ref TenantId - - Key: Tier - Value: !Ref Tier - LaunchTemplateData: - ImageId: - Fn::If: - - IsWin2019Full - - !Ref WIN2019FULL - - !If - - IsWin2019Core - - !Ref WIN2019CORE - - !If - - IsWin2022Full - - !Ref WIN2022FULL - - !If - - IsWin2022Core - - !Ref WIN2022CORE - - !Ref WIN2016FULL - InstanceType: !Ref ClusterInstanceType - IamInstanceProfile: - Arn: !GetAtt ECSInstanceProfile.Arn - KeyName: !Ref 'AWS::NoValue' - SecurityGroupIds: - - !Ref ECSSecurityGroup - UserData: - Fn::If: - - ProvisionFSx - - Fn::Base64: - !Sub | - - Start-Transcript -Path "C:\UserData.log" -Append - Write-Output ("Download and install the CloudWatch agent") - $cloudWatchAgentInstaller = "https://s3.${AWS::Region}.${AWS::URLSuffix}/amazoncloudwatch-agent-${AWS::Region}/windows/amd64/latest/amazon-cloudwatch-agent.msi" - If ('${AWS::Partition}' -eq 'aws-cn') { - # CloudWatch Agent installer is not hosted in the Ningxia region - $cloudWatchAgentInstaller = "https://s3.cn-north-1.${AWS::URLSuffix}/amazoncloudwatch-agent/windows/amd64/latest/amazon-cloudwatch-agent.msi" - } - Invoke-WebRequest $cloudWatchAgentInstaller -OutFile c:\Windows\Temp\amazon-cloudwatch-agent.msi - - Start-Process msiexec.exe -Wait -ArgumentList '/I c:\Windows\Temp\amazon-cloudwatch-agent.msi /quiet' - - Write-Output ("Write a config file for the CloudWatch agent and then reload/restart the agent") - cd $Env:ProgramFiles\Amazon\AmazonCloudWatchAgent - .\amazon-cloudwatch-agent-ctl.ps1 -m ec2 -a fetch-config -s - - # Setup the ECS agent to point to our cluster and enable task IAM role and the awslogs driver - Write-Output ("Starting up ECS Agent and joining the ${ECSCluster} cluster") - Import-Module ECSTools - Initialize-ECSAgent -Cluster ${ECSCluster} -EnableTaskIAMRole -LoggingDrivers '["json-file","awslogs"]' - - # If you have task IAM roles, awslogs doesn't work unless you set this environment variable - [Environment]::SetEnvironmentVariable("ECS_ENABLE_AWSLOGS_EXECUTIONROLE_OVERRIDE", $TRUE, "Machine") - Restart-Service AmazonECS - - Write-Output ("Getting Active Directory credentials from Secrets Manager") - $adCredentials = (Get-SECSecretValue -SecretId /saas-boost/${Environment}/${TenantId}/ACTIVE_DIRECTORY_ADMIN -Select 'SecretString') | ConvertFrom-Json - $username = $adCredentials.username - $password = $adCredentials.password | ConvertTo-SecureString -asPlainText -Force - $directoryName = "${ActiveDirectoryDnsName}" - $ipdns = "${ActiveDirectoryDnsIps}" - $ips = $ipdns.Split(",") - - Write-Output ("Reading the existing VPC DNS Server IP") - # Get the VPC DNS server. - $dnsClient = Get-DnsClientServerAddress -AddressFamily IPv4 | Where-Object {$_.ServerAddresses.Count -gt 0} | Select-Object -First 1 - - # During retry, we should avoid adding duplicate DNS servers, if it was already added in the previous attempt. - # VPC DNS server is the last one in the list. - $vpcdns = $dnsClient.ServerAddresses | Select -Last 1 - - # Set up the IPv4 address of the AD DNS server as the first DNS server on this machine - $dnsServersToUpdate = $("{0},{1}" -f $ips[0], $vpcdns) - Write-Output ("Adding AD DNS server addresses: {0} to the IPV4 interface Index: {1}" -f $dnsServersToUpdate, $dnsClient.InterfaceIndex) - Set-DnsClientServerAddress -InterfaceIndex $dnsClient.InterfaceIndex -ServerAddresses $dnsServersToUpdate - - # Join the domain - Write-Output ("Joining domain $directoryName") - $credential = New-Object System.Management.Automation.PSCredential("$directoryName\$username", $password) - Add-Computer -DomainName $directoryName -Credential $credential -Verbose -WarningAction Ignore - - If ('${FSxFileSystemType}' -eq 'FSX_ONTAP') { - $fileServerPath = "\\{0}\C$" -f "${fsx.Outputs.FSxDnsName}" - } - Else { - $fileServerPath = "\\{0}\share" -f "${fsx.Outputs.FSxDnsName}" - } - Write-Output ("Setting FSx File Server remote path to $fileServerPath") - - # Map the share to local drive letter - Write-Output ("Mapping $fileserverpath to ${FSxWindowsMountDrive}") - New-SmbGlobalMapping -RemotePath $fileServerPath -Credential $credential -LocalPath ${FSxWindowsMountDrive} -RequirePrivacy $true -ErrorAction Stop - - Write-Output ("CloudFormation signal completion") - # Signal CloudFormation that we're done setting up - cfn-init.exe -v -s ${AWS::StackName} -r WindowsLaunchTemplate --region ${AWS::Region} - cfn-signal.exe -e %ERRORLEVEL% --stack ${AWS::StackName} --resource ECSAutoScalingGroup --region ${AWS::Region} - - Stop-Transcript - - true - - Fn::Base64: - !Sub | - - Start-Transcript -Path "C:\UserData.log" -Append - Write-Output ("Download and install the CloudWatch agent") - $cloudWatchAgentInstaller = "https://s3.${AWS::Region}.${AWS::URLSuffix}/amazoncloudwatch-agent-${AWS::Region}/windows/amd64/latest/amazon-cloudwatch-agent.msi" - If ('${AWS::Partition}' -eq 'aws-cn') { - # CloudWatch Agent installer is not hosted in the Ningxia region - $cloudWatchAgentInstaller = "https://s3.cn-north-1.${AWS::URLSuffix}/amazoncloudwatch-agent/windows/amd64/latest/amazon-cloudwatch-agent.msi" - } - Invoke-WebRequest $cloudWatchAgentInstaller -OutFile c:\Windows\Temp\amazon-cloudwatch-agent.msi - - Start-Process msiexec.exe -Wait -ArgumentList '/I c:\Windows\Temp\amazon-cloudwatch-agent.msi /quiet' - - Write-Output ("Write a config file for the CloudWatch agent and then reload/restart the agent") - cd $Env:ProgramFiles\Amazon\AmazonCloudWatchAgent - .\amazon-cloudwatch-agent-ctl.ps1 -m ec2 -a fetch-config -s - - # Setup the ECS agent to point to our cluster and enable task IAM role and the awslogs driver - Write-Output ("Starting up ECS Agent and joining the ${ECSCluster} cluster") - Import-Module ECSTools - Initialize-ECSAgent -Cluster ${ECSCluster} -EnableTaskIAMRole -LoggingDrivers '["json-file","awslogs"]' - - # If you have task IAM roles, awslogs doesn't work unless you set this environment variable - [Environment]::SetEnvironmentVariable("ECS_ENABLE_AWSLOGS_EXECUTIONROLE_OVERRIDE", $TRUE, "Machine") - Restart-Service AmazonECS - - Write-Output ("CloudFormation signal completion") - # Signal CloudFormation that we're done setting up - cfn-init.exe -v -s ${AWS::StackName} -r WindowsLaunchTemplate --region ${AWS::Region} - cfn-signal.exe -e %ERRORLEVEL% --stack ${AWS::StackName} --resource ECSAutoScalingGroup --region ${AWS::Region} - - Stop-Transcript - - true - TagSpecifications: - - ResourceType: instance - Tags: - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - - Key: Tenant - Value: - !Ref TenantId - - Key: Tier - Value: !Ref Tier - LinuxLaunchTemplate: - Type: AWS::EC2::LaunchTemplate - Condition: Ec2Linux - DependsOn: FsxWaitCondition - Metadata: - AWS::CloudFormation::Init: - config: - files: - /etc/cfn/cfn-hup.conf: - content: !Sub | - [main] - stack=${AWS::StackId} - region=${AWS::Region} - /etc/cfn/hooks.d/cfn-auto-reloader.conf: - content: !Sub | - [cfn-auto-reloader-hook] - triggers=post.update - path=Resources.LinuxLaunchTemplate.Metadata.AWS::CloudFormation::Init - action=/opt/aws/bin/cfn-init -v -s ${AWS::StackName} -r LinuxLaunchTemplate --region ${AWS::Region} - services: - sysvinit: - cfn-hup: - enabled: true - ensureRunning: true - files: - - /etc/cfn/cfn-hup.conf - - /etc/cfn/hooks.d/cfn-auto-reloader.conf - Properties: - TagSpecifications: - - ResourceType: launch-template - Tags: - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - - Key: Tenant - Value: - !Ref TenantId - - Key: Tier - Value: !Ref Tier - LaunchTemplateData: - ImageId: !Ref AMZNLINUX2 - InstanceType: !Ref ClusterInstanceType - IamInstanceProfile: - Arn: !GetAtt ECSInstanceProfile.Arn - KeyName: !Ref 'AWS::NoValue' - SecurityGroupIds: - - !Ref ECSSecurityGroup - UserData: - Fn::If: - - ProvisionFSx - - Fn::Base64: !Sub | - #!/bin/bash -xe - yum install -y aws-cfn-bootstrap amazon-cloudwatch-agent - - # Setup the CloudWatch logs agent - /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -m ec2 -a fetch-config -s - - # Mount the FSx Volume - if [ "${FSxFileSystemType}" == "FSX_ONTAP" ]; then - mkdir /${fsx.Outputs.FileSystemId} - mount -t nfs ${fsx.Outputs.FSxDnsName}:/vol1 /${fsx.Outputs.FileSystemId} - fi - - # Setup the ECS agent to point to our cluster - echo ECS_CLUSTER="${ECSCluster}" >> /etc/ecs/ecs.config - - # Signal CloudFormation that we're done setting up - /opt/aws/bin/cfn-init -v -s ${AWS::StackName} -r LinuxLaunchTemplate --region ${AWS::Region} - /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource ECSAutoScalingGroup --region ${AWS::Region} - - Fn::Base64: !Sub | - #!/bin/bash -xe - yum install -y aws-cfn-bootstrap amazon-cloudwatch-agent - - # Setup the CloudWatch logs agent - /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -m ec2 -a fetch-config -s - - # Setup the ECS agent to point to our cluster - echo ECS_CLUSTER="${ECSCluster}" >> /etc/ecs/ecs.config - - # Signal CloudFormation that we're done setting up - /opt/aws/bin/cfn-init -v -s ${AWS::StackName} -r LinuxLaunchTemplate --region ${AWS::Region} - /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource ECSAutoScalingGroup --region ${AWS::Region} - TagSpecifications: - - ResourceType: instance - Tags: - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - - Key: Tenant - Value: - !Ref TenantId - - Key: Tier - Value: !Ref Tier - ALBTargetGroup: - Type: AWS::ElasticLoadBalancingV2::TargetGroup - Condition: IsPublic - Properties: - HealthCheckProtocol: HTTP - HealthCheckPath: !Ref ContainerHealthCheckPath - HealthCheckIntervalSeconds: 15 - HealthCheckTimeoutSeconds: 10 - HealthyThresholdCount: 2 - UnhealthyThresholdCount: 5 - Port: !Ref ContainerPort - Protocol: HTTP - TargetType: !If [WindowsOS, instance, ip] - VpcId: !Ref VPC - TargetGroupAttributes: - - Key: stickiness.enabled - Value: 'true' - - Key: stickiness.type - Value: lb_cookie - - Key: stickiness.lb_cookie.duration_seconds - Value: '86400' - - Key: deregistration_delay.timeout_seconds - Value: '30' - Tags: - - Key: Environment - Value: !Ref Environment - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - ALBRule: - Condition: IsPublicHttp - Type: AWS::ElasticLoadBalancingV2::ListenerRule - Properties: - Actions: - - Type: !If [DisableAccess, fixed-response, forward] - TargetGroupArn: !If [DisableAccess, !Ref 'AWS::NoValue', !Ref ALBTargetGroup] - FixedResponseConfig: - !If - - DisableAccess - - ContentType: text/html - StatusCode: '200' - MessageBody: Access to this application is disabled. Contact support if you have questions. - - !Ref 'AWS::NoValue' - Conditions: - - Field: path-pattern - Values: - - !Ref PublicPathRoute - ListenerArn: !Ref ECSLoadBalancerHttpListener - Priority: !Ref PublicPathRulePriority - ALBSSLRule: - Condition: IsPublicHttps - Type: AWS::ElasticLoadBalancingV2::ListenerRule - Properties: - Actions: - - Type: !If [DisableAccess, fixed-response, forward] - TargetGroupArn: !If [DisableAccess, !Ref 'AWS::NoValue', !Ref ALBTargetGroup] - FixedResponseConfig: - !If - - DisableAccess - - ContentType: text/html - StatusCode: '200' - MessageBody: Access to this application is disabled. Contact support if you have questions. - - !Ref 'AWS::NoValue' - Conditions: - - Field: path-pattern - Values: - - !Ref PublicPathRoute - ListenerArn: !Ref ECSLoadBalancerHttpsListener - Priority: !Ref PublicPathRulePriority - ServiceDiscovery: - Type: AWS::ServiceDiscovery::Service - Condition: IsPrivate - Properties: - Name: !Ref ServiceResourceName - DnsConfig: - RoutingPolicy: MULTIVALUE - DnsRecords: - - Type: A - TTL: 60 - - Type: SRV - TTL: 60 - HealthCheckCustomConfig: - FailureThreshold: 1 - NamespaceId: !Ref ServiceDiscoveryNamespace - ECSService: - Type: AWS::ECS::Service - DependsOn: - - ClusterCapacityAssociationWaitCondition - Properties: - EnableExecuteCommand: !Ref EnableECSExec - ServiceName: !Ref ServiceResourceName - Cluster: !Ref ECSCluster - TaskDefinition: !Ref ECSTaskDefinition - PropagateTags: TASK_DEFINITION - LaunchType: !If [FargateLaunchType, !Ref TaskLaunchType, !Ref 'AWS::NoValue'] - # Initially set DesiredCount to zero so the resource stabilizes on create stack. CodePipeline will update it when deploying the task. - DesiredCount: 0 - NetworkConfiguration: - !If - - WindowsOS - - !Ref 'AWS::NoValue' - - AwsvpcConfiguration: - SecurityGroups: - - !Ref ECSSecurityGroup - Subnets: - - !Ref SubnetPrivateA - - !Ref SubnetPrivateB - # Role: The SaaS Boost installer makes sure the ECS service linked role is available - LoadBalancers: - !If - - IsPublic - - - ContainerName: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - ContainerPort: !Ref ContainerPort - TargetGroupArn: !Ref ALBTargetGroup - - !Ref 'AWS::NoValue' - CapacityProviderStrategy: - !If - - Ec2LaunchType - - - CapacityProvider: !Ref CapacityProvider - Base: 1 - Weight: 1 - - !Ref 'AWS::NoValue' - ServiceRegistries: - !If - - IsPrivate - - - RegistryArn: !GetAtt ServiceDiscovery.Arn - ContainerName: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - ContainerPort: !Ref ContainerPort - - !Ref 'AWS::NoValue' - ECSServiceAutoScalingTarget: - Type: AWS::ApplicationAutoScaling::ScalableTarget - Properties: - ResourceId: !Sub service/${ECSCluster}/${ECSService.Name} - RoleARN: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService - ServiceNamespace: ecs - ScalableDimension: ecs:service:DesiredCount - MaxCapacity: !Ref MaxTaskCount - MinCapacity: !Ref MinTaskCount - ECSServiceCPUAutoScalingPolicy: - Type: AWS::ApplicationAutoScaling::ScalingPolicy - Properties: - PolicyName: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-autoscaling-policy-cpu', '-', !Ref ServiceResourceName]] - PolicyType: TargetTrackingScaling - ScalingTargetId: !Ref ECSServiceAutoScalingTarget - TargetTrackingScalingPolicyConfiguration: - ScaleOutCooldown: 60 # How long should we wait for a scale out activity to complete? - ScaleInCooldown: 120 # How long should we wait in between scale in activities? - PredefinedMetricSpecification: - PredefinedMetricType: ECSServiceAverageCPUUtilization - TargetValue: 65 - ECSServiceMemoryAutoScalingPolicy: - Type: AWS::ApplicationAutoScaling::ScalingPolicy - Properties: - PolicyName: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-autoscaling-policy-mem', '-', !Ref ServiceResourceName]] - PolicyType: TargetTrackingScaling - ScalingTargetId: !Ref ECSServiceAutoScalingTarget - TargetTrackingScalingPolicyConfiguration: - ScaleOutCooldown: 60 # How long should we wait for a scale out activity to complete? - ScaleInCooldown: 120 # How long should we wait in between scale in activities? - PredefinedMetricSpecification: - PredefinedMetricType: ECSServiceAverageMemoryUtilization - TargetValue: 85 - fsx: - Type: AWS::CloudFormation::Stack - Condition: ProvisionFSx - Properties: - TemplateURL: !Sub 'https://{{resolve:ssm:/saas-boost/${Environment}/SAAS_BOOST_BUCKET}}.s3.${AWS::Region}.${AWS::URLSuffix}/tenant-onboarding-fsx.yaml' - Parameters: - Environment: !Ref Environment - TenantId: !Ref TenantId - ServiceResourceName: !Ref ServiceResourceName - ActiveDirectoryId: !Ref ActiveDirectoryId - ActiveDirectoryDnsIps: !Ref ActiveDirectoryDnsIps - ActiveDirectoryDnsName: !Ref ActiveDirectoryDnsName - ActiveDirectoryCredentials: !Ref ActiveDirectoryCredentials - PrivateSubnetA: !Ref SubnetPrivateA - PrivateSubnetB: !Ref SubnetPrivateB - PrivateRouteTable: !Ref PrivateRouteTable - VPC: !Ref VPC - SaaSBoostBucket: !Sub '{{resolve:ssm:/saas-boost/${Environment}/SAAS_BOOST_BUCKET}}' - FileSystemType: !Ref FSxFileSystemType - ContainerOS: !Ref ContainerOS - BackupRetention: !Ref FSxBackupRetention - DailyBackupTime: !Ref FSxDailyBackupTime - WeeklyMaintenanceTime: !Ref FSxWeeklyMaintenanceTime - StorageCapacity: !Ref FileSystemStorage - ThroughputCapacity: !Ref FileSystemThroughput - ECSSecurityGroup: !Ref ECSSecurityGroup - OntapVolumeSize: !Ref OntapVolumeSize - Tier: !Ref Tier - efs: - Type: AWS::CloudFormation::Stack - Condition: ProvisionEFS - Properties: - TemplateURL: !Sub 'https://{{resolve:ssm:/saas-boost/${Environment}/SAAS_BOOST_BUCKET}}.s3.${AWS::Region}.${AWS::URLSuffix}/tenant-onboarding-efs.yaml' - Parameters: - Environment: !Ref Environment - TenantId: !Ref TenantId - ServiceResourceName: !Ref ServiceResourceName - VPC: !Ref VPC - PrivateSubnetA: !Ref SubnetPrivateA - PrivateSubnetB: !Ref SubnetPrivateB - ECSSecurityGroup: !Ref ECSSecurityGroup - EncryptEFS: !Ref EncryptEFS - EFSLifecyclePolicy: !Ref EFSLifecyclePolicy - Tier: !Ref Tier - rds: - Type: AWS::CloudFormation::Stack - Condition: ProvisionRDS - Properties: - TemplateURL: !Sub 'https://{{resolve:ssm:/saas-boost/${Environment}/SAAS_BOOST_BUCKET}}.s3.${AWS::Region}.${AWS::URLSuffix}/tenant-onboarding-rds.yaml' - Parameters: - Environment: !Ref Environment - SaaSBoostBucket: !Sub '{{resolve:ssm:/saas-boost/${Environment}/SAAS_BOOST_BUCKET}}' - TenantId: !Ref TenantId - ServiceResourceName: !Ref ServiceResourceName - VPC: !Ref VPC - PrivateSubnetA: !Ref SubnetPrivateA - PrivateSubnetB: !Ref SubnetPrivateB - ECSSecurityGroup: !Ref ECSSecurityGroup - RDSInstanceClass: !Ref RDSInstanceClass - RDSEngine: !Ref RDSEngine - RDSEngineVersion: !Ref RDSEngineVersion - RDSParameterGroupFamily: !Ref RDSParameterGroupFamily - RDSUsername: !Ref RDSUsername - RDSPasswordParam: !Ref RDSPasswordParam - RDSPort: !Ref RDSPort - RDSDatabase: !Ref RDSDatabase - RDSBootstrap: !Ref RDSBootstrap - Tier: !Ref Tier -Outputs: - RdsEndpoint: - Condition: ProvisionRDS - Description: RDS endpoint - Value: !GetAtt rds.Outputs.RdsEndpoint diff --git a/resources/tenant-onboarding-efs.yaml b/resources/tenant-onboarding-efs.yaml deleted file mode 100644 index eb7705f5..00000000 --- a/resources/tenant-onboarding-efs.yaml +++ /dev/null @@ -1,114 +0,0 @@ ---- -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -AWSTemplateFormatVersion: 2010-09-09 -Description: SaaS Boost Tenant Onboarding EFS Extension -Parameters: - Environment: - Description: Environment (test, uat, prod, etc.) - Type: String - TenantId: - Description: The GUID for the tenant - Type: String - Tier: - Description: The tier this tenant is onboading into - Type: String - Default: '' - ServiceResourceName: - Description: CloudFormation friendly version of the service name - Type: String - VPC: - Description: VPC id for this tenant - Type: AWS::EC2::VPC::Id - PrivateSubnetA: - Description: Private subnet for EFS mount target - Type: AWS::EC2::Subnet::Id - PrivateSubnetB: - Description: Private subnet for EFS mount target - Type: AWS::EC2::Subnet::Id - ECSSecurityGroup: - Description: Source security group for ECS to access EFS - Type: AWS::EC2::SecurityGroup::Id - EncryptEFS: - Description: Turn on EFS encryption at rest? - Type: String - AllowedValues: ['true', 'false'] - Default: 'true' - EFSLifecyclePolicy: - Description: Enable EFS IA lifecycle? - Type: String - AllowedValues: - - NEVER - - AFTER_7_DAYS - - AFTER_14_DAYS - - AFTER_30_DAYS - - AFTER_60_DAYS - - AFTER_90_DAYS - Default: NEVER -Conditions: - HasEFSLifecylePolicy: !Not [!Equals [!Ref EFSLifecyclePolicy, NEVER]] -Resources: - EFSSecurityGroup: - Type: AWS::EC2::SecurityGroup - Properties: - GroupName: - Fn::Join: ['', ['sb-', !Ref Environment, '-efs-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - GroupDescription: EFS mount point Security Group - VpcId: !Ref VPC - EFSSecurityGroupIngressECS: - Type: AWS::EC2::SecurityGroupIngress - Properties: - GroupId: !Ref EFSSecurityGroup - IpProtocol: tcp - FromPort: 2049 - ToPort: 2049 - SourceSecurityGroupId: !Ref ECSSecurityGroup - EFSFileSystem: - Type: AWS::EFS::FileSystem - Properties: - Encrypted: !Ref EncryptEFS - PerformanceMode: generalPurpose - ThroughputMode: bursting - LifecyclePolicies: - Fn::If: - - HasEFSLifecylePolicy - - - TransitionToIA: !Ref EFSLifecyclePolicy - - !Ref 'AWS::NoValue' - FileSystemTags: - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - MountTargetA: - Type: AWS::EFS::MountTarget - Properties: - FileSystemId: !Ref EFSFileSystem - SecurityGroups: - - !Ref EFSSecurityGroup - SubnetId: !Ref PrivateSubnetA - MountTargetB: - Type: AWS::EFS::MountTarget - Properties: - FileSystemId: !Ref EFSFileSystem - SecurityGroups: - - !Ref EFSSecurityGroup - SubnetId: !Ref PrivateSubnetB -Outputs: - FileSystemId: - Description: EFS File System Id - Value: !Ref EFSFileSystem -... \ No newline at end of file diff --git a/resources/tenant-onboarding-fsx.yaml b/resources/tenant-onboarding-fsx.yaml deleted file mode 100644 index b59b3515..00000000 --- a/resources/tenant-onboarding-fsx.yaml +++ /dev/null @@ -1,628 +0,0 @@ ---- -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -AWSTemplateFormatVersion: 2010-09-09 -Description: AWS SaaS Boost Tenant Onboarding FSx for Windows File Server Extension -Parameters: - Environment: - Description: Environment (test, uat, prod, etc.) - Type: String - Tier: - Description: The tier this tenant is onboading into - Type: String - Default: '' - TenantId: - Description: The GUID for the tenant - Type: String - SaaSBoostBucket: - Description: SaaS Boost assets S3 bucket - Type: String - ServiceResourceName: - Description: CloudFormation friendly version of the service name - Type: String - VPC: - Description: VPC id for this tenant - Type: AWS::EC2::VPC::Id - PrivateSubnetA: - Description: Choose the Id of the private subnet 1 in Availability Zone 1 (e.g., subnet-a0246dcd). - Type: AWS::EC2::Subnet::Id - PrivateSubnetB: - Description: Choose the Id of the private subnet 2 in Availability Zone 2 (e.g., subnet-a0246dcd). - Type: AWS::EC2::Subnet::Id - PrivateRouteTable: - Description: Route table for the private subnets in this tenant's VPC - Type: String - ECSSecurityGroup: - Description: Source security group of ECS instances - Type: AWS::EC2::SecurityGroup::Id - ActiveDirectoryCredentials: - Description: Secrets Manager ARN for active directory username and password - Type: String - Default: '' - ActiveDirectoryId: - Description: Id of the AWS Managed Microsoft AD. If you are using self-managed Active Directory, leave this blank. - Type: String - Default: '' - ActiveDirectoryDnsIps: - Description: Comma delimited string of DNS IP addresses for the Active Directory instance to join the SVM to for FSx ONTAP - Type: String - Default: '' - ActiveDirectoryDnsName: - Description: Active Directory controller hostname to use when joining Windows cluster hosts to domain for FSx - Type: String - Default: '' - FileSystemType: - Description: FSx file system type - Type: String - AllowedValues: [FSX_WINDOWS, FSX_ONTAP] - Default: FSX_WINDOWS - ContainerOS: - Description: Operating System to use for the Docker host - Type: String - # Can't have dashes or underscores in Mappings keys :( - AllowedValues: [WIN2019FULL, WIN2019CORE, WIN2022FULL, WIN2022CORE, WIN20H2CORE, WIN2016FULL, LINUX] - FSxEncryptionKey: - Description: Use the default AWS Key Management Service (AWS KMS) key for Amazon FSx, choose GenerateKey to create a key, - or choose UseKey to use an existing key for encryption at rest on the Amazon FSx for Windows file system. - Type: String - AllowedValues: [Default, GenerateKey, UseKey] - Default: Default - FSxExistingKeyID: - Description: If you chose the option to use an existing key, you must specify the KMS Key ID you want to use. - Type: String - Default: '' - StorageCapacity: - Description: Specify the storage capacity of the file system being created in GB - Type: Number - Default: 32 - ThroughputCapacity: - Description: Specify the throughput of the Amazon FSx file system - Type: Number - Default: 16 - BackupRetention: - Description: Number of days to retain automatic backups - Type: Number - Default: 7 - DailyBackupTime: - Description: Preferred time to take daily automatic backups, formatted HH:MM in the UTC time zone. - Type: String - Default: '02:00' - WeeklyMaintenanceTime: - Description: Specify the preferred start time to perform weekly maintenance, formatted d:HH:MM in the UTC time zone - Type: String - Default: '7:01:00' - OntapVolumeSize: - Description: Specify the size of the ONTAP volume to create inside the Storage Virtual Machine in MB - Type: Number - Default: 40 - MinValue: 20 - MaxValue: 104857600 -Conditions: - HasKey: !Equals [!Ref FSxEncryptionKey, 'UseKey'] - CreateKey: !Equals [!Ref FSxEncryptionKey, 'GenerateKey'] - UseNonDefault: !Not [!Equals [!Ref FSxEncryptionKey, 'Default']] - IsLinux: !Equals [!Ref ContainerOS, 'LINUX'] - IsWindows: !Not [!Equals [!Ref ContainerOS, 'LINUX']] - FSxWindows: !Equals [!Ref FileSystemType, 'FSX_WINDOWS'] - FSxONTAP: !Equals [!Ref FileSystemType, 'FSX_ONTAP'] - FSxONTAPWindows: !And - - Condition: FSxONTAP - - Condition: IsWindows - FSxONTAPLinux: !And - - Condition: FSxONTAP - - Condition: IsLinux - UseManagedActiveDirectory: !Not [!Equals [!Ref ActiveDirectoryId, '']] - HasActiveDirectory: !And - - !Not [!Equals [!Ref ActiveDirectoryDnsIps, '']] - - !Not [!Equals [!Ref ActiveDirectoryDnsName, '']] -Resources: - FSxKMSKey: - Condition: CreateKey - DeletionPolicy: Delete - UpdateReplacePolicy: Retain - Type: AWS::KMS::Key - Properties: - KeyPolicy: - Version: 2012-10-17 - Id: !Sub sb-${Environment}-fsx-key-${TenantId}-${ServiceResourceName} - Statement: - - Effect: Allow - Principal: - AWS: - - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root - Action: kms:* - Resource: '*' - - Effect: Allow - Principal: - AWS: '*' - Action: - - kms:Encrypt - - kms:Decrypt - - kms:ReEncrypt* - - kms:GenerateDataKey* - - kms:CreateGrant - - kms:ListGrants - - kms:DescribeKey - Resource: '*' - Condition: - StringEquals: - kms:ViaService: !Sub fsx.${AWS::Region}.amazonaws.com - kms:CallerAccount: !Ref 'AWS::AccountId' - Tags: - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-fsx-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - FSxKeyAlias: - Condition: CreateKey - Type: AWS::KMS::Alias - Properties: - AliasName: - Fn::Join: ['', ['alias/sb-', !Ref Environment, '-fsx-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - TargetKeyId: !Ref FSxKMSKey - FSxActiveDirectorySecurityGroup: - Type: AWS::EC2::SecurityGroup - Properties: - VpcId: !Ref VPC - GroupDescription: FSx Active Directory Security Group - GroupName: - Fn::Join: ['', ['sb-', !Ref Environment, '-fsx-ad-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - SecurityGroupIngress: - - IpProtocol: tcp - FromPort: 53 - ToPort: 53 - Description: DNS - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: udp - FromPort: 53 - ToPort: 53 - Description: DNS - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 88 - ToPort: 88 - Description: Kerberos authentication - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: udp - FromPort: 88 - ToPort: 88 - Description: Kerberos authentication - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: udp - FromPort: 123 - ToPort: 123 - Description: NTP - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 135 - ToPort: 135 - Description: DCE / EPMAP - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: udp - FromPort: 389 - ToPort: 389 - Description: LDAP - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 389 - ToPort: 389 - Description: LDAP - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 445 - ToPort: 445 - Description: Directory Services SMB file sharing - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: udp - FromPort: 464 - ToPort: 464 - Description: Change or Set password - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 464 - ToPort: 464 - Description: Change or Set password - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 636 - ToPort: 636 - Description: LDAP over SSL - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 3268 - ToPort: 3268 - Description: Microsoft Global Catalog - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 3269 - ToPort: 3269 - Description: Microsoft Global Catalog over SSL - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 5985 - ToPort: 5985 - Description: WinRM 2.0 - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 9389 - ToPort: 9389 - Description: Microsoft AD DS Web Services and PowerShell - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 49152 - ToPort: 65535 - Description: RPC ephemeral ports - SourceSecurityGroupId: !Ref ECSSecurityGroup - FSxOntapSecurityGroup: - Type: AWS::EC2::SecurityGroup - Condition: FSxONTAP - Properties: - VpcId: !Ref VPC - GroupDescription: FSx ONTAP Security Group - GroupName: - Fn::Join: ['', ['sb-', !Ref Environment, '-fsx-ontap-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - SecurityGroupIngress: - - IpProtocol: icmp - FromPort: -1 - ToPort: -1 - Description: ICMP Ping - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 22 - ToPort: 22 - Description: SSH access to cluster or node management LIF - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 111 - ToPort: 111 - Description: Remote procedure call for NFS - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: udp - FromPort: 111 - ToPort: 111 - Description: Remote procedure call for NFS - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 135 - ToPort: 135 - Description: Remote procedure call for CIFS - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: udp - FromPort: 135 - ToPort: 135 - Description: Remote procedure call for CIFS - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: udp - FromPort: 137 - ToPort: 137 - Description: NetBIOS name resolution for CIFS - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 139 - ToPort: 139 - Description: NetBIOS service session for CIFS - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: udp - FromPort: 139 - ToPort: 139 - Description: NetBIOS service session for CIFS - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 161 - ToPort: 162 - Description: SNMP - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: udp - FromPort: 161 - ToPort: 162 - Description: SNMP - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 443 - ToPort: 443 - Description: ONTAP REST API access for cluster management LIF or SVM management LIF - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 635 - ToPort: 635 - Description: NFS mount - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: udp - FromPort: 635 - ToPort: 635 - Description: NFS mount - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 749 - ToPort: 749 - Description: Kerberos - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 2049 - ToPort: 2049 - Description: NFS server daemon - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: udp - FromPort: 2049 - ToPort: 2049 - Description: NFS server daemon - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 3260 - ToPort: 3260 - Description: iSCSI to the iSCSI data LIF - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 4045 - ToPort: 4045 - Description: NFS lock daemon - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: udp - FromPort: 4045 - ToPort: 4045 - Description: NFS lock daemon - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 4046 - ToPort: 4046 - Description: NFS network status monitor - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: udp - FromPort: 4046 - ToPort: 4046 - Description: NFS network status monitor - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: udp - FromPort: 4049 - ToPort: 4049 - Description: NFS quota protocol - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 10000 - ToPort: 10000 - Description: NDMP and NetApp SnapMirror - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 11104 - ToPort: 11104 - Description: NetApp SnapMirror management - SourceSecurityGroupId: !Ref ECSSecurityGroup - - IpProtocol: tcp - FromPort: 11105 - ToPort: 11105 - Description: NetApp SnapMirror data transfer - SourceSecurityGroupId: !Ref ECSSecurityGroup - FSxFileSystem: - Type: AWS::FSx::FileSystem - Properties: - FileSystemType: !If - - FSxWindows - - WINDOWS - - !If - - FSxONTAP - - ONTAP - - !Ref 'AWS::NoValue' - KmsKeyId: !If - - UseNonDefault - - !If - - HasKey - - !Ref FSxExistingKeyID - - !Ref FSxKMSKey - - !Ref 'AWS::NoValue' - StorageCapacity: !Ref StorageCapacity - SubnetIds: - - !Ref PrivateSubnetA - - !Ref PrivateSubnetB - SecurityGroupIds: - - !Ref FSxActiveDirectorySecurityGroup - - !If [FSxONTAP, !Ref FSxOntapSecurityGroup, !Ref 'AWS::NoValue'] - Tags: - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - WindowsConfiguration: !If - - FSxWindows - - - ActiveDirectoryId: !If [UseManagedActiveDirectory, !Ref ActiveDirectoryId, !Ref 'AWS::NoValue'] - WeeklyMaintenanceStartTime: !Ref WeeklyMaintenanceTime - DailyAutomaticBackupStartTime: !Ref DailyBackupTime - AutomaticBackupRetentionDays: !Ref BackupRetention - DeploymentType: MULTI_AZ_1 - PreferredSubnetId: !Ref PrivateSubnetA - ThroughputCapacity: !Ref ThroughputCapacity - - !Ref 'AWS::NoValue' - OntapConfiguration: !If - - FSxONTAP - - - WeeklyMaintenanceStartTime: !Ref WeeklyMaintenanceTime - DailyAutomaticBackupStartTime: !Ref DailyBackupTime - AutomaticBackupRetentionDays: !Ref BackupRetention - DeploymentType: MULTI_AZ_1 - PreferredSubnetId: !Ref PrivateSubnetA - ThroughputCapacity: !Ref ThroughputCapacity - RouteTableIds: !Split [',', !Ref PrivateRouteTable] - - !Ref 'AWS::NoValue' - StorageVirtualMachine: - Type: AWS::FSx::StorageVirtualMachine - Condition: FSxONTAP - Properties: - FileSystemId: !Ref FSxFileSystem - Name: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - RootVolumeSecurityStyle: !If [FSxONTAPLinux, 'UNIX', 'NTFS'] - ActiveDirectoryConfiguration: !If - - HasActiveDirectory - - - # Max string length 15. Since this is bound to a SVM and a File System, we don't need the env or service name. - NetBiosName: - Fn::Join: ['', ['tenant-', !Select [0, !Split ['-', !Ref TenantId]]]] - # TODO how do we support non SaaS Boost provisioned AD instances? - SelfManagedActiveDirectoryConfiguration: - OrganizationalUnitDistinguishedName: - !Join - - '' - - - 'OU=Computers,OU=tenant-' - - !Select [0, !Split ['-', !Ref TenantId]] - - ',DC=' - - !Select [0, !Split ['.', !Ref ActiveDirectoryDnsName]] - - ',DC=' - - !Select [1, !Split ['.', !Ref ActiveDirectoryDnsName]] - - ',DC=' - - !Select [2, !Split ['.', !Ref ActiveDirectoryDnsName]] - FileSystemAdministratorsGroup: AWS Delegated FSx Administrators - # Domain name is limited to 47 characters by FSx - DomainName: !Ref ActiveDirectoryDnsName - # Must pass the comma delimited string in as a parameter, because {{resolve:ssm:}} can't - # be nested inside a Fn::Split. The split happens before the resolution of the parameter. - DnsIps: !Split [',', !Ref ActiveDirectoryDnsIps] - # Don't use Domain\Username here - only Username - UserName: !Sub '{{resolve:secretsmanager:${ActiveDirectoryCredentials}:SecretStrings:username}}' - # Can't use ssm-secure here - Password: !Sub '{{resolve:secretsmanager:${ActiveDirectoryCredentials}:SecretString:password}}' - - !Ref 'AWS::NoValue' - Tags: - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - StorageVolume: - Type: AWS::FSx::Volume - Condition: FSxONTAP - Properties: - VolumeType: ONTAP - Name: vol1 - OntapConfiguration: - JunctionPath: /vol1 - SecurityStyle: !If [FSxONTAPLinux, 'UNIX', 'NTFS'] - SizeInMegabytes: !Ref OntapVolumeSize - StorageEfficiencyEnabled: 'false' # Quoted to make cfn-lint happy - StorageVirtualMachineId: !Ref StorageVirtualMachine - Tags: - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - FsxDnsNameRole: - Type: AWS::IAM::Role - Properties: - RoleName: - Fn::Join: ['', ['sb-', !Ref Environment, '-fsx-dns-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName, '-', !Ref AWS::Region]] - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: - Fn::Join: ['', ['sb-', !Ref Environment, '-fsx-dns-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:DescribeLogGroups - - logs:DescribeLogStreams - - logs:CreateLogStream - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - ec2:CreateNetworkInterface - - ec2:DescribeNetworkInterfaces - - ec2:DeleteNetworkInterface - - fsx:DescribeFileSystems - - fsx:DescribeStorageVirtualMachines - Resource: '*' - FsxDnsNameLogs: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: - Fn::Join: ['', ['/aws/lambda/sb-', !Ref Environment, '-fsx-dns-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - RetentionInDays: 30 - FsxDnsNameFunction: - Type: AWS::Lambda::Function - DependsOn: - - FsxDnsNameLogs - Properties: - FunctionName: - Fn::Join: ['', ['sb-', !Ref Environment, '-fsx-dns-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - Role: !GetAtt FsxDnsNameRole.Arn - Runtime: java11 - Timeout: 870 - MemorySize: 640 - # Has to be a VPC Lambda because we're talking to FSx - VpcConfig: - SecurityGroupIds: - - !Ref FSxActiveDirectorySecurityGroup - - !If [FSxONTAP, !Ref FSxOntapSecurityGroup, !Ref 'AWS::NoValue'] - SubnetIds: - - !Ref PrivateSubnetA - - !Ref PrivateSubnetB - Handler: com.amazon.aws.partners.saasfactory.saasboost.FsxDnsName - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub '{{resolve:ssm:/saas-boost/${Environment}/SAAS_BOOST_LAMBDAS_FOLDER}}/FsxDnsName-lambda.zip' - Layers: - - !Sub '{{resolve:ssm:/saas-boost/${Environment}/UTILS_LAYER}}' - - !Sub '{{resolve:ssm:/saas-boost/${Environment}/CFN_UTILS_LAYER}}' - Environment: - Variables: - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - InvokeGetFsxDnsName: - Type: Custom::CustomResource - DependsOn: - - FsxDnsNameLogs - Properties: - ServiceToken: !GetAtt FsxDnsNameFunction.Arn - FsxFileSystemId: !Ref FSxFileSystem - StorageVirtualMachineId: !If [FSxONTAP, !Ref StorageVirtualMachine, ''] - VolumeSecurityStyle: !If [FSxONTAP, !If [FSxONTAPLinux, 'UNIX', ''], ''] -Outputs: - FileSystemId: - Description: FSx File System ID - Value: !Ref FSxFileSystem - FSxDnsName: - Value: !GetAtt InvokeGetFsxDnsName.DnsName - Description: FSx File Server DNS Name diff --git a/resources/tenant-onboarding-rds.yaml b/resources/tenant-onboarding-rds.yaml deleted file mode 100644 index 65fabeb5..00000000 --- a/resources/tenant-onboarding-rds.yaml +++ /dev/null @@ -1,407 +0,0 @@ ---- -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -AWSTemplateFormatVersion: 2010-09-09 -Description: SaaS Boost Tenant Onboarding RDS Extension -Parameters: - Environment: - Description: Environment (test, uat, prod, etc.) - Type: String - SaaSBoostBucket: - Description: SaaS Boost assets S3 bucket - Type: String - TenantId: - Description: The GUID for the tenant - Type: String - ServiceResourceName: - Description: CloudFormation friendly version of the service name - Type: String - VPC: - Description: VPC id for this tenant - Type: AWS::EC2::VPC::Id - PrivateSubnetA: - Description: Private subnet for EFS mount target - Type: AWS::EC2::Subnet::Id - PrivateSubnetB: - Description: Private subnet for EFS mount target - Type: AWS::EC2::Subnet::Id - ECSSecurityGroup: - Description: Source security group of ECS instances - Type: AWS::EC2::SecurityGroup::Id - RDSInstanceClass: - Description: The compute and memory capacity of the DB instance - Type: String - RDSEngine: - Description: The database engine - Type: String - RDSEngineVersion: - Description: The version number of the database engine to use - Type: String - RDSParameterGroupFamily: - Description: The database parameter group family supporting the engine and engine version. Only used for Aurora clusters. - Type: String - RDSUsername: - Description: The username for the database - Type: String - RDSPasswordParam: - Description: The Parameter Store secure string parameter and version containing the database password - Type: String - RDSPort: - Description: The TCP port to connect to the database on - Type: String - RDSDatabase: - Description: Optional. The name of the database to create. - Type: String - RDSBootstrap: - Description: Optional. The filename of the SQL bootstrap file. - Type: String - Tier: - Description: The tier this tenant is onboading into - Type: String - Default: '' -Conditions: - Aurora: - Fn::Or: - - !Equals [!Ref RDSEngine, 'aurora-mysql'] - - !Equals [!Ref RDSEngine, 'aurora-postgresql'] - NotAurora: !Not [Condition: Aurora] - SqlServer: - Fn::Or: - - !Equals [!Ref RDSEngine, 'sqlserver-ex'] - - !Equals [!Ref RDSEngine, 'sqlserver-web'] - - !Equals [!Ref RDSEngine, 'sqlserver-se'] - - !Equals [!Ref RDSEngine, 'sqlserver-ee'] - DatabaseName: !Not [!Equals [!Ref RDSDatabase, '']] - CreateDatabase: - Fn::And: - - Condition: SqlServer - - Condition: DatabaseName - BootstrapFile: !Not [!Equals [!Ref RDSBootstrap, '']] - BootstrapDatabase: - Fn::Or: - - Condition: CreateDatabase - - Condition: BootstrapFile - SupportsEncryption: !Not [!Equals [!Ref RDSEngine, 'sqlserver-ex']] -Resources: - RDSSubnetGroup: - Type: AWS::RDS::DBSubnetGroup - Properties: - DBSubnetGroupDescription: - Fn::Join: ['', ['sb-', !Ref Environment, '-rds-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - DBSubnetGroupName: - Fn::Join: ['', ['sb-', !Ref Environment, '-rds-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - SubnetIds: - - !Ref PrivateSubnetA - - !Ref PrivateSubnetB - RDSSecurityGroup: - Type: AWS::EC2::SecurityGroup - Properties: - GroupName: - Fn::Join: ['', ['sb-', !Ref Environment, '-rds-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - GroupDescription: RDS Security Group - VpcId: !Ref VPC - RDSSecurityGroupIngressECS: - Type: AWS::EC2::SecurityGroupIngress - Properties: - GroupId: !Ref RDSSecurityGroup - IpProtocol: tcp - FromPort: !Ref RDSPort - ToPort: !Ref RDSPort - SourceSecurityGroupId: !Ref ECSSecurityGroup - RDSBootstrapSecurityGroup: - Type: AWS::EC2::SecurityGroup - Condition: BootstrapDatabase - Properties: - GroupName: - Fn::Join: ['', ['sb-', !Ref Environment, '-rds-bootstrap-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - GroupDescription: RDS Security Group - VpcId: !Ref VPC - SecurityGroupEgress: - - CidrIp: 0.0.0.0/0 - IpProtocol: '-1' - EncryptionKey: - Condition: SupportsEncryption - Type: AWS::KMS::Key - Properties: - KeyPolicy: - Version: 2012-10-17 - Id: !Sub sb-${Environment}-rds-key-${TenantId}-${ServiceResourceName} - Statement: - - Effect: Allow - Principal: - AWS: - - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root - Action: kms:* - Resource: '*' - - Effect: Allow - Principal: - AWS: '*' - Action: - - kms:Encrypt - - kms:Decrypt - - kms:ReEncrypt* - - kms:GenerateDataKey* - - kms:CreateGrant - - kms:ListGrants - - kms:DescribeKey - Resource: '*' - Condition: - StringEquals: - kms:CallerAccount: !Ref 'AWS::AccountId' - kms:ViaService: !Sub rds.${AWS::Region}.amazonaws.com - Tags: - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-rds-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - EncryptionKeyAlias: - Condition: SupportsEncryption - Type: AWS::KMS::Alias - Properties: - AliasName: - Fn::Join: ['', ['alias/sb-', !Ref Environment, '-rds-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - TargetKeyId: !Ref EncryptionKey - RDSSecurityGroupIngressBootstrap: - Type: AWS::EC2::SecurityGroupIngress - Condition: BootstrapDatabase - Properties: - GroupId: !Ref RDSSecurityGroup - IpProtocol: tcp - FromPort: !Ref RDSPort - ToPort: !Ref RDSPort - SourceSecurityGroupId: !Ref RDSBootstrapSecurityGroup - RDSCluster: - Type: AWS::RDS::DBCluster - Condition: Aurora - DependsOn: RDSSecurityGroup - Properties: - DBClusterIdentifier: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - VpcSecurityGroupIds: - - !Ref RDSSecurityGroup - DBSubnetGroupName: !Ref RDSSubnetGroup - DBClusterParameterGroupName: !Sub 'default.${RDSParameterGroupFamily}' - Engine: !Ref RDSEngine - EngineVersion: !Ref RDSEngineVersion - DatabaseName: !If [DatabaseName, !Ref RDSDatabase, !Ref 'AWS::NoValue'] - # TODO: Parameterize these - BackupRetentionPeriod: 14 - # PreferredMaintenanceWindow: sat:22:30-sun:02:00 - # PreferredBackupWindow: sat:22:30-sun:02:00 - KmsKeyId: !If [SupportsEncryption, !GetAtt EncryptionKey.Arn, !Ref 'AWS::NoValue'] - StorageEncrypted: !If [SupportsEncryption, True, False] - Port: !Ref RDSPort - MasterUsername: !Ref RDSUsername - MasterUserPassword: !Sub '{{resolve:ssm-secure:${RDSPasswordParam}}}' - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - RDSAuroraInstance: - Type: AWS::RDS::DBInstance - Condition: Aurora - DeletionPolicy: Delete - Properties: - DBClusterIdentifier: !Ref RDSCluster - PubliclyAccessible: false - DBInstanceClass: !Ref RDSInstanceClass - Engine: !Ref RDSEngine - DeleteAutomatedBackups: False - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - RDSInstance: - Type: AWS::RDS::DBInstance - Condition: NotAurora - DependsOn: RDSSecurityGroup - DeletionPolicy: Delete - Properties: - DBInstanceIdentifier: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - DBInstanceClass: !Ref RDSInstanceClass - VPCSecurityGroups: - - !Ref RDSSecurityGroup - DBSubnetGroupName: !Ref RDSSubnetGroup - # TODO: Parameterize these - BackupRetentionPeriod: 14 - # PreferredMaintenanceWindow: sat:22:30-sun:02:00 - # PreferredBackupWindow: sat:22:30-sun:02:00 - DeleteAutomatedBackups: False - MultiAZ: false - Engine: !Ref RDSEngine - EngineVersion: !Ref RDSEngineVersion - KmsKeyId: !If [SupportsEncryption, !GetAtt EncryptionKey.Arn, !Ref 'AWS::NoValue'] - StorageEncrypted: !If [SupportsEncryption, True, False] - LicenseModel: - Fn::If: - - SqlServer - - license-included - - !Ref 'AWS::NoValue' - DBName: - Fn::If: - - SqlServer - - !Ref 'AWS::NoValue' - - !If [DatabaseName, !Ref RDSDatabase, !Ref 'AWS::NoValue'] - MasterUsername: !Ref RDSUsername - MasterUserPassword: !Sub '{{resolve:ssm-secure:${RDSPasswordParam}}}' - AllocatedStorage: '100' - StorageType: gp2 - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - RDSBootstrapDatabaseRole: - Type: AWS::IAM::Role - Condition: BootstrapDatabase - Properties: - RoleName: - Fn::Join: ['', ['sb-', !Ref Environment, '-rds-bootstrap-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName, '-', !Ref AWS::Region]] - Path: '/' - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Policies: - - PolicyName: - Fn::Join: ['', ['sb-', !Ref Environment, '-rds-bootstrap-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:PutLogEvents - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - - Effect: Allow - Action: - - logs:DescribeLogGroups - - logs:DescribeLogStreams - - logs:CreateLogStream - Resource: - - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - - Effect: Allow - Action: - - ec2:CreateNetworkInterface - - ec2:DescribeNetworkInterfaces - - ec2:DeleteNetworkInterface - Resource: '*' - - Effect: Allow - Action: - - s3:GetObject - Resource: - - !Sub 'arn:${AWS::Partition}:s3:::{{resolve:ssm:/saas-boost/${Environment}/RESOURCES_BUCKET}}/services/*' - - Effect: Allow - Action: - - ssm:GetParameter - Resource: - - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${RDSPasswordParam} - - Effect: Allow - Action: - - kms:Decrypt - Resource: !Sub arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/* - Condition: - StringEquals: - kms:ViaService: - - !Sub ssm.${AWS::Region}.amazonaws.com - RDSBootstrapDatabaseLogs: - Type: AWS::Logs::LogGroup - Condition: BootstrapDatabase - Properties: - LogGroupName: - Fn::Join: ['', ['/aws/lambda/sb-', !Ref Environment, '-rds-bootstrap-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - RetentionInDays: 30 - RDSBootstrapDatabase: - Type: AWS::Lambda::Function - Condition: BootstrapDatabase - DependsOn: - - RDSBootstrapDatabaseLogs - Properties: - FunctionName: - Fn::Join: ['', ['sb-', !Ref Environment, '-rds-bootstrap-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-', !Ref ServiceResourceName]] - Role: !GetAtt RDSBootstrapDatabaseRole.Arn - Runtime: java11 - Timeout: 870 - MemorySize: 640 - # Has to be a VPC Lambda because we're talking to RDS - # Have to make sure the entire network is still up when you delete - # or we won't be able to call back to the CFN response URL - VpcConfig: - SecurityGroupIds: - - !Ref RDSBootstrapSecurityGroup - SubnetIds: - - !Ref PrivateSubnetA - - !Ref PrivateSubnetB - Handler: com.amazon.aws.partners.saasfactory.saasboost.RdsBootstrap - Code: - S3Bucket: !Ref SaaSBoostBucket - S3Key: !Sub '{{resolve:ssm:/saas-boost/${Environment}/SAAS_BOOST_LAMBDAS_FOLDER}}/RdsBootstrap-lambda.zip' - Layers: - - !Sub '{{resolve:ssm:/saas-boost/${Environment}/UTILS_LAYER}}' - - !Sub '{{resolve:ssm:/saas-boost/${Environment}/CFN_UTILS_LAYER}}' - Environment: - Variables: - SAAS_BOOST_EVENT_BUS: !Sub '{{resolve:ssm:/saas-boost/${Environment}/EVENT_BUS}}' - JAVA_TOOL_OPTIONS: '-XX:+TieredCompilation -XX:TieredStopAtLevel=1' - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - AuroraWaitHandle: - Type: AWS::CloudFormation::WaitConditionHandle - Condition: Aurora - DependsOn: RDSAuroraInstance - NotAuroraWaitHandle: - Type: AWS::CloudFormation::WaitConditionHandle - Condition: NotAurora - DependsOn: RDSInstance - BootstrapWaitCondition: - Type: AWS::CloudFormation::WaitCondition - Properties: - Handle: !If [Aurora, !Ref AuroraWaitHandle, !Ref NotAuroraWaitHandle] - Timeout: '1' - Count: 0 - InvokeRDSBootstrapDatabase: - Type: Custom::CustomResource - Condition: BootstrapDatabase - DependsOn: - - RDSBootstrapDatabaseLogs - - BootstrapWaitCondition - Properties: - ServiceToken: !GetAtt RDSBootstrapDatabase.Arn - Host: !If [Aurora, !GetAtt RDSCluster.Endpoint.Address, !GetAtt RDSInstance.Endpoint.Address] - Port: !Ref RDSPort - Database: !Ref RDSDatabase - User: !Ref RDSUsername - Password: !Ref RDSPasswordParam # CloudFormation doesn't allow auto decrypting of secure params here... - BootstrapFileBucket: !Sub '{{resolve:ssm:/saas-boost/${Environment}/RESOURCES_BUCKET}}' - BootstrapFileKey: !Ref RDSBootstrap -Outputs: - RdsEndpoint: - Description: RDS endpoint - Value: - !If [Aurora, !GetAtt RDSCluster.Endpoint.Address, !GetAtt RDSInstance.Endpoint.Address] -... \ No newline at end of file diff --git a/resources/tenant-onboarding.yaml b/resources/tenant-onboarding.yaml deleted file mode 100644 index 6afe9c38..00000000 --- a/resources/tenant-onboarding.yaml +++ /dev/null @@ -1,403 +0,0 @@ ---- -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -AWSTemplateFormatVersion: 2010-09-09 -Description: AWS SaaS Boost Tenant Onboarding -Parameters: - Environment: - Description: Environment (test, uat, prod, etc.) - Type: String - DomainName: - Description: The hosted zone domain name - Type: String - HostedZoneId: - Description: The hosted zone for this domain name - Type: String - SSLCertificateArn: - Description: The ACM ARN of the SSL certificate for the application's domain - Type: String - Default: '' - TenantId: - Description: The GUID for the tenant - Type: String - TenantSubDomain: - Description: The subdomain for this tenant - Type: String - CidrPrefix: - Description: Prefix of Cidr for this tenant such as 10.1, 10.2 etc. - Type: String - Tier: - Description: The tier this tenant is onboading into - Type: String - Default: '' - PrivateServices: - Description: True if the appConfig for this tenant contains private services - Type: String - Default: 'false' - AllowedValues: ['true', 'false'] - DeployActiveDirectory: - Description: Deploy Active Directory - Type: String - AllowedValues: ['true', 'false'] - Default: 'false' -Conditions: - ProvisionManagedAD: !Equals [!Ref DeployActiveDirectory, 'true'] - HasDomainName: !Not [!Equals [!Ref DomainName, '']] - HasHostedZone: !Not [!Equals [!Ref HostedZoneId, '']] - HasSubDomainName: !Not [!Equals [!Ref TenantSubDomain, '']] - HasCertificate: !Not [!Equals [!Ref SSLCertificateArn, '']] - NoCertificate: !Equals [!Ref SSLCertificateArn, ''] - CreateSubDomainAlias: !And - - !Condition HasDomainName - - !Condition HasHostedZone - - !Condition HasSubDomainName - HasPrivateServices: !Equals [!Ref PrivateServices, 'true'] -Resources: - VPC: - Type: AWS::EC2::VPC - Properties: - CidrBlock: !Sub ${CidrPrefix}.0.0/16 - EnableDnsSupport: true - EnableDnsHostnames: true - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]]]] - - Key: Tier - Value: !Ref Tier - ServiceDiscoveryNamespace: - Type: AWS::ServiceDiscovery::PrivateDnsNamespace - Condition: HasPrivateServices - Properties: - Name: local - Vpc: !Ref VPC - InternetGateway: - Type: AWS::EC2::InternetGateway - Properties: - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - AttachGateway: - Type: AWS::EC2::VPCGatewayAttachment - Properties: - VpcId: !Ref VPC - InternetGatewayId: !Ref InternetGateway - RouteTablePublic: - Type: AWS::EC2::RouteTable - Properties: - VpcId: !Ref VPC - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-public-tenant-', !Select [0, !Split ['-', !Ref TenantId]]]] - - Key: Tier - Value: !Ref Tier - RoutePublic: - Type: AWS::EC2::Route - DependsOn: AttachGateway - Properties: - RouteTableId: !Ref RouteTablePublic - DestinationCidrBlock: 0.0.0.0/0 - GatewayId: !Ref InternetGateway - SubnetPublicA: - Type: AWS::EC2::Subnet - Properties: - VpcId: !Ref VPC - AvailabilityZone: !Select [0, !GetAZs ''] - CidrBlock: !Sub ${CidrPrefix}.32.0/19 - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-public-az1-tenant-', !Select [0, !Split ['-', !Ref TenantId]]]] - - Key: Tier - Value: !Ref Tier - SubnetPublicARouteTable: - Type: AWS::EC2::SubnetRouteTableAssociation - Properties: - SubnetId: !Ref SubnetPublicA - RouteTableId: !Ref RouteTablePublic - SubnetPublicB: - Type: AWS::EC2::Subnet - Properties: - VpcId: !Ref VPC - AvailabilityZone: !Select [1, !GetAZs ''] - CidrBlock: !Sub ${CidrPrefix}.96.0/19 - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-public-az2-tenant-', !Select [0, !Split ['-', !Ref TenantId]]]] - - Key: Tier - Value: !Ref Tier - SubnetPublicBRouteTable: - Type: AWS::EC2::SubnetRouteTableAssociation - Properties: - SubnetId: !Ref SubnetPublicB - RouteTableId: !Ref RouteTablePublic - SubnetPrivateA: - Type: AWS::EC2::Subnet - Properties: - VpcId: !Ref VPC - AvailabilityZone: !Select [0, !GetAZs ''] - CidrBlock: !Sub ${CidrPrefix}.0.0/19 - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-private-az1-tenant-', !Select [0, !Split ['-', !Ref TenantId]]]] - - Key: Tier - Value: !Ref Tier - SubnetPrivateB: - Type: AWS::EC2::Subnet - Properties: - VpcId: !Ref VPC - AvailabilityZone: !Select [1, !GetAZs ''] - CidrBlock: !Sub ${CidrPrefix}.64.0/19 - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-private-az2-tenant-', !Select [0, !Split ['-', !Ref TenantId]]]] - - Key: Tier - Value: !Ref Tier - RouteTablePrivate: - Type: AWS::EC2::RouteTable - Properties: - VpcId: !Ref VPC - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-private-tenant-', !Select [0, !Split ['-', !Ref TenantId]]]] - - Key: Tier - Value: !Ref Tier - Subnet1RouteTableAssociation: - Type: AWS::EC2::SubnetRouteTableAssociation - Properties: - SubnetId: !Ref SubnetPrivateA - RouteTableId: !Ref RouteTablePrivate - Subnet2RouteTableAssociation: - Type: AWS::EC2::SubnetRouteTableAssociation - Properties: - SubnetId: !Ref SubnetPrivateB - RouteTableId: !Ref RouteTablePrivate - # Attach tenant VPC to TGW - TenantTGWAttachment: - Type: AWS::EC2::TransitGatewayAttachment - Properties: - SubnetIds: - - !Ref SubnetPrivateA - - !Ref SubnetPrivateB - Tags: - - Key: Tenant - Value: - !Ref TenantId - - Key: Name - Value: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]]]] - - Key: Tier - Value: !Ref Tier - TransitGatewayId: - Fn::Join: ['', ['{{resolve:ssm:/saas-boost/', !Ref Environment, '/TRANSIT_GATEWAY}}']] - VpcId: !Ref VPC - # Add the route from the egress VPC back to this tenant's CIDR range and attachment - TenantRoute: - Type: AWS::EC2::TransitGatewayRoute - Properties: - DestinationCidrBlock: !Sub ${CidrPrefix}.0.0/16 - TransitGatewayAttachmentId: !Ref TenantTGWAttachment - TransitGatewayRouteTableId: - Fn::Join: ['', ['{{resolve:ssm:/saas-boost/', !Ref Environment, '/EGRESS_ROUTE_TABLE}}']] - # Associate the route table to this tenant's TGW Attachment - TenantVpcTgwAssociation: - Type: AWS::EC2::TransitGatewayRouteTableAssociation - Properties: - TransitGatewayAttachmentId: !Ref TenantTGWAttachment - TransitGatewayRouteTableId: - Fn::Join: ['', ['{{resolve:ssm:/saas-boost/', !Ref Environment, '/TRANSIT_GATEWAY_ROUTE_TABLE}}']] - # Update VPC route tables to point towards transit gateway for appropriate target CIDR ranges - UpdateRouteTable: - Type: AWS::EC2::Route - DependsOn: TenantTGWAttachment - Properties: - RouteTableId: !Ref RouteTablePrivate - DestinationCidrBlock: 0.0.0.0/0 - TransitGatewayId: - Fn::Join: ['', ['{{resolve:ssm:/saas-boost/', !Ref Environment, '/TRANSIT_GATEWAY}}']] - ECSCluster: - Type: AWS::ECS::Cluster - Properties: - ClusterName: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]]]] - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - ALBSecurityGroup: - Type: AWS::EC2::SecurityGroup - Properties: - GroupName: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-alb-sg']] - GroupDescription: HTTP/S access to the load balancer - VpcId: !Ref VPC - SecurityGroupIngress: - - CidrIp: 0.0.0.0/0 - IpProtocol: tcp - FromPort: 80 - ToPort: 80 - - CidrIp: 0.0.0.0/0 - IpProtocol: tcp - FromPort: 443 - ToPort: 443 - ECSSecurityGroup: - Type: AWS::EC2::SecurityGroup - Properties: - GroupName: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]], '-ecs-sg']] - GroupDescription: Access to containers - VpcId: !Ref VPC - ECSSecurityGroupAlbIngress: - Type: AWS::EC2::SecurityGroupIngress - Properties: - Description: Allow traffic from the ALB security group - GroupId: !Ref ECSSecurityGroup - SourceSecurityGroupId: !Ref ALBSecurityGroup - IpProtocol: '-1' - ECSSecurityGroupEcsIngress: - Type: AWS::EC2::SecurityGroupIngress - Properties: - Description: Allow traffic from other resources in the ECS security group - GroupId: !Ref ECSSecurityGroup - SourceSecurityGroupId: !Ref ECSSecurityGroup - IpProtocol: '-1' - ApplicationLoadBalancer: - Type: AWS::ElasticLoadBalancingV2::LoadBalancer - DependsOn: AttachGateway - Properties: - Name: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]]]] - Scheme: internet-facing - LoadBalancerAttributes: - - Key: idle_timeout.timeout_seconds - Value: '30' - - Key: access_logs.s3.enabled - Value: 'true' - - Key: access_logs.s3.bucket - Value: - Fn::Join: ['', ['{{resolve:ssm:/saas-boost/', !Ref Environment, '/ACCESS_LOGS_BUCKET}}']] - - Key: access_logs.s3.prefix - Value: 'access-logs' - Subnets: - - !Ref SubnetPublicA - - !Ref SubnetPublicB - SecurityGroups: [!Ref ALBSecurityGroup] - Tags: - - Key: Tenant - Value: !Ref TenantId - - Key: Tier - Value: !Ref Tier - RecordSetAlias: - Type: AWS::Route53::RecordSet - Condition: CreateSubDomainAlias - Properties: - HostedZoneId: !Ref HostedZoneId - Name: !Sub ${TenantSubDomain}.${DomainName} - Type: 'A' - AliasTarget: - DNSName: !Sub dualstack.${ApplicationLoadBalancer.DNSName} - HostedZoneId: !GetAtt ApplicationLoadBalancer.CanonicalHostedZoneID - EvaluateTargetHealth: false - DefaultTargetGroup: - Type: AWS::ElasticLoadBalancingV2::TargetGroup - Properties: - Name: - Fn::Join: ['', ['sb-', !Ref Environment, '-tenant-', !Select [0, !Split ['-', !Ref TenantId]]]] - VpcId: !Ref VPC - Port: 80 - Protocol: HTTP - HttpListener: - Type: AWS::ElasticLoadBalancingV2::Listener - Condition: NoCertificate - Properties: - LoadBalancerArn: !Ref ApplicationLoadBalancer - Port: 80 - Protocol: HTTP - DefaultActions: - - Type: forward - TargetGroupArn: !Ref DefaultTargetGroup - HttpsListener: - Condition: HasCertificate - Type: AWS::ElasticLoadBalancingV2::Listener - Properties: - LoadBalancerArn: !Ref ApplicationLoadBalancer - Port: 443 - Protocol: HTTPS - DefaultActions: - - Type: forward - TargetGroupArn: !Ref DefaultTargetGroup - Certificates: - - CertificateArn: !Ref SSLCertificateArn - RedirectToHttpsListener: - Condition: HasCertificate - Type: AWS::ElasticLoadBalancingV2::Listener - Properties: - LoadBalancerArn: !Ref ApplicationLoadBalancer - Port: 80 - Protocol: HTTP - DefaultActions: - - Type: redirect - RedirectConfig: - Protocol: HTTPS - Port: 443 - Host: '#{host}' - Path: '/#{path}' - Query: '#{query}' - StatusCode: HTTP_301 - ad: - Type: AWS::CloudFormation::Stack - Condition: ProvisionManagedAD - Properties: - TemplateURL: !Sub https://{{resolve:ssm:/saas-boost/${Environment}/SAAS_BOOST_BUCKET}}.s3.${AWS::Region}.${AWS::URLSuffix}/tenant-onboarding-ad.yaml - Parameters: - Environment: !Ref Environment - Subnets: - !Join - - ',' - - - !Ref SubnetPrivateA - - !Ref SubnetPrivateB - VpcId: !Ref VPC - TenantId: !Ref TenantId - # Onboarding service is responsible for invoking create stack for as many - # services as this application has -Outputs: - LoadBalancer: - Description: Full name for this tenant's application load balancer - Value: !GetAtt ApplicationLoadBalancer.LoadBalancerFullName - DNSName: - Description: DNSName for this tenant's application load balancer - Value: !GetAtt ApplicationLoadBalancer.DNSName -... diff --git a/services/quotas-service/pom.xml b/services/app-config-service/pom.xml similarity index 88% rename from services/quotas-service/pom.xml rename to services/app-config-service/pom.xml index a4564099..1aefeefe 100644 --- a/services/quotas-service/pom.xml +++ b/services/app-config-service/pom.xml @@ -22,7 +22,7 @@ limitations under the License. saasboost-services 1.0.0 - QuotasService + AppConfigService 1.0.0 jar @@ -33,7 +33,8 @@ limitations under the License. - 25 + ${project.basedir}/../.. + 0 @@ -60,19 +61,22 @@ limitations under the License. - junit - junit + com.amazon.aws.partners.saasfactory.saasboost + Utils + 1.0.0 + + provided com.amazon.aws.partners.saasfactory.saasboost - Utils + ApiGatewayHelper 1.0.0 provided software.amazon.awssdk - servicequotas + dynamodb ${aws.java.sdk.version} @@ -87,7 +91,7 @@ limitations under the License. software.amazon.awssdk - rds + eventbridge ${aws.java.sdk.version} @@ -102,7 +106,7 @@ limitations under the License. software.amazon.awssdk - elasticloadbalancingv2 + s3 ${aws.java.sdk.version} @@ -117,7 +121,7 @@ limitations under the License. software.amazon.awssdk - ec2 + acm ${aws.java.sdk.version} @@ -132,7 +136,7 @@ limitations under the License. software.amazon.awssdk - cloudwatch + route53 ${aws.java.sdk.version} @@ -146,4 +150,5 @@ limitations under the License. + diff --git a/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfig.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfig.java new file mode 100644 index 00000000..cbda1c81 --- /dev/null +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfig.java @@ -0,0 +1,168 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import java.time.LocalDateTime; +import java.util.*; + +public class AppConfig { + + private UUID id; + private LocalDateTime created; + private LocalDateTime modified; + private String name; + private String domainName; + private String hostedZone; + private String sslCertificate; + private Map services = new LinkedHashMap<>(); + + public AppConfig() { + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public LocalDateTime getCreated() { + return created; + } + + public void setCreated(LocalDateTime created) { + this.created = created; + } + + public LocalDateTime getModified() { + return modified; + } + + public void setModified(LocalDateTime modified) { + this.modified = modified; + } + + public String getDomainName() { + return domainName; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + public String getHostedZone() { + return hostedZone; + } + + public void setHostedZone(String hostedZone) { + this.hostedZone = hostedZone; + } + + public String getSslCertificate() { + return sslCertificate; + } + + public void setSslCertificate(String sslCertificate) { + this.sslCertificate = sslCertificate; + } + + public Map getServices() { + return services != null ? Map.copyOf(services) : null; + } + + public void setServices(Map services) { + this.services = services != null ? services : new LinkedHashMap<>(); + } + + @JsonIgnore + public boolean isEmpty() { + return (id == null && Utils.isBlank(name) && Utils.isBlank(domainName) && Utils.isBlank(hostedZone) + && Utils.isBlank(sslCertificate) + && (services == null || services.isEmpty())); + } + + @Override + public String toString() { + return Utils.toJson(this); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + // Same reference? + if (this == obj) { + return true; + } + // Same type? + if (getClass() != obj.getClass()) { + return false; + } + final AppConfig other = (AppConfig) obj; + return (Objects.equals(id, other.getId()) + && Objects.equals(name, other.getName()) + && Objects.equals(domainName, other.getDomainName()) + && Objects.equals(hostedZone, other.getHostedZone()) + && Objects.equals(sslCertificate, other.getSslCertificate()) + && ((services == null && other.services == null) || (servicesEqual(services, other.services)))); + } + + public static boolean servicesEqual(Map services, Map otherServices) { + boolean equal = false; + if (services != null && otherServices != null) { + if (services.size() == otherServices.size()) { + boolean entriesEqual = true; + for (Map.Entry entry : services.entrySet()) { + if (!otherServices.containsKey(entry.getKey())) { + entriesEqual = false; + break; + } else { + ServiceConfig service1 = entry.getValue(); + ServiceConfig service2 = otherServices.get(entry.getKey()); + if (service1 == null && service2 == null) { + continue; + } + if (service1 == null || !service1.equals(service2)) { + entriesEqual = false; + break; + } + } + } + equal = entriesEqual; + } + } + return equal; + } + + @Override + public int hashCode() { + return Objects.hash(id, created, modified, name, domainName, hostedZone, sslCertificate, services); + } + +} diff --git a/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigDataAccessLayer.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigDataAccessLayer.java new file mode 100644 index 00000000..e31cc3c6 --- /dev/null +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigDataAccessLayer.java @@ -0,0 +1,342 @@ +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.acm.AcmClient; +import software.amazon.awssdk.services.acm.model.*; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DynamoDbException; +import software.amazon.awssdk.services.dynamodb.model.QueryResponse; +import software.amazon.awssdk.services.dynamodb.model.ScanResponse; +import software.amazon.awssdk.services.route53.Route53Client; +import software.amazon.awssdk.services.route53.model.HostedZone; +import software.amazon.awssdk.services.route53.model.ListHostedZonesRequest; +import software.amazon.awssdk.services.route53.model.ListHostedZonesResponse; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.*; +import java.util.stream.Collectors; + +public class AppConfigDataAccessLayer { + + private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigDataAccessLayer.class); + private static final String AWS_REGION = System.getenv("AWS_REGION"); + private final String dbOptionsTable; + private final String appConfigTable; + private final DynamoDbClient ddb; + private final AcmClient acm; + private final Route53Client route53; + + public AppConfigDataAccessLayer(DynamoDbClient ddb, String dbOptionsTable, String appConfigTable, + AcmClient acm, Route53Client route53) { + this.dbOptionsTable = dbOptionsTable; + this.appConfigTable = appConfigTable; + this.ddb = ddb; + this.acm = acm; + this.route53 = route53; + // Cold start performance hack -- take the TLS hit for the client in the constructor + this.ddb.describeTable(request -> request.tableName(appConfigTable)); + } + + public AppConfig getAppConfig() { + AppConfig appConfig = null; + try { + ScanResponse response = ddb.scan(request -> request.tableName(appConfigTable)); + if (response.hasItems()) { + if (response.items().size() == 1) { + appConfig = fromAttributeValueMap(response.items().get(0)); + } else if (response.items().size() > 1) { + LOGGER.warn("Unexpected number of appConfig items {}", response.items().size()); + } + } + } catch (DynamoDbException e) { + LOGGER.error(Utils.getFullStackTrace(e)); + throw new RuntimeException(e); + } + return appConfig; + } + + public AppConfig insertAppConfig(AppConfig appConfig) { + // Unique identifier is owned by the DAL + if (appConfig.getId() != null) { + throw new IllegalArgumentException("Can't insert a new appConfig that already has an id"); + } + UUID appConfigId = UUID.randomUUID(); + appConfig.setId(appConfigId); + + // Created and Modified are owned by the DAL since they reflect when the + // object was persisted + LocalDateTime now = LocalDateTime.now(); + appConfig.setCreated(now); + appConfig.setModified(now); + Map item = toAttributeValueMap(appConfig); + try { + ddb.putItem(request -> request.tableName(appConfigTable).item(item)); + } catch (DynamoDbException e) { + LOGGER.error(Utils.getFullStackTrace(e)); + throw e; + } + return appConfig; + } + + // Choosing to do a replacement update as you might do in a RDBMS by + // setting columns = NULL when they do not exist in the updated value + public AppConfig updateAppConfig(AppConfig appConfig) { + try { + // Created and Modified are owned by the DAL since they reflect when the + // object was persisted + appConfig.setModified(LocalDateTime.now()); + Map item = toAttributeValueMap(appConfig); + ddb.putItem(request -> request.tableName(appConfigTable).item(item)); + } catch (DynamoDbException e) { + LOGGER.error("OnboardingServiceDAL::updateOnboarding " + Utils.getFullStackTrace(e)); + throw e; + } + return appConfig; + } + + public void deleteAppConfig(AppConfig appConfig) { + try { + ddb.deleteItem(request -> request + .tableName(appConfigTable) + .key(Map.of("id", AttributeValue.builder().s(appConfig.getId().toString()).build())) + ); + } catch (DynamoDbException e) { + LOGGER.error(e.awsErrorDetails().errorMessage()); + LOGGER.error(Utils.getFullStackTrace(e)); + throw new RuntimeException(e); + } + } + + public List> rdsOptions() { + List> orderableOptionsByRegion = new ArrayList<>(); + QueryResponse response = ddb.query(request -> request + .tableName(dbOptionsTable) + .keyConditionExpression("#region = :region") + .expressionAttributeNames(Map.of("#region", "region")) + .expressionAttributeValues(Map.of(":region", AttributeValue.builder().s(AWS_REGION).build())) + ); + response.items().forEach(item -> + orderableOptionsByRegion.add(fromRdsOptionsAttributeValueMap(item)) + ); + return orderableOptionsByRegion; + } + + public List acmCertificateOptions() { + List certificateSummaries = new ArrayList<>(); + String nextToken = null; + do { + try { + // only list certificates that aren't expired, invalid, revoked, or otherwise unusable + ListCertificatesResponse response = acm.listCertificates(ListCertificatesRequest.builder() + .certificateStatuses(List.of(CertificateStatus.PENDING_VALIDATION, CertificateStatus.ISSUED)) + .nextToken(nextToken) + .build()); + LOGGER.info("ACM PENDING_VALIDATION and ISSUED certs: {}", response); + if (response.certificateSummaryList() != null) { + certificateSummaries.addAll(response.certificateSummaryList()); + } + nextToken = response.nextToken(); + } catch (InvalidArgsException iae) { + LOGGER.error("Error retrieving certificates", iae); + } + } while (nextToken != null); + return certificateSummaries; + } + + public List hostedZoneOptions() { + List allHostedZones = new ArrayList<>(); + String marker = null; + do { + ListHostedZonesResponse response = route53.listHostedZones(ListHostedZonesRequest.builder() + .marker(marker) + .build()); + LOGGER.info("Listed hostedZones: {}", response); + if (response.hasHostedZones() && response.hostedZones() != null) { + // we only want to list public zones, since we're attaching them to an internet-facing + // application load balancer for the tenant + for (HostedZone zone : response.hostedZones()) { + if (zone.config() != null && !zone.config().privateZone()) { + allHostedZones.add(zone); + } + } + } + marker = response.marker(); + } while (marker != null); + return allHostedZones; + } + + protected static Map toAttributeValueMap(AppConfig appConfig) { + Map item = new HashMap<>(); + item.put("id", AttributeValue.builder().s(appConfig.getId().toString()).build()); + if (appConfig.getCreated() != null) { + item.put("created", AttributeValue.builder().s( + appConfig.getCreated().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); + } + if (appConfig.getModified() != null) { + item.put("modified", AttributeValue.builder().s( + appConfig.getModified().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); + } + if (appConfig.getName() != null) { + item.put("name", AttributeValue.builder().s(appConfig.getName()).build()); + } + if (appConfig.getDomainName() != null) { + item.put("domain_name", AttributeValue.builder().s(appConfig.getDomainName()).build()); + } + if (appConfig.getHostedZone() != null) { + item.put("hosted_zone", AttributeValue.builder().s(appConfig.getHostedZone()).build()); + } + if (appConfig.getSslCertificate() != null) { + item.put("ssl_certificate", AttributeValue.builder().s(appConfig.getSslCertificate()).build()); + } + if (appConfig.getServices() != null && !appConfig.getServices().isEmpty()) { + item.put("services", AttributeValue.builder().m(appConfig.getServices().entrySet() + .stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> AttributeValue.builder().s(Utils.toJson(entry.getValue())).build()) + ) + ).build()); + } + return item; + } + + protected static AppConfig fromAttributeValueMap(Map item) { + AppConfig appConfig = null; + if (item != null && !item.isEmpty()) { + appConfig = new AppConfig(); + if (item.containsKey("id")) { + try { + appConfig.setId(UUID.fromString(item.get("id").s())); + } catch (IllegalArgumentException e) { + LOGGER.error("Failed to parse UUID from database: " + item.get("id").s()); + LOGGER.error(Utils.getFullStackTrace(e)); + } + } + if (item.containsKey("created")) { + try { + LocalDateTime created = LocalDateTime.parse(item.get("created").s(), + DateTimeFormatter.ISO_DATE_TIME); + appConfig.setCreated(created); + } catch (DateTimeParseException e) { + LOGGER.error("Failed to parse created date from database: " + item.get("created").s()); + LOGGER.error(Utils.getFullStackTrace(e)); + } + } + if (item.containsKey("modified")) { + try { + LocalDateTime created = LocalDateTime.parse(item.get("modified").s(), + DateTimeFormatter.ISO_DATE_TIME); + appConfig.setModified(created); + } catch (DateTimeParseException e) { + LOGGER.error("Failed to parse created date from database: " + item.get("modified").s()); + LOGGER.error(Utils.getFullStackTrace(e)); + } + } + if (item.containsKey("name")) { + appConfig.setName(item.get("name").s()); + } + if (item.containsKey("domain_name")) { + appConfig.setDomainName(item.get("domain_name").s()); + } + if (item.containsKey("hosted_zone")) { + appConfig.setHostedZone(item.get("hosted_zone").s()); + } + if (item.containsKey("ssl_certificate")) { + appConfig.setSslCertificate(item.get("ssl_certificate").s()); + } + final Map services = new LinkedHashMap<>(); + if (item.containsKey("services")) { + item.get("services").m().entrySet().forEach( + entry -> services.put(entry.getKey(), Utils.fromJson(entry.getValue().s(), ServiceConfig.class)) + ); + } + } + return appConfig; + } + + private static final Comparator> INSTANCE_TYPE_COMPARATOR = ((instance1, instance2) -> { + // T's before M's before R's + int compare = 0; + char type1 = instance1.get("instance").charAt(0); + char type2 = instance2.get("instance").charAt(0); + if (type1 != type2) { + if ('T' == type1) { + compare = -1; + } else if ('T' == type2) { + compare = 1; + } else if ('M' == type1) { + compare = -1; + } else if ('M' == type2) { + compare = 1; + } + } + return compare; + }); + + private static final Comparator> INSTANCE_GENERATION_COMPARATOR = ((instance1, instance2) -> { + Integer gen1 = Integer.valueOf(instance1.get("instance").substring(1, 2)); + Integer gen2 = Integer.valueOf(instance2.get("instance").substring(1, 2)); + return gen1.compareTo(gen2); + }); + + private static final Comparator> INSTANCE_SIZE_COMPARATOR = ((instance1, instance2) -> { + String size1 = instance1.get("instance").substring(3); + String size2 = instance2.get("instance").substring(3); + List sizes = Arrays.asList( + "MICRO", + "SMALL", + "MEDIUM", + "LARGE", + "XL", + "2XL", + "4XL", + "12XL", + "24XL" + ); + return Integer.compare(sizes.indexOf(size1), sizes.indexOf(size2)); + }); + + public static final Comparator> RDS_INSTANCE_COMPARATOR = INSTANCE_TYPE_COMPARATOR + .thenComparing(INSTANCE_GENERATION_COMPARATOR) + .thenComparing(INSTANCE_SIZE_COMPARATOR); + + public static Map fromRdsOptionsAttributeValueMap(Map item) { + Map option = new LinkedHashMap<>(); + option.put("engine", item.get("engine").s()); + option.put("region", item.get("region").s()); + Map optionAttributes = item.get("options").m(); + option.put("name", optionAttributes.get("name").s()); + option.put("description", optionAttributes.get("description").s()); + + List> versions = new ArrayList<>(); + for (AttributeValue versionAttribute : optionAttributes.get("versions").l()) { + // build the version entry + Map versionAttributeMap = versionAttribute.m(); + Map version = new LinkedHashMap<>(); // use a linked map so we can sort + version.put("description", versionAttributeMap.get("description").s()); + version.put("family", versionAttributeMap.get("family").s()); + version.put("version", versionAttributeMap.get("version").s()); + + List> instances = new ArrayList<>(); + Map instancesAttributeMap = versionAttributeMap.get("instances").m(); + for (Map.Entry instanceAttribute : instancesAttributeMap.entrySet()) { + Map instance = new LinkedHashMap<>(); // use a linked map so we can sort + instance.put("instance", instanceAttribute.getKey()); + instance.put("class", instanceAttribute.getValue().m().get("class").s()); + instance.put("description", instanceAttribute.getValue().m().get("description").s()); + instances.add(instance); + } + Collections.sort(instances, RDS_INSTANCE_COMPARATOR); + version.put("instances", instances); + versions.add(version); + } + option.put("versions", versions); + + return option; + } + +} diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/AppConfigEvent.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigEvent.java similarity index 96% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/AppConfigEvent.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigEvent.java index ec7130aa..db119ec3 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/AppConfigEvent.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigEvent.java @@ -1,4 +1,4 @@ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig; +package com.amazon.aws.partners.saasfactory.saasboost; import java.util.Map; diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/AppConfigHelper.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigHelper.java similarity index 78% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/AppConfigHelper.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigHelper.java index 2ee7424e..c5cf4ca4 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/AppConfigHelper.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigHelper.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig; +package com.amazon.aws.partners.saasfactory.saasboost; import java.util.HashSet; import java.util.Set; @@ -46,21 +46,6 @@ public static boolean isHostedZoneChanged(AppConfig existing, AppConfig altered) && !altered.getHostedZone().equalsIgnoreCase(existing.getHostedZone()))); } - public static boolean isBillingChanged(AppConfig existing, AppConfig altered) { - return ((existing.getBilling() != null && !existing.getBilling().equals(altered.getBilling())) - || (altered.getBilling() != null && !altered.getBilling().equals(existing.getBilling()))); - } - - public static boolean isBillingFirstTime(AppConfig existing, AppConfig altered) { - return ((existing.getBilling() == null || !existing.getBilling().hasApiKey()) - && (altered.getBilling() != null && altered.getBilling().hasApiKey())); - } - - public static boolean isBillingRemoved(AppConfig existing, AppConfig altered) { - return ((existing.getBilling() != null && existing.getBilling().hasApiKey()) - && (altered.getBilling() == null || !altered.getBilling().hasApiKey())); - } - public static Set removedServices(AppConfig existing, AppConfig altered) { Set removed = new HashSet<>(); for (String existingKey : existing.getServices().keySet()) { diff --git a/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigService.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigService.java new file mode 100644 index 00000000..34251942 --- /dev/null +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigService.java @@ -0,0 +1,527 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.amazon.aws.partners.saasfactory.saasboost.compute.AbstractCompute; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.acm.AcmClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.eventbridge.EventBridgeClient; +import software.amazon.awssdk.services.route53.Route53Client; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest; + +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; +import java.util.*; +import java.util.stream.Collectors; + +public class AppConfigService { + + private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigService.class); + private static final Map CORS = Map.of("Access-Control-Allow-Origin", "*"); + private static final String AWS_REGION = System.getenv("AWS_REGION"); + private static final String API_APP_CLIENT = System.getenv("API_APP_CLIENT"); + private static final String OPTIONS_TABLE = System.getenv("OPTIONS_TABLE"); + private static final String APP_CONFIG_TABLE = System.getenv("APP_CONFIG_TABLE"); + private static final String SAAS_BOOST_EVENT_BUS = System.getenv("SAAS_BOOST_EVENT_BUS"); + private static final String RESOURCES_BUCKET = System.getenv("RESOURCES_BUCKET"); + private final AppConfigDataAccessLayer dal; + private final EventBridgeClient eventBridge; + private final S3Presigner presigner; + + public AppConfigService() { + this(new DefaultDependencyFactory()); + } + + // Facilitates testing by being able to mock out AWS SDK dependencies + public AppConfigService(AppConfigServiceDependencyFactory init) { + if (Utils.isBlank(AWS_REGION)) { + throw new IllegalStateException("Missing environment variable AWS_REGION"); + } + if (Utils.isBlank(APP_CONFIG_TABLE)) { + throw new IllegalStateException("Missing environment variable APP_CONFIG_TABLE"); + } + LOGGER.info("Version Info: {}", Utils.version(this.getClass())); + this.dal = init.dal(); + this.eventBridge = init.eventBridge(); + this.presigner = init.s3Presigner(); + } + + /** + * Get existing account settings available to use with the AppConfig. Integration for GET /config/options endpoint. + * Contains available supported operating systems, ACM SSL/TLS certificates, Route53 hosted zones, + * and orderable RDS engines and instance types. + * @param event API Gateway proxy request event + * @param context + * @return Map of available account settings + */ + public APIGatewayProxyResponseEvent configOptions(APIGatewayProxyRequestEvent event, Context context) { + if (Utils.warmup(event)) { + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); + } + + LOGGER.info("SettingsService::configOptions"); + //Utils.logRequestEvent(event); + + // TODO This data really needs to come from the Application Plane account! + Map options = new HashMap<>(); + options.put("osOptions", Arrays.stream(OperatingSystem.values()) + .collect( + Collectors.toMap(OperatingSystem::name, OperatingSystem::getDescription) + )); + options.put("dbOptions", dal.rdsOptions()); + options.put("acmOptions", dal.acmCertificateOptions()); + options.put("hostedZoneOptions", dal.hostedZoneOptions()); + + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_OK) + .withHeaders(CORS) + .withBody(Utils.toJson(options)); + + return response; + } + + public APIGatewayProxyResponseEvent getAppConfig(APIGatewayProxyRequestEvent event, Context context) { + if (Utils.warmup(event)) { + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); + } + + final long startTimeMillis = System.currentTimeMillis(); + LOGGER.info("SettingsService::getAppConfig"); + //Utils.logRequestEvent(event); + APIGatewayProxyResponseEvent response; + + AppConfig appConfig = dal.getAppConfig(); + // The Web UI won't work if it receives null + if (appConfig == null) { + appConfig = new AppConfig(); + } + response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_OK) + .withHeaders(CORS) + .withBody(Utils.toJson(appConfig)); + + long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; + LOGGER.info("SettingsService::getAppConfig exec " + totalTimeMillis); + return response; + } + + public APIGatewayProxyResponseEvent updateAppConfig(APIGatewayProxyRequestEvent event, Context context) { + if (Utils.isBlank(SAAS_BOOST_EVENT_BUS)) { + throw new IllegalStateException("Missing environment variable SAAS_BOOST_EVENT_BUS"); + } + if (Utils.isBlank(API_APP_CLIENT)) { + throw new IllegalStateException("Missing required environment variable API_APP_CLIENT"); + } + if (Utils.warmup(event)) { + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); + } + + final long startTimeMillis = System.currentTimeMillis(); + LOGGER.info("SettingsService::updateAppConfig"); + Utils.logRequestEvent(event); + APIGatewayProxyResponseEvent response; + + AppConfig updatedAppConfig = Utils.fromJson(event.getBody(), AppConfig.class); + if (updatedAppConfig == null) { + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) + .withBody("{\"message\":\"Invalid request body.\"}"); + } else if (Utils.isBlank(updatedAppConfig.getName())) { + LOGGER.error("Can't update application configuration without an app name"); + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) + .withBody("{\"message\":\"Application name is required.\"}"); + } else { + AppConfig currentAppConfig = dal.getAppConfig(); + if (currentAppConfig == null || currentAppConfig.isEmpty()) { + LOGGER.info("Processing first time app config save"); + // First time setting the app config object don't bother validating whether we can modify it or not + updatedAppConfig = dal.insertAppConfig(updatedAppConfig); + + // If the app config has any databases, get the presigned S3 urls to upload bootstrap files + generateDatabaseBootstrapFileUrl(updatedAppConfig); + + Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, "saas-boost", + AppConfigEvent.APP_CONFIG_CHANGED.detailType(), + Collections.emptyMap() + ); + + response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_OK) + .withHeaders(CORS) + .withBody(Utils.toJson(updatedAppConfig)); + } else { + LOGGER.info("Processing update to existing app config"); + List> provisionedTenants = getProvisionedTenants(context); + boolean provisioned = !provisionedTenants.isEmpty(); + boolean okToUpdate = validateAppConfigUpdate(currentAppConfig, updatedAppConfig, provisioned); + boolean fireUpdateAppConfigEvent = false; + + if (okToUpdate) { + LOGGER.info("Ok to proceed with app config update"); + if (AppConfigHelper.isDomainChanged(currentAppConfig, updatedAppConfig)) { + LOGGER.info("AppConfig domain name has changed"); + fireUpdateAppConfigEvent = true; + } + + if (AppConfigHelper.isServicesChanged(currentAppConfig, updatedAppConfig)) { + LOGGER.info("AppConfig application services changed"); + // Currently you can only remove services if there are no provisioned tenants + Set removedServices = AppConfigHelper.removedServices(currentAppConfig, + updatedAppConfig); + if (!removedServices.isEmpty()) { + LOGGER.info("Services {} were removed from AppConfig", removedServices); + } + fireUpdateAppConfigEvent = true; + } + + // TODO how do we want to deal with tier settings changes? + + LOGGER.info("Persisting updated app config"); + updatedAppConfig = dal.updateAppConfig(updatedAppConfig); + + // If the app config has any databases, get the presigned S3 urls to upload bootstrap files + if (!provisioned) { + generateDatabaseBootstrapFileUrl(updatedAppConfig); + } + + if (fireUpdateAppConfigEvent) { + // The provisioning system can take care of modifying/adding/removing infra + // due to changes in the app config + Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, "saas-boost", + AppConfigEvent.APP_CONFIG_CHANGED.detailType(), + Collections.emptyMap() + ); + } + + response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_OK) + .withHeaders(CORS) + .withBody(Utils.toJson(updatedAppConfig)); + } else { + LOGGER.info("App config update validation failed"); + response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) + .withHeaders(CORS) + .withBody("{\"message\":\"Application config update validation failed.\"}"); + } + } + } + + long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; + LOGGER.info("SettingsService::updateAppConfig exec " + totalTimeMillis); + return response; + } + + public APIGatewayProxyResponseEvent deleteAppConfig(APIGatewayProxyRequestEvent event, Context context) { + if (Utils.warmup(event)) { + //LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); + } + //Utils.logRequestEvent(event); + APIGatewayProxyResponseEvent response; + //Map params = event.getPathParameters(); + //String id = params.get("id"); + AppConfig appConfig = dal.getAppConfig(); + //if (appConfig == null || appConfig.getId() == null || !appConfig.getId().toString().equals(id)) { + if (appConfig == null) { + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_NO_CONTENT); // No content + //response = new APIGatewayProxyResponseEvent() + // .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) + // .withHeaders(CORS) + // .withBody(Utils.toJson(Map.of("message", "Invalid appConfig id"))); + } else { + try { + dal.deleteAppConfig(appConfig); + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_NO_CONTENT); // No content + } catch (Exception e) { + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) + .withBody(Utils.toJson(Map.of("message", "Failed to delete appConfig"))); + } + } + return response; + } + + public void handleAppConfigEvent(Map event, Context context) { + if ("saas-boost".equals(event.get("source"))) { + AppConfigEvent appConfigEvent = AppConfigEvent.fromDetailType((String) event.get("detail-type")); + if (appConfigEvent != null) { + switch (appConfigEvent) { + case APP_CONFIG_RESOURCE_CHANGED: + LOGGER.info("Handling App Config Resource Changed"); + handleAppConfigResourceChanged(event, context); + break; + case APP_CONFIG_CHANGED: + // We produce this event, but currently aren't consuming it + break; + case APP_CONFIG_UPDATE_COMPLETED: + // We produce this event, but currently aren't consuming it + break; + default: { + LOGGER.error("Can't find app config event for detail-type {}", event.get("detail-type")); + // TODO Throw here? Would end up in DLQ. + } + } + } else { + LOGGER.error("Can't find app config event for detail-type {}", event.get("detail-type")); + // TODO Throw here? Would end up in DLQ. + } + } else if ("aws.s3".equals(event.get("source"))) { + LOGGER.info("Handling App Config Resources File S3 Event"); + handleAppConfigResourcesFileEvent(event, context); + } else { + LOGGER.error("Unknown event source " + event.get("source")); + // TODO Throw here? Would end up in DLQ. + } + } + + protected void handleAppConfigResourceChanged(Map event, Context context) { + Utils.logRequestEvent(event); + Map detail = (Map) event.get("detail"); + String json = Utils.toJson(detail); + if (json != null) { + AppConfig changedAppConfig = Utils.fromJson(json, AppConfig.class); + if (changedAppConfig != null) { + boolean update = false; + AppConfig existingAppConfig = dal.getAppConfig(); + // Only update the services if they were passed in + if (json.contains("services") && changedAppConfig.getServices() != null) { + for (Map.Entry changedService : changedAppConfig.getServices().entrySet()) { + String changedServiceName = changedService.getKey(); + ServiceConfig changedServiceConfig = changedService.getValue(); + ServiceConfig requestedService = existingAppConfig.getServices().get(changedServiceName); + ServiceConfig.Builder newServiceConfigBuilder = ServiceConfig.builder(requestedService); + if (requestedService != null && changedServiceConfig != null) { + // change container repo if passed + if (requestedService.getCompute() != null && changedServiceConfig.getCompute() != null) { + String changedContainerRepo = changedServiceConfig.getCompute().getContainerRepo(); + String existingContainerRepo = requestedService.getCompute().getContainerRepo(); + if (!Utils.nullableEquals(existingContainerRepo, changedContainerRepo)) { + LOGGER.info("Updating service {} ECR repo from {} to {}", changedServiceName, + requestedService.getCompute().getContainerRepo(), + changedServiceConfig.getCompute().getContainerRepo()); + // TODO what if the service shouldn't have a container repo, because compute + // TODO is of the wrong type? core stack listener shouldn't fire the ECR repo event + AbstractCompute.Builder existingComputeBuilder = requestedService + .getCompute().builder(); + newServiceConfigBuilder = newServiceConfigBuilder + .compute(existingComputeBuilder + .containerRepo(changedServiceConfig.getCompute().getContainerRepo()) + .build()); + } + } + // change s3 bucket name if passed (and if s3 already exists in service config) + if (requestedService.getObjectStorage() != null + && changedServiceConfig.getObjectStorage() != null) { + String existingBucketName = requestedService.getObjectStorage().getBucketName(); + String newBucketName = changedServiceConfig.getObjectStorage().getBucketName(); + if (!Utils.nullableEquals(existingBucketName, newBucketName)) { + newServiceConfigBuilder = newServiceConfigBuilder + .objectStorage(changedServiceConfig.getObjectStorage()); + } + } + ServiceConfig newServiceConfig = newServiceConfigBuilder.build(); + if (!newServiceConfig.equals(requestedService)) { + LOGGER.info("Updating serviceConfig from {} to {}", + requestedService, newServiceConfig); + update = true; + existingAppConfig.getServices().put(changedServiceName, newServiceConfig); + } + } else { + LOGGER.error("Can't find app config service {}", changedServiceName); + } + } + if (update) { + dal.updateAppConfig(existingAppConfig); + } + } + // If there are provisioned tenants, and we just ran an update to the infrastructure + // we need to update the tenant environments to reflect any changes + if (update) { + List> provisionedTenants = getProvisionedTenants(context); + if (!provisionedTenants.isEmpty()) { + LOGGER.info("Updated app config with provisioned tenants"); + Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, "saas-boost", + AppConfigEvent.APP_CONFIG_UPDATE_COMPLETED.detailType(), + Collections.emptyMap()); + } + } else { + LOGGER.info("No app config changes to process"); + } + } else { + LOGGER.error("Can't parse event detail as AppConfig {}", json); + } + } else { + LOGGER.error("Can't serialize detail to JSON {}", event.get("detail")); + } + } + + protected List> getProvisionedTenants(Context context) { + LOGGER.info("Calling tenant service to fetch all provisioned tenants"); + ApiGatewayHelper api = ApiGatewayHelper.clientCredentialsHelper(API_APP_CLIENT); + String getTenantsResponseBody = api.authorizedRequest("GET", "tenants?status=provisioned"); + List> tenants = Utils.fromJson(getTenantsResponseBody, ArrayList.class); + if (tenants == null) { + tenants = new ArrayList<>(); + } + return tenants; + } + + protected void handleAppConfigResourcesFileEvent(Map event, Context context) { + Utils.logRequestEvent(event); + + // A database bootstrap file was uploaded for one of the app config services. + // We'll update the service config so Onboarding will know to run the file as + // part of provisioning RDS. + Map detail = (Map) event.get("detail"); + String bucket = (String) ((Map) detail.get("bucket")).get("name"); + String key = (String) ((Map) detail.get("object")).get("key"); + LOGGER.info("Processing resources bucket PUT {}, {}", bucket, key); + + // key will be services/${service_name}/bootstrap.sql + String serviceName = key.substring("services/".length(), (key.length() - "/bootstrap.sql".length())); + AppConfig appConfig = dal.getAppConfig(); + for (Map.Entry serviceConfig : appConfig.getServices().entrySet()) { + if (serviceName.equals(serviceConfig.getKey())) { + ServiceConfig service = serviceConfig.getValue(); + LOGGER.info("Saving bootstrap.sql file for {}", service.getName()); + service.getDatabase().setBootstrapFilename(key); + // TODO fix this + throw new UnsupportedOperationException("can't save bootstrap.sql file for " + service.getName()); + //dal.setServiceConfig(service); + //break; + } + } + } + + protected static boolean validateAppConfigUpdate(AppConfig currentAppConfig, AppConfig updatedAppConfig, + boolean provisionedTenants) { + boolean domainNameValid = true; + if (AppConfigHelper.isDomainChanged(currentAppConfig, updatedAppConfig) && provisionedTenants) { + LOGGER.error("Can't change domain name after onboarding tenants"); + domainNameValid = false; + } + + boolean serviceConfigValid = true; + if (AppConfigHelper.isServicesChanged(currentAppConfig, updatedAppConfig)) { + if (provisionedTenants) { + Set removedServices = AppConfigHelper.removedServices(currentAppConfig, updatedAppConfig); + if (!removedServices.isEmpty()) { + LOGGER.error("Can't remove existing application services after onboarding tenants"); + serviceConfigValid = false; + } + } + } + + return domainNameValid && serviceConfigValid; + } + + protected void generateDatabaseBootstrapFileUrl(AppConfig appConfig) { + // Create the pre-signed S3 URLs for the bootstrap SQL files. We won't save these to the + // database record because the user might not upload any SQL files. If they do, we'll + // process those uploads async and persist the relevant data to the database. + for (Map.Entry serviceConfig : appConfig.getServices().entrySet()) { + String serviceName = serviceConfig.getKey(); + ServiceConfig service = serviceConfig.getValue(); + if (service.hasDatabase() && Utils.isBlank(service.getDatabase().getBootstrapFilename())) { + try { + // Create a presigned S3 URL to upload the database bootstrap file to + final String key = "services/" + serviceName + "/bootstrap.sql"; + final Duration expires = Duration.ofMinutes(15); // UI times out in 10 min + PresignedPutObjectRequest presignedObject = presigner.presignPutObject(request -> request + .signatureDuration(expires) + .putObjectRequest(PutObjectRequest.builder() + .bucket(RESOURCES_BUCKET) + .key(key) + .build() + ).build() + ); + service.getDatabase().setBootstrapFilename(presignedObject.url().toString()); + } catch (S3Exception s3Error) { + LOGGER.error("s3 presign url failed", s3Error); + LOGGER.error(Utils.getFullStackTrace(s3Error)); + throw s3Error; + } + } + } + } + + interface AppConfigServiceDependencyFactory { + + AppConfigDataAccessLayer dal(); + + EventBridgeClient eventBridge(); + + S3Presigner s3Presigner(); + } + + private static final class DefaultDependencyFactory implements AppConfigServiceDependencyFactory { + + @Override + public AppConfigDataAccessLayer dal() { + return new AppConfigDataAccessLayer(Utils.sdkClient(DynamoDbClient.builder(), DynamoDbClient.SERVICE_NAME), + OPTIONS_TABLE, APP_CONFIG_TABLE, Utils.sdkClient(AcmClient.builder(), AcmClient.SERVICE_NAME), + Utils.sdkClient(Route53Client.builder(), Route53Client.SERVICE_NAME)); + } + + @Override + public EventBridgeClient eventBridge() { + return Utils.sdkClient(EventBridgeClient.builder(), EventBridgeClient.SERVICE_NAME); + } + + @Override + public S3Presigner s3Presigner() { + try { + return S3Presigner.builder() + .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) + .region(Region.of(AWS_REGION)) + .endpointOverride(new URI("https://" + S3Client.SERVICE_NAME + "." + + Region.of(AWS_REGION) + + "." + + Utils.endpointSuffix(AWS_REGION))) + .build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + } + +} \ No newline at end of file diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/Database.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Database.java similarity index 99% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/Database.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Database.java index 1303eeca..262b4349 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/Database.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Database.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig; +package com.amazon.aws.partners.saasfactory.saasboost; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/DatabaseTierConfig.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/DatabaseTierConfig.java similarity index 98% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/DatabaseTierConfig.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/DatabaseTierConfig.java index 1f3f6d3c..3c4fdc52 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/DatabaseTierConfig.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/DatabaseTierConfig.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig; +package com.amazon.aws.partners.saasfactory.saasboost; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/EcsLaunchType.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/EcsLaunchType.java similarity index 91% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/EcsLaunchType.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/EcsLaunchType.java index bb188fcc..77d18853 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/EcsLaunchType.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/EcsLaunchType.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig; +package com.amazon.aws.partners.saasfactory.saasboost; public enum EcsLaunchType { EC2, diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/S3Storage.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ObjectStorage.java similarity index 81% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/S3Storage.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ObjectStorage.java index c554c1f0..db41d2ce 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/S3Storage.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ObjectStorage.java @@ -14,18 +14,17 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig; +package com.amazon.aws.partners.saasfactory.saasboost; -import com.amazon.aws.partners.saasfactory.saasboost.Utils; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; -@JsonDeserialize(builder = S3Storage.Builder.class) -public final class S3Storage { +@JsonDeserialize(builder = ObjectStorage.Builder.class) +public final class ObjectStorage { private final String bucketName; - public S3Storage(Builder b) { + public ObjectStorage(Builder b) { this.bucketName = b.bucketName; } @@ -47,7 +46,7 @@ public boolean equals(Object obj) { return false; } - S3Storage other = (S3Storage) obj; + ObjectStorage other = (ObjectStorage) obj; return Utils.nullableEquals(this.getBucketName(), other.getBucketName()); } @@ -61,7 +60,7 @@ public static Builder builder() { return new Builder(); } - public static Builder builder(S3Storage other) { + public static Builder builder(ObjectStorage other) { return new Builder(); } @@ -75,8 +74,8 @@ public Builder bucketName(String bucketName) { return this; } - public S3Storage build() { - return new S3Storage(this); + public ObjectStorage build() { + return new ObjectStorage(this); } } } diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/OperatingSystem.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OperatingSystem.java similarity index 96% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/OperatingSystem.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OperatingSystem.java index c356c679..d98c7c0a 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/OperatingSystem.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OperatingSystem.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig; +package com.amazon.aws.partners.saasfactory.saasboost; public enum OperatingSystem { diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/ServiceConfig.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ServiceConfig.java similarity index 87% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/ServiceConfig.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ServiceConfig.java index 3ca2491b..276437ae 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/ServiceConfig.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ServiceConfig.java @@ -14,11 +14,10 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig; +package com.amazon.aws.partners.saasfactory.saasboost; -import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.compute.AbstractCompute; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.AbstractFilesystem; +import com.amazon.aws.partners.saasfactory.saasboost.compute.AbstractCompute; +import com.amazon.aws.partners.saasfactory.saasboost.filesystem.AbstractFilesystem; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; @@ -34,7 +33,7 @@ public class ServiceConfig { private final String description; private final String path; private final Database database; - private final S3Storage s3; + private final ObjectStorage objectStorage; private final AbstractFilesystem filesystem; private final AbstractCompute compute; @@ -44,7 +43,7 @@ private ServiceConfig(Builder builder) { this.description = builder.description; this.path = builder.path; this.database = builder.database; - this.s3 = builder.s3; + this.objectStorage = builder.objectStorage; this.filesystem = builder.filesystem; this.compute = builder.compute; } @@ -60,7 +59,7 @@ public static Builder builder(ServiceConfig other) { .description(other.getDescription()) .path(other.getPath()) .database(other.getDatabase()) - .s3(other.s3) + .objectStorage(other.objectStorage) .filesystem(other.getFilesystem()) .compute(other.getCompute()); } @@ -89,8 +88,8 @@ public boolean hasDatabase() { return database != null; } - public S3Storage getS3() { - return s3; + public ObjectStorage getObjectStorage() { + return objectStorage; } public AbstractFilesystem getFilesystem() { @@ -122,14 +121,14 @@ public boolean equals(Object obj) { && Utils.nullableEquals(path, other.path) && Utils.nullableEquals(publiclyAddressable, other.publiclyAddressable) && Utils.nullableEquals(database, other.database) - && Utils.nullableEquals(s3, other.s3) + && Utils.nullableEquals(objectStorage, other.objectStorage) && Utils.nullableEquals(filesystem, other.filesystem) && Utils.nullableEquals(compute, other.compute); } @Override public int hashCode() { - return Objects.hash(name, description, path, publiclyAddressable, database, s3, filesystem, compute); + return Objects.hash(name, description, path, publiclyAddressable, database, objectStorage, filesystem, compute); } @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property] @@ -141,7 +140,7 @@ public static final class Builder { private String description; private String path; private Database database; - private S3Storage s3; + private ObjectStorage objectStorage; private AbstractFilesystem filesystem; private AbstractCompute compute; @@ -173,8 +172,8 @@ public Builder database(Database database) { return this; } - public Builder s3(S3Storage s3) { - this.s3 = s3; + public Builder objectStorage(ObjectStorage s3) { + this.objectStorage = s3; return this; } diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/compute/AbstractCompute.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/compute/AbstractCompute.java similarity index 95% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/compute/AbstractCompute.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/compute/AbstractCompute.java index a0971c4a..5f0c425e 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/compute/AbstractCompute.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/compute/AbstractCompute.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig.compute; +package com.amazon.aws.partners.saasfactory.saasboost.compute; +import com.amazon.aws.partners.saasfactory.saasboost.OperatingSystem; import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.OperatingSystem; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.compute.ecs.EcsCompute; +import com.amazon.aws.partners.saasfactory.saasboost.compute.ecs.EcsCompute; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -29,7 +29,6 @@ @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, property = "type" ) @JsonSubTypes({ diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/compute/AbstractComputeTier.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/compute/AbstractComputeTier.java similarity index 98% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/compute/AbstractComputeTier.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/compute/AbstractComputeTier.java index e871991e..550b8dfa 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/compute/AbstractComputeTier.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/compute/AbstractComputeTier.java @@ -1,4 +1,4 @@ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig.compute; +package com.amazon.aws.partners.saasfactory.saasboost.compute; import com.amazon.aws.partners.saasfactory.saasboost.Utils; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/compute/ComputeSize.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/compute/ComputeSize.java similarity index 94% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/compute/ComputeSize.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/compute/ComputeSize.java index e75767da..38857c7a 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/compute/ComputeSize.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/compute/ComputeSize.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig.compute; +package com.amazon.aws.partners.saasfactory.saasboost.compute; public enum ComputeSize { diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/compute/ecs/EcsCompute.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/compute/ecs/EcsCompute.java similarity index 91% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/compute/ecs/EcsCompute.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/compute/ecs/EcsCompute.java index c87e1cdd..20680df1 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/compute/ecs/EcsCompute.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/compute/ecs/EcsCompute.java @@ -1,8 +1,8 @@ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig.compute.ecs; +package com.amazon.aws.partners.saasfactory.saasboost.compute.ecs; +import com.amazon.aws.partners.saasfactory.saasboost.EcsLaunchType; import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.EcsLaunchType; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.compute.AbstractCompute; +import com.amazon.aws.partners.saasfactory.saasboost.compute.AbstractCompute; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; @@ -100,7 +100,7 @@ public Builder ecsLaunchType(String ecsLaunchType) { this.ecsLaunchType = EcsLaunchType.valueOf(ecsLaunchType); } catch (IllegalArgumentException e) { throw new RuntimeException( - new IllegalArgumentException("Can't find EcsLaunchType for value " + ecsLaunchType) + new IllegalArgumentException("Can't find EcsLaunchType for value " + ecsLaunchType) ); } } diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/compute/ecs/EcsComputeTier.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/compute/ecs/EcsComputeTier.java similarity index 87% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/compute/ecs/EcsComputeTier.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/compute/ecs/EcsComputeTier.java index c0a944a1..5f70c0fe 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/compute/ecs/EcsComputeTier.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/compute/ecs/EcsComputeTier.java @@ -1,6 +1,6 @@ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig.compute.ecs; +package com.amazon.aws.partners.saasfactory.saasboost.compute.ecs; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.compute.AbstractComputeTier; +import com.amazon.aws.partners.saasfactory.saasboost.compute.AbstractComputeTier; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/AbstractFilesystem.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/AbstractFilesystem.java similarity index 88% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/AbstractFilesystem.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/AbstractFilesystem.java index 8233a546..7e800597 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/AbstractFilesystem.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/AbstractFilesystem.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem; +package com.amazon.aws.partners.saasfactory.saasboost.filesystem; import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.efs.EfsFilesystem; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.fsx.FsxOntapFilesystem; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.fsx.FsxWindowsFilesystem; +import com.amazon.aws.partners.saasfactory.saasboost.filesystem.efs.EfsFilesystem; +import com.amazon.aws.partners.saasfactory.saasboost.filesystem.fsx.FsxOntapFilesystem; +import com.amazon.aws.partners.saasfactory.saasboost.filesystem.fsx.FsxWindowsFilesystem; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/AbstractFilesystemTierConfig.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/AbstractFilesystemTierConfig.java similarity index 93% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/AbstractFilesystemTierConfig.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/AbstractFilesystemTierConfig.java index eb912eb6..7a605532 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/AbstractFilesystemTierConfig.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/AbstractFilesystemTierConfig.java @@ -1,4 +1,4 @@ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem; +package com.amazon.aws.partners.saasfactory.saasboost.filesystem; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/efs/EfsFilesystem.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/efs/EfsFilesystem.java similarity index 92% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/efs/EfsFilesystem.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/efs/EfsFilesystem.java index 9669f765..c373ec2c 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/efs/EfsFilesystem.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/efs/EfsFilesystem.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.efs; +package com.amazon.aws.partners.saasfactory.saasboost.filesystem.efs; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.AbstractFilesystem; +import com.amazon.aws.partners.saasfactory.saasboost.filesystem.AbstractFilesystem; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/efs/EfsFilesystemTierConfig.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/efs/EfsFilesystemTierConfig.java similarity index 95% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/efs/EfsFilesystemTierConfig.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/efs/EfsFilesystemTierConfig.java index f7192b98..f9a44fb8 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/efs/EfsFilesystemTierConfig.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/efs/EfsFilesystemTierConfig.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.efs; +package com.amazon.aws.partners.saasfactory.saasboost.filesystem.efs; import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.AbstractFilesystemTierConfig; +import com.amazon.aws.partners.saasfactory.saasboost.filesystem.AbstractFilesystemTierConfig; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/AbstractFsxFilesystem.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/AbstractFsxFilesystem.java similarity index 93% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/AbstractFsxFilesystem.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/AbstractFsxFilesystem.java index 3c4b8673..d37577f9 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/AbstractFsxFilesystem.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/AbstractFsxFilesystem.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.fsx; +package com.amazon.aws.partners.saasfactory.saasboost.filesystem.fsx; import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.AbstractFilesystem; +import com.amazon.aws.partners.saasfactory.saasboost.filesystem.AbstractFilesystem; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import java.util.Objects; diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/AbstractFsxFilesystemTierConfig.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/AbstractFsxFilesystemTierConfig.java similarity index 95% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/AbstractFsxFilesystemTierConfig.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/AbstractFsxFilesystemTierConfig.java index 9f4ab1cd..5a2dbcb7 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/AbstractFsxFilesystemTierConfig.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/AbstractFsxFilesystemTierConfig.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.fsx; +package com.amazon.aws.partners.saasfactory.saasboost.filesystem.fsx; import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.AbstractFilesystemTierConfig; +import com.amazon.aws.partners.saasfactory.saasboost.filesystem.AbstractFilesystemTierConfig; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import java.util.Objects; diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/FsxOntapFilesystem.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/FsxOntapFilesystem.java similarity index 96% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/FsxOntapFilesystem.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/FsxOntapFilesystem.java index b8f4991f..fc14cfb5 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/FsxOntapFilesystem.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/FsxOntapFilesystem.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.fsx; +package com.amazon.aws.partners.saasfactory.saasboost.filesystem.fsx; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/FsxOntapFilesystemTierConfig.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/FsxOntapFilesystemTierConfig.java similarity index 96% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/FsxOntapFilesystemTierConfig.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/FsxOntapFilesystemTierConfig.java index bce0e5e9..5c84c072 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/FsxOntapFilesystemTierConfig.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/FsxOntapFilesystemTierConfig.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.fsx; +package com.amazon.aws.partners.saasfactory.saasboost.filesystem.fsx; import com.amazon.aws.partners.saasfactory.saasboost.Utils; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/FsxWindowsFilesystem.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/FsxWindowsFilesystem.java similarity index 96% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/FsxWindowsFilesystem.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/FsxWindowsFilesystem.java index 94eabadf..99040117 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/FsxWindowsFilesystem.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/FsxWindowsFilesystem.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.fsx; +package com.amazon.aws.partners.saasfactory.saasboost.filesystem.fsx; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/FsxWindowsFilesystemTierConfig.java b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/FsxWindowsFilesystemTierConfig.java similarity index 96% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/FsxWindowsFilesystemTierConfig.java rename to services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/FsxWindowsFilesystemTierConfig.java index 450318d5..3424c579 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/filesystem/fsx/FsxWindowsFilesystemTierConfig.java +++ b/services/app-config-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/fsx/FsxWindowsFilesystemTierConfig.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.fsx; +package com.amazon.aws.partners.saasfactory.saasboost.filesystem.fsx; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; diff --git a/functions/core-stack-listener/src/main/resources/lambda-assembly.xml b/services/app-config-service/src/main/resources/lambda-assembly.xml similarity index 100% rename from functions/core-stack-listener/src/main/resources/lambda-assembly.xml rename to services/app-config-service/src/main/resources/lambda-assembly.xml diff --git a/functions/core-stack-listener/src/main/resources/log4j2.xml b/services/app-config-service/src/main/resources/log4j2.xml similarity index 100% rename from functions/core-stack-listener/src/main/resources/log4j2.xml rename to services/app-config-service/src/main/resources/log4j2.xml diff --git a/services/app-config-service/src/main/resources/spotbugs-exclude.xml b/services/app-config-service/src/main/resources/spotbugs-exclude.xml new file mode 100644 index 00000000..17059f38 --- /dev/null +++ b/services/app-config-service/src/main/resources/spotbugs-exclude.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SettingsServiceDALTest.java b/services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigDataAccessLayerTest.java similarity index 54% rename from services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SettingsServiceDALTest.java rename to services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigDataAccessLayerTest.java index 6f68873d..aca36ed4 100644 --- a/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SettingsServiceDALTest.java +++ b/services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigDataAccessLayerTest.java @@ -1,246 +1,17 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.amazon.aws.partners.saasfactory.saasboost; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import software.amazon.awssdk.services.ssm.model.Parameter; -import software.amazon.awssdk.services.ssm.model.ParameterType; +import org.junit.jupiter.api.Test; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; - -import static org.junit.Assert.*; - -// Note that these tests will only work if you run them from Maven or if you add -// the AWS_REGION and SAAS_BOOST_ENV environment variables to your IDE's configuration settings -public class SettingsServiceDALTest { - - private static String env; - private static Setting emptyBillingApiKey; - private static Setting billingApiKey; - private HashMap appSettings; - - public SettingsServiceDALTest() { - if (System.getenv("AWS_REGION") == null || System.getenv("SAAS_BOOST_ENV") == null) { - throw new IllegalStateException("Missing required environment variables for tests!"); - } - } +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; - @BeforeClass - public static void setup() { - env = System.getenv("SAAS_BOOST_ENV"); - emptyBillingApiKey = Setting.builder() - .name("BILLING_API_KEY") - .value("") - .secure(true) - .readOnly(false) - .version(null) - .description(null) - .build(); - billingApiKey = Setting.builder() - .name("BILLING_API_KEY") - .value("1234567890") - .secure(true) - .readOnly(false) - .version(null) - .description(null) - .build(); - } +import static org.junit.jupiter.api.Assertions.assertTrue; - @Before - public void init() { - appSettings = new HashMap<>(); - appSettings.put("APP_NAME", "test"); - appSettings.put("DOMAIN_NAME", "example.com"); - appSettings.put("SSL_CERT_ARN", "arn:aws:acm:region:account:certificate/certificate_ID_1"); - appSettings.put("HEALTH_CHECK", "/index.html"); - appSettings.put("COMPUTE_SIZE", "M"); - appSettings.put("TASK_CPU", "1024"); - appSettings.put("TASK_MEMORY", "2048"); - appSettings.put("CONTAINER_PORT", "7000"); - appSettings.put("MIN_COUNT", "1"); - appSettings.put("MAX_COUNT", "2"); - appSettings.put("CLUSTER_OS", "LINUX"); - appSettings.put("CLUSTER_INSTANCE_TYPE", "t3.medium"); - } - - @Test - public void testFromParameterStore() { - String settingName = "SAAS_BOOST_BUCKET"; - String parameterName = "/" + SettingsServiceDAL.SAAS_BOOST_PREFIX + "/" + env + "/" + settingName; - String parameterValue = "sb-" + env + "-artifacts-test"; - - assertTrue("null parameter returns null setting", SettingsServiceDAL.fromParameterStore(null) == null); - assertThrows("null parameter name throws RuntimeException", RuntimeException.class, () -> {SettingsServiceDAL.fromParameterStore(Parameter.builder().build());}); - assertThrows("Empty parameter name throws RuntimeException", RuntimeException.class, () -> {SettingsServiceDAL.fromParameterStore(Parameter.builder().name("").build());}); - assertThrows("Blank parameter name is invalid pattern throws RuntimeException", RuntimeException.class, () -> {SettingsServiceDAL.fromParameterStore(Parameter.builder().name(" ").build());}); - assertThrows("Invalid pattern parameter name throws RuntimeException", RuntimeException.class, () -> {SettingsServiceDAL.fromParameterStore(Parameter.builder().name("foobar").build());}); - - Parameter validParam = Parameter.builder() - .name(parameterName) - .value(parameterValue) - .type(ParameterType.STRING) - .version(null) - .build(); - Setting expectedValidSetting = Setting.builder() - .name(settingName) - .value(parameterValue) - .readOnly(true) - .secure(false) - .version(null) - .description(null) - .build(); - assertEquals("Valid " + parameterName + " param equals " + settingName + " setting", expectedValidSetting, SettingsServiceDAL.fromParameterStore(validParam)); - - String readWriteParameterName ="/" + SettingsServiceDAL.SAAS_BOOST_PREFIX + "/" + env + "/APP_NAME"; - Parameter readWriteParameter = Parameter.builder() - .name(readWriteParameterName) - .value("foobar") - .type(ParameterType.STRING) - .version(null) - .build(); - Setting expectedReadWriteSetting = Setting.builder() - .name("APP_NAME") - .value("foobar") - .readOnly(false) - .secure(false) - .version(null) - .description(null) - .build(); - assertEquals("Read/Write param " + readWriteParameterName + " equals APP_NAME setting", expectedReadWriteSetting, SettingsServiceDAL.fromParameterStore(readWriteParameter)); - - Parameter emptyParameter = Parameter.builder() - .name(parameterName) - .value("N/A") - .type(ParameterType.STRING) - .version(null) - .build(); - Setting expectedEmptySetting = Setting.builder() - .name(settingName) - .value("") - .readOnly(true) - .secure(false) - .version(null) - .description(null) - .build(); - assertEquals("Empty " + parameterName + " param equals blank setting", expectedEmptySetting, SettingsServiceDAL.fromParameterStore(emptyParameter)); - - Parameter secretParameter = Parameter.builder() - .name(parameterName) - .value(parameterValue) - .type(ParameterType.SECURE_STRING) - .version(null) - .build(); - Setting expectedSecretSetting = Setting.builder() - .name(settingName) - .value(parameterValue) - .readOnly(true) - .secure(true) - .version(null) - .description(null) - .build(); - assertEquals("Valid secret param equals secure setting", expectedSecretSetting, SettingsServiceDAL.fromParameterStore(secretParameter)); - } - - @Test - public void testToParameterStore() { - String settingName = "SAAS_BOOST_BUCKET"; - String parameterName = "/" + SettingsServiceDAL.SAAS_BOOST_PREFIX + "/" + env + "/" + settingName; - String parameterValue = "sb-" + env + "-artifacts-test"; - - assertThrows("null setting throws RuntimeException", RuntimeException.class, () -> {SettingsServiceDAL.toParameterStore(null);}); - assertThrows("null setting name throws RuntimeException", RuntimeException.class, () -> {SettingsServiceDAL.toParameterStore(Setting.builder().build());}); - assertThrows("Empty setting name throws RuntimeException", RuntimeException.class, () -> {SettingsServiceDAL.toParameterStore(Setting.builder().name("").build());}); - assertThrows("Blank setting name throws RuntimeException", RuntimeException.class, () -> {SettingsServiceDAL.toParameterStore(Setting.builder().name(" ").build());}); - - Parameter expectedEmptyParameter = Parameter.builder() - .name(parameterName) - .type(ParameterType.STRING) - .value("N/A") - .build(); - Setting settingNullValue = Setting.builder() - .name(settingName) - .value(null) - .description(null) - .version(null) - .secure(false) - .readOnly(false) - .build(); - assertEquals("null setting value equals N/A parameter value", expectedEmptyParameter, SettingsServiceDAL.toParameterStore(settingNullValue)); - - Setting settingEmptyValue = Setting.builder() - .name(settingName) - .value("") - .description(null) - .version(null) - .secure(false) - .readOnly(false) - .build(); - assertEquals("Empty setting value equals N/A parameter value", expectedEmptyParameter, SettingsServiceDAL.toParameterStore(settingEmptyValue)); - - Parameter expectedBlankParameter = Parameter.builder() - .name(parameterName) - .type(ParameterType.STRING) - .value(" ") - .build(); - Setting settingBlankValue = Setting.builder() - .name(settingName) - .value(" ") - .description(null) - .version(null) - .secure(false) - .readOnly(false) - .build(); - assertEquals("Blank setting value equals N/A parameter value", expectedBlankParameter, SettingsServiceDAL.toParameterStore(settingBlankValue)); - - Parameter expectedValueParameter = Parameter.builder() - .name(parameterName) - .type(ParameterType.STRING) - .value(parameterValue) - .build(); - Setting settingWithValue = Setting.builder() - .name(settingName) - .value(parameterValue) - .description(null) - .version(null) - .secure(false) - .readOnly(false) - .build(); - assertEquals("Setting value equals parameter value", expectedValueParameter, SettingsServiceDAL.toParameterStore(settingWithValue)); - - Parameter expectedSecretParameter = Parameter.builder() - .name(parameterName) - .type(ParameterType.SECURE_STRING) - .value(parameterValue) - .build(); - Setting settingSecretValue = Setting.builder() - .name(settingName) - .value(parameterValue) - .description(null) - .version(null) - .secure(true) - .readOnly(false) - .build(); - assertEquals("Setting secret value equals secure parameter", expectedSecretParameter, SettingsServiceDAL.toParameterStore(settingSecretValue)); - } +public class AppConfigDataAccessLayerTest { @Test public void testToAppConfigNoExtensions() throws Exception { @@ -441,7 +212,7 @@ public void testRdsOptionsSorting() throws Exception { //System.out.println("Unsorted:"); //System.out.println(Utils.toJson(instances)); - Collections.sort(instances, SettingsServiceDAL.RDS_INSTANCE_COMPARATOR); + Collections.sort(instances, AppConfigDataAccessLayer.RDS_INSTANCE_COMPARATOR); //System.out.println(); //System.out.println("Sorted:"); @@ -567,45 +338,46 @@ public void testRdsOptionsSorting() throws Exception { } } - assertTrue("lastIndexOfT is defined", lastIndexOfT != -1); - assertTrue("lastIndexOfM is defined", lastIndexOfM != -1); - assertTrue("lastIndexOfM4 is defined", lastIndexOfM4 != -1); - assertTrue("firstIndexOfM is defined", firstIndexOfM != -1); - assertTrue("firstIndexOfR is defined", firstIndexOfR != -1); - assertTrue("firstIndexOfM5 is defined", firstIndexOfM5 != -1); - assertTrue("lastIndexOfMicro is defined", lastIndexOfMicro != -1); - assertTrue("lastIndexOfSmall is defined", lastIndexOfSmall != -1); - assertTrue("lastIndexOfMedium is defined", lastIndexOfMedium != -1); - assertTrue("lastIndexOfLarge is defined", lastIndexOfLarge != -1); - assertTrue("lastIndexOfXL is defined", lastIndexOfXL != -1); - assertTrue("lastIndexOf2XL is defined", lastIndexOf2XL != -1); - assertTrue("lastIndexOf4XL is defined", lastIndexOf4XL != -1); - assertTrue("lastIndexOf12XL is defined", lastIndexOf12XL != -1); - assertTrue("firstIndexOfSmall is defined", firstIndexOfSmall != -1); - assertTrue("firstIndexOfMedium is defined", firstIndexOfMedium != -1); - assertTrue("firstIndexOfLarge is defined", firstIndexOfLarge != -1); - assertTrue("firstIndexOfXL is defined", firstIndexOfXL != -1); - assertTrue("firstIndexOf2XL is defined", firstIndexOf2XL != -1); - assertTrue("firstIndexOf4XL is defined", firstIndexOf4XL != -1); - assertTrue("firstIndexOf12XL is defined", firstIndexOf12XL != -1); - assertTrue("firstIndexOf24XL is defined", firstIndexOf24XL != -1); + assertTrue(lastIndexOfT != -1, "lastIndexOfT is defined"); + assertTrue(lastIndexOfM != -1, "lastIndexOfM is defined"); + assertTrue(lastIndexOfM4 != -1, "lastIndexOfM4 is defined"); + assertTrue(firstIndexOfM != -1, "firstIndexOfM is defined"); + assertTrue(firstIndexOfR != -1, "firstIndexOfR is defined"); + assertTrue(firstIndexOfM5 != -1, "firstIndexOfM5 is defined"); + assertTrue(lastIndexOfMicro != -1, "lastIndexOfMicro is defined"); + assertTrue(lastIndexOfSmall != -1, "lastIndexOfSmall is defined"); + assertTrue(lastIndexOfMedium != -1, "lastIndexOfMedium is defined"); + assertTrue(lastIndexOfLarge != -1, "lastIndexOfLarge is defined"); + assertTrue(lastIndexOfXL != -1, "lastIndexOfXL is defined"); + assertTrue(lastIndexOf2XL != -1, "lastIndexOf2XL is defined"); + assertTrue(lastIndexOf4XL != -1, "lastIndexOf4XL is defined"); + assertTrue(lastIndexOf12XL != -1, "lastIndexOf12XL is defined"); + assertTrue(firstIndexOfSmall != -1, "firstIndexOfSmall is defined"); + assertTrue(firstIndexOfMedium != -1, "firstIndexOfMedium is defined"); + assertTrue(firstIndexOfLarge != -1, "firstIndexOfLarge is defined"); + assertTrue(firstIndexOfXL != -1, "firstIndexOfXL is defined"); + assertTrue(firstIndexOf2XL != -1, "firstIndexOf2XL is defined"); + assertTrue(firstIndexOf4XL != -1, "firstIndexOf4XL is defined"); + assertTrue(firstIndexOf12XL != -1, "firstIndexOf12XL is defined"); + assertTrue(firstIndexOf24XL != -1, "firstIndexOf24XL is defined"); // T's before M's before R's - assertTrue("T's before M's", lastIndexOfT < firstIndexOfM); - assertTrue("M's before R's", lastIndexOfM < firstIndexOfR); + assertTrue(lastIndexOfT < firstIndexOfM, "T's before M's"); + assertTrue(lastIndexOfM < firstIndexOfR, "M's before R's"); // Earlier generations before later - assertTrue("4th generations before 5th generations", lastIndexOfM4 < firstIndexOfM5); + assertTrue(lastIndexOfM4 < firstIndexOfM5, "4th generations before 5th generations"); // Smaller compute size before larger - assertTrue("MICRO before SMALL", lastIndexOfMicro < firstIndexOfSmall); - assertTrue("SMALL before MEDIUM", lastIndexOfSmall < firstIndexOfMedium); - assertTrue("MEDIUM before LARGE", lastIndexOfMedium < firstIndexOfLarge); - assertTrue("LARGE before XL", lastIndexOfLarge < firstIndexOfXL); - assertTrue("XL before 2XL", lastIndexOfXL < firstIndexOf2XL); - assertTrue("2XL before 4XL", lastIndexOf2XL < firstIndexOf4XL); - assertTrue("4XL before 12XL", lastIndexOf4XL < firstIndexOf12XL); - assertTrue("12XL before 24XL", lastIndexOf2XL < firstIndexOf24XL); + assertTrue(lastIndexOfMicro < firstIndexOfSmall, "MICRO before SMALL"); + assertTrue(lastIndexOfSmall < firstIndexOfMedium, "SMALL before MEDIUM"); + assertTrue(lastIndexOfMedium < firstIndexOfLarge, "MEDIUM before LARGE"); + assertTrue(lastIndexOfLarge < firstIndexOfXL, "LARGE before XL"); + assertTrue(lastIndexOfXL < firstIndexOf2XL, "XL before 2XL"); + assertTrue(lastIndexOf2XL < firstIndexOf4XL, "2XL before 4XL"); + assertTrue(lastIndexOf4XL < firstIndexOf12XL, "4XL before 12XL"); + assertTrue(lastIndexOf2XL < firstIndexOf24XL, "12XL before 24XL"); } } } + diff --git a/services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigHelperTest.java b/services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigHelperTest.java new file mode 100644 index 00000000..8aa358ab --- /dev/null +++ b/services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigHelperTest.java @@ -0,0 +1,161 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + + +public class AppConfigHelperTest { + + @Test + public void testIsDomainChanged() { + AppConfig existing = new AppConfig(); + AppConfig altered = new AppConfig(); + assertFalse(AppConfigHelper.isDomainChanged(existing, altered), "Both null"); + + existing.setDomainName(""); + altered.setDomainName(""); + assertFalse(AppConfigHelper.isDomainChanged(existing, altered), "Both empty"); + + existing.setDomainName("ABC"); + altered.setDomainName("abc"); + assertFalse(AppConfigHelper.isDomainChanged(existing, altered), "Ignore case"); + + existing = new AppConfig(); + altered.setDomainName("abc"); + assertTrue(AppConfigHelper.isDomainChanged(existing, altered), "null != non-empty"); + + existing = new AppConfig(); + existing.setDomainName("abc"); + altered = new AppConfig(); + assertTrue(AppConfigHelper.isDomainChanged(existing, altered), "null != non-empty"); + + existing = new AppConfig(); + existing.setDomainName("abc"); + altered = new AppConfig(); + altered.setDomainName("xzy"); + assertTrue(AppConfigHelper.isDomainChanged(existing, altered), "Different values"); + } + + @Test + public void testIsSslCertArnChanged() { + AppConfig existing = new AppConfig(); + AppConfig altered = new AppConfig(); + assertFalse(AppConfigHelper.isSslArnChanged(existing, altered), "Both null"); + + existing.setSslCertificate(""); + altered.setSslCertificate(""); + assertFalse(AppConfigHelper.isSslArnChanged(existing, altered), "Both empty"); + + existing.setSslCertificate("ABC"); + altered.setSslCertificate("abc"); + assertFalse(AppConfigHelper.isSslArnChanged(existing, altered), "Ignore case"); + + existing = new AppConfig(); + altered = new AppConfig(); + altered.setSslCertificate("abc"); + assertTrue(AppConfigHelper.isSslArnChanged(existing, altered), "null != non-empty"); + + existing = new AppConfig(); + existing.setSslCertificate("abc"); + altered = new AppConfig(); + assertTrue(AppConfigHelper.isSslArnChanged(existing, altered), "null != non-empty"); + + existing = new AppConfig(); + existing.setSslCertificate("abc"); + altered = new AppConfig(); + altered.setSslCertificate("xyz"); + assertTrue(AppConfigHelper.isSslArnChanged(existing, altered), "Different values"); + } + + @Test + public void testIsServicesChanged() { + AppConfig existing = new AppConfig(); + AppConfig altered = new AppConfig(); + assertFalse(AppConfigHelper.isServicesChanged(existing, altered)); + + Map services1 = new HashMap<>(); + services1.put("foo", ServiceConfig.builder().build()); + Map services2 = new HashMap<>(); + services2.put("foo", ServiceConfig.builder().build()); + existing.setServices(services1); + altered.setServices(services2); + assertFalse(AppConfigHelper.isServicesChanged(existing, altered)); + + existing = new AppConfig(); + existing.setServices(services1); + services2.put("bar", ServiceConfig.builder().build()); + altered.setServices(services2); + assertTrue(AppConfigHelper.isServicesChanged(existing, altered)); + + services2.remove("bar"); + altered.setServices(services2); + assertFalse(AppConfigHelper.isServicesChanged(existing, altered)); + + services1.clear(); + existing.setServices(services1); + assertTrue(AppConfigHelper.isServicesChanged(existing, altered)); + } + + @Test + public void testRemovedServices() { + AppConfig existing = new AppConfig(); + AppConfig altered = new AppConfig(); + assertTrue(AppConfigHelper.removedServices(existing, altered).isEmpty()); + + Map services1 = new HashMap<>(); + services1.put("foo", ServiceConfig.builder().build()); + Map services2 = new HashMap<>(); + services2.put("FOO", ServiceConfig.builder().build()); + existing.setServices(services1); + altered.setServices(services2); + // foo | FOO + assertTrue(AppConfigHelper.removedServices(existing, altered).isEmpty()); + + // foo | FOO,bar + services2.put("bar", ServiceConfig.builder().build()); + altered.setServices(services2); + assertTrue(AppConfigHelper.removedServices(existing, altered).isEmpty()); + + // foo | FOO + services2.remove("bar"); + altered.setServices(services2); + assertTrue(AppConfigHelper.removedServices(existing, altered).isEmpty()); + + // foo | bar + services2.remove("FOO"); + services2.put("bar", ServiceConfig.builder().build()); + altered.setServices(services2); + assertFalse(AppConfigHelper.removedServices(existing, altered).isEmpty()); + + // christmas,easter | bar,baz + services1.clear(); + services1.put("christmas", ServiceConfig.builder().build()); + services1.put("easter", ServiceConfig.builder().build()); + services2.clear(); + services2.put("bar", ServiceConfig.builder().build()); + services2.put("baz", ServiceConfig.builder().build()); + existing.setServices(services1); + altered.setServices(services2); + assertFalse(AppConfigHelper.removedServices(existing, altered).isEmpty()); + } +} diff --git a/services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigTest.java b/services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigTest.java new file mode 100644 index 00000000..330efd06 --- /dev/null +++ b/services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigTest.java @@ -0,0 +1,141 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class AppConfigTest { + + @Test + public void testEquals() { + AppConfig config1 = new AppConfig(); + AppConfig config2 = null; + + assertFalse(config1.equals(config2), "NULL is not equal"); + + config2 = config1; + assertTrue(config1.equals(config2), "Same instance"); + + assertFalse(config1.equals(new HashMap<>()), "Different types are not equal"); + + config2 = new AppConfig(); + assertTrue(config1.equals(config2), "Empty config objects are equal"); + + Map services1 = new HashMap<>(); + Map services2 = new HashMap<>(); + services1.put("foo", null); + services2.put("foo", null); + config1.setServices(services1); + config2.setServices(services2); + assertTrue(config1.equals(config2), "Both null services"); + + services1.put("foo", ServiceConfig.builder().build()); + services2.put("foo", ServiceConfig.builder().build()); + config1.setServices(services1); + config2.setServices(services2); + assertTrue(config1.equals(config2), "Same services"); + + services2.put("foo", null); + config1.setServices(services1); + config2.setServices(services2); + assertFalse(config1.equals(config2), "One service null"); + + services1.put("foo", null); + services2.put("foo", ServiceConfig.builder().build()); + config1.setServices(services1); + config2.setServices(services2); + assertFalse(config1.equals(config2), "One service null"); + + services1.put("foo", ServiceConfig.builder().build()); + services2.remove("foo"); + services2.put("bar", ServiceConfig.builder().build()); + config1.setServices(services1); + config2.setServices(services2); + assertFalse(config1.equals(config2), "Different service names"); + + services2.put("foo", ServiceConfig.builder().build()); + config1.setServices(services1); + config2.setServices(services2); + assertFalse(config1.equals(config2), "Different number of services"); + + services1.clear(); + services2.clear(); + services1.put("foo", ServiceConfig.builder().name("foo").build()); + services2.put("foo", ServiceConfig.builder().name("bar").build()); + config1.setServices(services1); + config2.setServices(services2); + assertFalse(config1.equals(config2), "Different service configs"); + + config1 = new AppConfig(); + config1.setName("foo"); + config1.setDomainName("bar"); + config1.setSslCertificate("baz"); + config1.setServices(Map.of("foo", ServiceConfig.builder().build())); + + config2 = new AppConfig(); + config2.setName("foo"); + config2.setDomainName("bar"); + config2.setSslCertificate("baz"); + config2.setServices(Map.of("foo", ServiceConfig.builder().build())); + assertTrue(config1.equals(config2), "Same name"); + } + + @Test + public void testDeserialize() { + try (InputStream is = getClass().getClassLoader().getResourceAsStream("appConfig.json")) { + AppConfig appConfig = Utils.MAPPER.readValue(is, AppConfig.class); + assertNotNull(appConfig); + //System.out.println(appConfig.toString()); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + + @Test + public void testIsEmpty() { + AppConfig config = new AppConfig(); + assertTrue(config.isEmpty()); + + AppConfig config2 = new AppConfig(); + config2.setName("test"); + assertFalse(config2.isEmpty()); + + AppConfig config3 = new AppConfig(); + config3.setDomainName("example.com"); + assertFalse(config3.isEmpty()); + + AppConfig config4 = new AppConfig(); + config4.setHostedZone("123456"); + assertFalse(config4.isEmpty()); + + AppConfig config5 = new AppConfig(); + config5.setSslCertificate("arn:aws:acm:xxxxx"); + assertFalse(config5.isEmpty()); + + AppConfig config6 = new AppConfig(); + config6.setServices(Map.of("foo", ServiceConfig.builder().build())); + assertFalse(config6.isEmpty()); + } + +} diff --git a/services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ObjectStorageTest.java b/services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ObjectStorageTest.java new file mode 100644 index 00000000..4fb016e5 --- /dev/null +++ b/services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ObjectStorageTest.java @@ -0,0 +1,12 @@ +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ObjectStorageTest { + @Test + public void basic() { + assertEquals(new ObjectStorage(ObjectStorage.builder()), Utils.fromJson("{}", ObjectStorage.class)); + } +} diff --git a/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/compute/AbstractComputeTest.java b/services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/compute/AbstractComputeTest.java similarity index 63% rename from services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/compute/AbstractComputeTest.java rename to services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/compute/AbstractComputeTest.java index d827f8fd..b4dcb59b 100644 --- a/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/compute/AbstractComputeTest.java +++ b/services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/compute/AbstractComputeTest.java @@ -1,25 +1,43 @@ package com.amazon.aws.partners.saasfactory.saasboost.compute; import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.compute.AbstractCompute; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.compute.AbstractComputeTier; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.compute.ecs.EcsCompute; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.compute.ecs.EcsComputeTier; -import org.junit.Test; +import com.amazon.aws.partners.saasfactory.saasboost.compute.ecs.EcsCompute; +import com.amazon.aws.partners.saasfactory.saasboost.compute.ecs.EcsComputeTier; +import org.junit.jupiter.api.Test; import java.util.Map; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.*; public class AbstractComputeTest { @Test public void deserialize_ecsCompute_basic() { - String ecsJson = "{\"type\":\"ECS\", \"ecsExecEnabled\": true, \"tiers\":{" - + "\"Free\":{\"instanceType\":\"t3.medium\", \"cpu\":512, \"memory\":1024, \"min\":1, \"max\":2, \"ec2min\":0, \"ec2max\":5}," - + "\"Gold\":{\"instanceType\":\"t3.large\", \"cpu\":1024, \"memory\":2048, \"min\":2, \"max\":4, \"ec2min\":5, \"ec2max\":9}}}"; - AbstractCompute compute = Utils.fromJson(ecsJson, AbstractCompute.class); + String json = """ + { + "type": "ECS", + "ecsExecEnabled": true, + "tiers": { + "Free": { + "instanceType": "t3.medium", + "cpu": 512, + "memory": 1024, + "min": 1, + "max": 2, + "ec2min": 0, + "ec2max": 5 + }, + "Gold": { + "instanceType": "t3.large", + "cpu": 1024, + "memory": 2048, + "min": 2, + "max": 4, + "ec2min": 5, + "ec2max": 9 + } + } + }"""; + AbstractCompute compute = Utils.fromJson(json, AbstractCompute.class); assertEquals(EcsCompute.class, compute.getClass()); assertEquals(Boolean.TRUE, ((EcsCompute) compute).getEcsExecEnabled()); assertNotNull(compute.getTiers()); diff --git a/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/AbstractFilesystemTest.java b/services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/AbstractFilesystemTest.java similarity index 89% rename from services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/AbstractFilesystemTest.java rename to services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/AbstractFilesystemTest.java index 42da0153..39b7096a 100644 --- a/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/AbstractFilesystemTest.java +++ b/services/app-config-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/filesystem/AbstractFilesystemTest.java @@ -17,22 +17,17 @@ package com.amazon.aws.partners.saasfactory.saasboost.filesystem; import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.AbstractFilesystem; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.AbstractFilesystemTierConfig; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.efs.EfsFilesystem; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.efs.EfsFilesystemTierConfig; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.fsx.FsxOntapFilesystem; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.fsx.FsxOntapFilesystemTierConfig; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.fsx.FsxWindowsFilesystem; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.filesystem.fsx.FsxWindowsFilesystemTierConfig; -import org.junit.Test; +import com.amazon.aws.partners.saasfactory.saasboost.filesystem.efs.EfsFilesystem; +import com.amazon.aws.partners.saasfactory.saasboost.filesystem.efs.EfsFilesystemTierConfig; +import com.amazon.aws.partners.saasfactory.saasboost.filesystem.fsx.FsxOntapFilesystem; +import com.amazon.aws.partners.saasfactory.saasboost.filesystem.fsx.FsxOntapFilesystemTierConfig; +import com.amazon.aws.partners.saasfactory.saasboost.filesystem.fsx.FsxWindowsFilesystem; +import com.amazon.aws.partners.saasfactory.saasboost.filesystem.fsx.FsxWindowsFilesystemTierConfig; +import org.junit.jupiter.api.Test; import java.util.Map; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.*; public class AbstractFilesystemTest { diff --git a/services/settings-service/src/test/resources/appConfig.json b/services/app-config-service/src/test/resources/appConfig.json similarity index 99% rename from services/settings-service/src/test/resources/appConfig.json rename to services/app-config-service/src/test/resources/appConfig.json index cbe421af..e3ed0be9 100644 --- a/services/settings-service/src/test/resources/appConfig.json +++ b/services/app-config-service/src/test/resources/appConfig.json @@ -189,6 +189,5 @@ } } } - }, - "billing": null + } } \ No newline at end of file diff --git a/services/settings-service/src/test/resources/rdsInstancesUnsorted.json b/services/app-config-service/src/test/resources/rdsInstancesUnsorted.json similarity index 100% rename from services/settings-service/src/test/resources/rdsInstancesUnsorted.json rename to services/app-config-service/src/test/resources/rdsInstancesUnsorted.json diff --git a/functions/ecs-startup-services/update.sh b/services/app-config-service/update.sh similarity index 81% rename from functions/ecs-startup-services/update.sh rename to services/app-config-service/update.sh index b4cc945b..671daeeb 100755 --- a/functions/ecs-startup-services/update.sh +++ b/services/app-config-service/update.sh @@ -26,7 +26,7 @@ LAMBDA_STAGE_FOLDER=$2 if [ -z $LAMBDA_STAGE_FOLDER ]; then LAMBDA_STAGE_FOLDER="lambdas" fi -LAMBDA_CODE=EcsStartupServices-lambda.zip +LAMBDA_CODE=AppConfigService-lambda.zip #set this for V2 AWS CLI to disable paging export AWS_PAGER="" @@ -48,9 +48,10 @@ fi # And copy it up to S3 aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ -eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-ecs-startup-services\`)] | [].FunctionName' --output text"\) - -for FUNCTION in ${FUNCTIONS[@]}; do - #echo $FUNCTION - aws lambda --region $MY_AWS_REGION update-function-code --function-name $FUNCTION --s3-bucket $SAAS_BOOST_BUCKET --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE +# Find all the functions for this microservice +eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-app-config-\`)] | [].FunctionName' --output text"\) +FUNCTIONS=($FUNCTIONS) +for FX in "${FUNCTIONS[@]}"; do + printf "Updating function code for %s\n" $FX + aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "$FX" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE done diff --git a/resources/custom-resources/rds-bootstrap/pom.xml b/services/billing-service/pom.xml similarity index 58% rename from resources/custom-resources/rds-bootstrap/pom.xml rename to services/billing-service/pom.xml index c99fc957..4f0300c7 100644 --- a/resources/custom-resources/rds-bootstrap/pom.xml +++ b/services/billing-service/pom.xml @@ -19,10 +19,10 @@ limitations under the License. 4.0.0 com.amazon.aws.partners.saasfactory.saasboost - saasboost-custom-resources + saasboost-services 1.0.0 - RdsBootstrap + BillingService 1.0.0 jar @@ -31,7 +31,9 @@ limitations under the License. http://www.apache.org/licenses/LICENSE-2.0 + + ${project.basedir}/../.. 0 @@ -54,32 +56,51 @@ limitations under the License. io.github.git-commit-id git-commit-id-maven-plugin + + com.github.spotbugs + spotbugs-maven-plugin + + Max + medium + + + software.amazon.lambda.snapstart + aws-lambda-snapstart-java-rules + 0.1.0 + + + ${project.basedir}/src/main/resources/spotbugs-exclude.xml + +
    com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - - - com.amazon.aws.partners.saasfactory.saasboost - CloudFormationUtils + ApiGatewayHelper 1.0.0 provided software.amazon.awssdk - url-connection-client + dynamodb ${aws.java.sdk.version} + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + apache-client + + software.amazon.awssdk - s3 + eventbridge ${aws.java.sdk.version} @@ -94,7 +115,7 @@ limitations under the License. software.amazon.awssdk - ssm + marketplacecatalog ${aws.java.sdk.version} @@ -108,19 +129,34 @@ limitations under the License. - org.postgresql - postgresql - 42.4.3 - - - org.mariadb.jdbc - mariadb-java-client - 2.7.5 + software.amazon.awssdk + marketplaceentitlement + ${aws.java.sdk.version} + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + apache-client + + - com.microsoft.sqlserver - mssql-jdbc - 10.2.0.jre11 + software.amazon.awssdk + marketplacemetering + ${aws.java.sdk.version} + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + apache-client + + diff --git a/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AbstractBillingProviderApi.java b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AbstractBillingProviderApi.java new file mode 100644 index 00000000..22139923 --- /dev/null +++ b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AbstractBillingProviderApi.java @@ -0,0 +1,17 @@ +package com.amazon.aws.partners.saasfactory.saasboost; + +public abstract class AbstractBillingProviderApi implements BillingProviderApi { + + private BillingProviderCredentials credentials; + + public AbstractBillingProviderApi(BillingProviderCredentials credentials) { + this.credentials = credentials; + } + + private AbstractBillingProviderApi() { + } + + public BillingProviderCredentials getCredentials() { + return credentials; + } +} \ No newline at end of file diff --git a/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AwsMarketplaceApi.java b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AwsMarketplaceApi.java new file mode 100644 index 00000000..4e639a0c --- /dev/null +++ b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AwsMarketplaceApi.java @@ -0,0 +1,71 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.marketplacecatalog.MarketplaceCatalogClient; + +import java.util.Collection; +import java.util.Collections; + +public class AwsMarketplaceApi extends AbstractBillingProviderApi { + + private static final Logger LOGGER = LoggerFactory.getLogger(StripeApi.class); + private final MarketplaceCatalogClient marketplace; + + public AwsMarketplaceApi(IamCredentials credentials, String catalogEntityId) { + this(credentials, new DefaultDependencyFactory(catalogEntityId)); + } + + public AwsMarketplaceApi(IamCredentials credentials, AwsMarketplaceApiDependencyFactory init) { + super(credentials); + this.marketplace = init.marketplace(); + } + + @Override + public Collection getPlans() { + // Marketplace Product & Pricing Dimensions + return Collections.emptyList(); + } + + interface AwsMarketplaceApiDependencyFactory { + + String catalogEntityId(); + + MarketplaceCatalogClient marketplace(); + } + + private static final class DefaultDependencyFactory implements AwsMarketplaceApiDependencyFactory { + + private String catalogEntityId; + + public DefaultDependencyFactory(String catalogEntityId) { + this.catalogEntityId = catalogEntityId; + } + + @Override + public String catalogEntityId() { + return catalogEntityId; + } + + @Override + public MarketplaceCatalogClient marketplace() { + return Utils.sdkClient(MarketplaceCatalogClient.builder(), MarketplaceCatalogClient.SERVICE_NAME); + } + } +} diff --git a/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AwsMarketplaceProvider.java b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AwsMarketplaceProvider.java new file mode 100644 index 00000000..53b18077 --- /dev/null +++ b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AwsMarketplaceProvider.java @@ -0,0 +1,74 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import software.amazon.awssdk.services.sts.StsClient; + +import java.util.Properties; + +public class AwsMarketplaceProvider implements BillingProvider { + + static final Properties DEFAULTS = new Properties(); + + static { + DEFAULTS.put("assumedRole", ""); + DEFAULTS.put("catalogEntityId", ""); + } + + private final Properties properties; + private final StsClient sts; + + public AwsMarketplaceProvider(Properties properties) { + this(properties, new DefaultDependencyFactory()); + } + + public AwsMarketplaceProvider(Properties properties, AwsMarketplaceBillingProviderDependencyFactory init) { + this.properties = new Properties(DEFAULTS); + this.properties.putAll(properties); + this.sts = init.sts(); + } + + @Override + public ProviderType type() { + return ProviderType.STRIPE; + } + + @Override + public Properties getProperties() { + return (Properties) properties.clone(); + } + + @Override + public BillingProviderApi api() { + IamCredentials credentials = new IamCredentials(sts, properties.getProperty("assumeRole")); + AwsMarketplaceApi api = new AwsMarketplaceApi(credentials, properties.getProperty("catalogEntityId")); + return api; + } + + interface AwsMarketplaceBillingProviderDependencyFactory { + + StsClient sts(); + } + + private static final class DefaultDependencyFactory implements AwsMarketplaceBillingProviderDependencyFactory { + + @Override + public StsClient sts() { + return Utils.sdkClient(StsClient.builder(), StsClient.SERVICE_NAME); + } + } +} diff --git a/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingDataAccessLayer.java b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingDataAccessLayer.java new file mode 100644 index 00000000..a64b8375 --- /dev/null +++ b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingDataAccessLayer.java @@ -0,0 +1,109 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.DynamoDbException; +import software.amazon.awssdk.services.dynamodb.model.QueryResponse; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class BillingDataAccessLayer { + + private static final Logger LOGGER = LoggerFactory.getLogger(BillingDataAccessLayer.class); + private final String billingTable; + private final DynamoDbClient ddb; + private final SecretsManagerClient secrets; + private final String providerConfigSecretId; + + public BillingDataAccessLayer(DynamoDbClient ddb, String billingTable, + SecretsManagerClient secrets, String providerConfigSecretId) { + this.billingTable = billingTable; + this.ddb = ddb; + this.secrets = secrets; + this.providerConfigSecretId = providerConfigSecretId; + + // Cold start performance hack -- take the TLS hit for the client in the constructor + this.ddb.describeTable(request -> request.tableName(billingTable)); + } + + public List getPlans() { + BillingProviderConfig providerConfig = getProviderConfig(); + List plans = new ArrayList<>(); + + plans.addAll(mockBillingPlans()); + /* + if (providerConfig != null) { + try { + QueryResponse response = ddb.query(request -> request + .tableName(billingTable) + ); + response.items().forEach(item -> + plans.add(fromAttributeValueMap(item)) + ); + } catch (DynamoDbException e) { + LOGGER.error(Utils.getFullStackTrace(e)); + throw new RuntimeException(e); + } + } + */ + return plans; + } + + private List mockBillingPlans() { + BillingPlan standardPlan = BillingPlan.builder() + .id(UUID.randomUUID().toString()) + .build(); + BillingPlan enterprisePlan = BillingPlan.builder() + .id(UUID.randomUUID().toString()) + .build(); + return List.of(standardPlan, enterprisePlan); + } + + public List getAvailableProviders() { + List providers = new ArrayList<>(); + BillingProviderConfig activeProvider = getProviderConfig(); + for (BillingProvider.ProviderType type : BillingProvider.ProviderType.values()) { + if (activeProvider != null && type == activeProvider.getType()) { + providers.add(activeProvider); + } else { + providers.add(new BillingProviderConfig(type)); + } + } + return providers; + } + + public BillingProviderConfig getProviderConfig() { + GetSecretValueResponse response = secrets.getSecretValue(request -> request + .secretId(providerConfigSecretId) + ); + return Utils.fromJson(response.secretString(), BillingProviderConfig.class); + } + + public void setProviderConfig(BillingProviderConfig providerConfig) { + secrets.putSecretValue(request -> request + .secretId(providerConfigSecretId) + .secretString(Utils.toJson(providerConfig)) + ); + } +} diff --git a/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingEvent.java b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingEvent.java new file mode 100644 index 00000000..cda0abd0 --- /dev/null +++ b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingEvent.java @@ -0,0 +1,86 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.UUID; + +public enum BillingEvent { + + BILLING_SUBSCRIBED("Billing Subscribed"), + BILLING_UNSUBSCRIBED("Billing Unsubscribed") + ; + + private static final Logger LOGGER = LoggerFactory.getLogger(BillingEvent.class); + private final String detailType; + + BillingEvent(String detailType) { + this.detailType = detailType; + } + + public String detailType() { + return detailType; + } + + public static BillingEvent fromDetailType(String detailType) { + BillingEvent event = null; + for (BillingEvent onboardingEvent : BillingEvent.values()) { + if (onboardingEvent.detailType().equals(detailType)) { + event = onboardingEvent; + break; + } + } + return event; + } + + public static boolean validate(Map event) { + return validate(event, null); + } + + public static boolean validate(Map event, String... requiredKeys) { + if (event == null || !event.containsKey("detail") || !event.containsKey("source")) { + LOGGER.error("Event is null or is missing 'detail' or 'source' attributes"); + return false; + } + if (!"saas-boost".equals(event.get("source"))) { + LOGGER.error("Event 'source' != saas-boost"); + return false; + } + try { + Map detail = (Map) event.get("detail"); + if (detail == null) { + LOGGER.error("Event is missing 'detail'"); + return false; + } + if (requiredKeys != null) { + for (String requiredKey : requiredKeys) { + if (!detail.containsKey(requiredKey)) { + LOGGER.error("Event 'detail' is missing required key '" + requiredKey + "'"); + return false; + } + } + } + } catch (ClassCastException cce) { + LOGGER.error("Event detail is not a Map " + cce.getMessage()); + return false; + } + return true; + } +} diff --git a/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingPlan.java b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingPlan.java new file mode 100644 index 00000000..1abd3fc4 --- /dev/null +++ b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingPlan.java @@ -0,0 +1,114 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; + +import java.util.Objects; + +@JsonDeserialize(builder = BillingPlan.Builder.class) +public class BillingPlan { + + private final String id; + private final String providerId; + private final String name; + + private BillingPlan(Builder builder) { + this.id = builder.id; + this.providerId = builder.providerId; + this.name = builder.name; + } + + public String getId() { + return id; + } + + public String getProviderId() { + return providerId; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return name; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + // Same reference? + if (this == obj) { + return true; + } + // Same type? + if (getClass() != obj.getClass()) { + return false; + } + final BillingPlan other = (BillingPlan) obj; + return (Objects.equals(id, other.id) + && Objects.equals(providerId, other.providerId) + && Objects.equals(name, other.name)); + } + + @Override + public int hashCode() { + return Objects.hash(id, providerId, name); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property] + public static final class Builder { + + private String id; + private String providerId; + private String name; + + private Builder() { + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder providerId(String providerId) { + this.providerId = providerId; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public BillingPlan build() { + if (name == null || name.isBlank()) { + throw new IllegalStateException("Can't build BillingPlan without name"); + } + return new BillingPlan(this); + } + } +} diff --git a/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingProvider.java b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingProvider.java new file mode 100644 index 00000000..cfeb0c41 --- /dev/null +++ b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import java.util.Properties; + +public interface BillingProvider { + + enum ProviderType { + AWS_MARKETPLACE, + STRIPE + } + + ProviderType type(); + + Properties getProperties(); + + BillingProviderApi api(); +} diff --git a/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/dal/exception/TierNotFoundException.java b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingProviderApi.java similarity index 75% rename from services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/dal/exception/TierNotFoundException.java rename to services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingProviderApi.java index 0a29c0b1..41e55d05 100644 --- a/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/dal/exception/TierNotFoundException.java +++ b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingProviderApi.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.dal.exception; +package com.amazon.aws.partners.saasfactory.saasboost; -public class TierNotFoundException extends RuntimeException { - public TierNotFoundException(String message) { - super(message); - } +import java.util.Collection; + +public interface BillingProviderApi { + + Collection getPlans(); } diff --git a/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingProviderConfig.java b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingProviderConfig.java new file mode 100644 index 00000000..2b9b3a5f --- /dev/null +++ b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingProviderConfig.java @@ -0,0 +1,59 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; +import java.util.Properties; + +public class BillingProviderConfig { + + private final BillingProvider.ProviderType type; + private final Properties properties; + + @JsonCreator + public BillingProviderConfig(@JsonProperty("type") BillingProvider.ProviderType type) { + this.type = type; + this.properties = new Properties(); + switch (this.type) { + case AWS_MARKETPLACE: + this.properties.putAll(AwsMarketplaceProvider.DEFAULTS); + break; + case STRIPE: + this.properties.putAll(StripeBillingProvider.DEFAULTS); + break; + default: + break; + } + } + + public BillingProvider.ProviderType getType() { + return this.type; + } + + public Properties getProperties() { + return (Properties) properties.clone(); + } + + public void setProperties(Map properties) { + if (properties != null) { + this.properties.putAll(properties); + } + } +} diff --git a/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingProviderCredentials.java b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingProviderCredentials.java new file mode 100644 index 00000000..e7be7053 --- /dev/null +++ b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingProviderCredentials.java @@ -0,0 +1,13 @@ +package com.amazon.aws.partners.saasfactory.saasboost; + +public interface BillingProviderCredentials { + + enum CredentialType { + IAM, + JWT + } + + CredentialType type(); + + Object resolveCredentials(); +} diff --git a/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingProviderFactory.java b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingProviderFactory.java new file mode 100644 index 00000000..e3044a9f --- /dev/null +++ b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingProviderFactory.java @@ -0,0 +1,47 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BillingProviderFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(BillingProviderFactory.class); + + private BillingProviderFactory() { + } + + public static BillingProviderFactory getInstance() { + return BillingProviderFactoryInstance.instance; + } + + public BillingProvider getProvider(BillingProviderConfig providerConfig) { + switch (providerConfig.getType()) { + case AWS_MARKETPLACE: + return new AwsMarketplaceProvider(providerConfig.getProperties()); + case STRIPE: + return new StripeBillingProvider(providerConfig.getProperties()); + default: + return null; + } + } + + private static class BillingProviderFactoryInstance { + public static final BillingProviderFactory instance = new BillingProviderFactory(); + } +} diff --git a/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingService.java b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingService.java new file mode 100644 index 00000000..6cd5f5ea --- /dev/null +++ b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/BillingService.java @@ -0,0 +1,292 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.eventbridge.EventBridgeClient; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; + +import java.net.HttpURLConnection; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class BillingService { + + private static final Logger LOGGER = LoggerFactory.getLogger(BillingService.class); + private static final Map CORS = Map.of("Access-Control-Allow-Origin", "*"); + private static final String API_APP_CLIENT = System.getenv("API_APP_CLIENT"); + private static final String BILLING_TABLE = System.getenv("BILLING_TABLE"); + private static final String SAAS_BOOST_EVENT_BUS = System.getenv("SAAS_BOOST_EVENT_BUS"); + private static final String EVENT_SOURCE = "saas-boost"; + private static final String BILLING_PROVIDER_CONFIG = System.getenv("BILLING_PROVIDER_CONFIG"); + private final BillingDataAccessLayer dal; + private final EventBridgeClient eventBridge; + private ApiGatewayHelper api; + + public BillingService() { + this(new DefaultDependencyFactory()); + } + + // Facilitates testing by being able to mock out AWS SDK dependencies + public BillingService(BillingServiceDependencyFactory init) { + if (Utils.isBlank(SAAS_BOOST_EVENT_BUS)) { + throw new IllegalStateException("Missing required environment variable SAAS_BOOST_EVENT_BUS"); + } + if (Utils.isBlank(BILLING_TABLE)) { + throw new IllegalStateException("Missing environment variable BILLING_TABLE"); + } + if (Utils.isBlank(BILLING_PROVIDER_CONFIG)) { + throw new IllegalStateException("Missing environment variable BILLING_PROVIDER_CONFIG"); + } + LOGGER.info("Version Info: {}", Utils.version(this.getClass())); + this.dal = init.dal(); + this.eventBridge = init.eventBridge(); + } + + /** + * Get billing plans for the active provider. Integration for GET /billing/plans endpoint + * @param event API Gateway proxy request event + * @param context Lambda function context + * @return List of billing plan objects + */ + public APIGatewayProxyResponseEvent getPlans(APIGatewayProxyRequestEvent event, Context context) { + if (Utils.warmup(event)) { + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); + } + + //Utils.logRequestEvent(event); + List plans = dal.getPlans(); + APIGatewayProxyResponseEvent response; + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_OK) + .withBody(Utils.toJson(plans)); + return response; + } + + /** + * Get available billing providers as config templates. Integration for GET /billing/providers endpoint + * @param event API Gateway proxy request event + * @param context Lambda function context + * @return List of onboarding objects + */ + public APIGatewayProxyResponseEvent getProviders(APIGatewayProxyRequestEvent event, Context context) { + if (Utils.warmup(event)) { + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); + } + + //Utils.logRequestEvent(event); + APIGatewayProxyResponseEvent response; + List providers = dal.getAvailableProviders(); + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_OK) + .withBody(Utils.toJson(providers)); + + return response; + } + + /** + * Returns the active Billing Provider configuration. Integration for GET /billing/provider endpoint. + * @param event API Gateway proxy request event + * @param context Lambda function context + * @return + */ + public APIGatewayProxyResponseEvent getProvider(APIGatewayProxyRequestEvent event, Context context) { + if (Utils.warmup(event)) { + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); + } + + //Utils.logRequestEvent(event); + APIGatewayProxyResponseEvent response; + BillingProviderConfig providerConfig = dal.getProviderConfig(); + if (providerConfig != null) { + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_OK) + .withBody(Utils.toJson(providerConfig)); + } else { + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_NOT_FOUND); + } + + return response; + } + + /** + * Sets the active Billing Provider configuration. Integration for POST /billing/provider endpoint. + * @param event API Gateway proxy request event + * @param context Lambda function context + * @return + */ + public APIGatewayProxyResponseEvent setProvider(APIGatewayProxyRequestEvent event, Context context) { + if (Utils.warmup(event)) { + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); + } + + //Utils.logRequestEvent(event); + BillingProviderConfig providerConfig = Utils.fromJson(event.getBody(), BillingProviderConfig.class); + APIGatewayProxyResponseEvent response; + if (providerConfig == null) { + response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) + .withHeaders(CORS) + .withBody(Utils.toJson(Map.of("message", "Invalid request body"))); + } else { + dal.setProviderConfig(providerConfig); + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_CREATED); + } + return response; + } + + /** + * Event listener (EventBridge Rule target) for Billing Events. The single + * public entry point both reduces the number of rules and simplifies debug logging. + * @param event the EventBridge event + */ + public void handleBillingEvent(Map event) { + Utils.logRequestEvent(event); + if (BillingEvent.validate(event)) { + String detailType = (String) event.get("detail-type"); + BillingEvent billingEvent = BillingEvent.fromDetailType(detailType); + if (billingEvent != null) { + switch (billingEvent) { + default: + LOGGER.error("Unknown Billing Event!"); + } + } else if (detailType.startsWith("Tenant ")) { + // Tenant events that trigger billing system workflows + // Use this entry point for consolidated logging of the billing system + LOGGER.info("Handling Tenant Event"); + handleTenantEvent(event); + } else { + LOGGER.error("Can't find billing event for detail-type {}", event.get("detail-type")); + // TODO Throw here? Would end up in DLQ. + } + } else { + LOGGER.error("Invalid SaaS Boost Billing Event " + Utils.toJson(event)); + // TODO Throw here? Would end up in DLQ. + } + } + + protected void handleTenantEvent(Map event) { + String detailType = (String) event.get("detail-type"); + if ("Tenant Onboarding Status Changed".equals(detailType)) { + LOGGER.info("Handling Tenant Onboarding Status Changed Event"); + handleTenantOnboardingStatusChangedEvent(event); + } + } + + protected void handleTenantOnboardingStatusChangedEvent(Map event) { + @SuppressWarnings("unchecked") + Map detail = (Map) event.get("detail"); + String onboardingStatus = (String) detail.get("onboardingStatus"); + if ("deployed".equals(onboardingStatus)) { + String tenantId = (String) detail.get("tenantId"); + if (Utils.isNotBlank(tenantId)) { + Map tenant = getTenant(tenantId); + if (tenant != null) { + String tierName = (String) tenant.getOrDefault("tier", ""); + Map tier = getTier(tierName); + if (tier != null && tier.containsKey("billingPlan")) { + // TODO wire-up actual billing provider subscription for a given plan + } else { + LOGGER.info("Tenant {} is subscribed to Tier {} with no billing plan", tenant, tierName); + } + // Let everyone know this tenant is subscribed + Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, + BillingEvent.BILLING_SUBSCRIBED.detailType(), + Map.of("tenantId", tenantId) + ); + } else { + LOGGER.error("Can't fetch tenant from TenantService {}", detail.get("tenantId")); + } + } else { + LOGGER.error("Missing tenantId in event detail {}", Utils.toJson(event.get("detail"))); + } + } + } + + protected Map getTenant(String tenantId) { + LOGGER.info("Calling tenant service to fetch tenant by id {}", tenantId); + LinkedHashMap tenant = null; + if (Utils.isNotBlank(tenantId)) { + ApiGatewayHelper api = getApiGatewayHelper(); + String getTenantResponseBody = api.authorizedRequest("GET", "tenants/" + tenantId); + tenant = Utils.fromJson(getTenantResponseBody, LinkedHashMap.class); + } + return tenant; + } + + protected Map getTier(String tierName) { + LOGGER.info("Calling tier service to fetch tier by id {}", tierName); + LinkedHashMap tier = null; + if (Utils.isNotBlank(tierName)) { + ApiGatewayHelper api = getApiGatewayHelper(); + String getTierResponseBody = api.authorizedRequest("GET", "tiers?name=" + + URLEncoder.encode(tierName, StandardCharsets.UTF_8)); + tier = Utils.fromJson(getTierResponseBody, LinkedHashMap.class); + } + return tier; + } + + private ApiGatewayHelper getApiGatewayHelper() { + if (this.api == null) { + this.api = ApiGatewayHelper.clientCredentialsHelper(API_APP_CLIENT); + } + return this.api; + } + + interface BillingServiceDependencyFactory { + + BillingDataAccessLayer dal(); + + EventBridgeClient eventBridge(); + } + + private static final class DefaultDependencyFactory implements BillingServiceDependencyFactory { + + @Override + public BillingDataAccessLayer dal() { + return new BillingDataAccessLayer( + Utils.sdkClient(DynamoDbClient.builder(), DynamoDbClient.SERVICE_NAME), + BILLING_TABLE, + Utils.sdkClient(SecretsManagerClient.builder(), SecretsManagerClient.SERVICE_NAME), + BILLING_PROVIDER_CONFIG); + } + + @Override + public EventBridgeClient eventBridge() { + return Utils.sdkClient(EventBridgeClient.builder(), EventBridgeClient.SERVICE_NAME); + } + } +} diff --git a/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IamCredentials.java b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IamCredentials.java new file mode 100644 index 00000000..8c9a952a --- /dev/null +++ b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IamCredentials.java @@ -0,0 +1,67 @@ +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.exception.SdkServiceException; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; +import software.amazon.awssdk.services.sts.model.Credentials; + +public class IamCredentials implements BillingProviderCredentials { + + private static final Logger LOGGER = LoggerFactory.getLogger(IamCredentials.class); + private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); + private StsClient sts; + private String assumedRole; + + public IamCredentials(StsClient sts, String assumedRole) { + this.sts = sts; + this.assumedRole = assumedRole; + } + + private IamCredentials() { + } + + public String getAssumedRole() { + return assumedRole; + } + + @Override + public final CredentialType type() { + return CredentialType.IAM; + } + + @Override + public AwsCredentialsProvider resolveCredentials() { + StaticCredentialsProvider credentialsProvider; + try { + AssumeRoleResponse response = sts.assumeRole(request -> request + .roleArn(assumedRole) + .durationSeconds(900) + .roleSessionName("sb-" + SAAS_BOOST_ENV + "-billing-service") + ); + //AssumedRoleUser assumedUser = response.assumedRoleUser(); + //LOGGER.info("Assumed IAM User {}", assumedUser.arn()); + //LOGGER.info("Assumed IAM Role {}", assumedUser.assumedRoleId()); + + // Could use STSAssumeRoleSessionCredentialsProvider here, but this + // lambda will timeout before we need to refresh the temporary creds + Credentials temporaryCredentials = response.credentials(); + credentialsProvider = StaticCredentialsProvider.create( + AwsSessionCredentials.create( + temporaryCredentials.accessKeyId(), + temporaryCredentials.secretAccessKey(), + temporaryCredentials.sessionToken() + ) + ); + } catch (SdkServiceException stsError) { + LOGGER.error("sts::AssumeRole error {}", stsError.getMessage()); + LOGGER.error(Utils.getFullStackTrace(stsError)); + throw stsError; + } + return credentialsProvider; + } +} diff --git a/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/StripeApi.java b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/StripeApi.java new file mode 100644 index 00000000..5ed18b2f --- /dev/null +++ b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/StripeApi.java @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Collections; + +public class StripeApi implements BillingProviderApi { + + private static final Logger LOGGER = LoggerFactory.getLogger(StripeApi.class); + private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); + + @Override + public Collection getPlans() { + // Stripe Product & Pricing Model + return Collections.emptyList(); + } +} diff --git a/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/StripeBillingProvider.java b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/StripeBillingProvider.java new file mode 100644 index 00000000..1aeca874 --- /dev/null +++ b/services/billing-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/StripeBillingProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import java.util.Properties; + +public class StripeBillingProvider implements BillingProvider { + + static final Properties DEFAULTS = new Properties(); + + static { + DEFAULTS.put("secretKey", ""); + } + + private final Properties properties; + + public StripeBillingProvider(Properties properties) { + this.properties = new Properties(DEFAULTS); + this.properties.putAll(properties); + } + + @Override + public ProviderType type() { + return ProviderType.STRIPE; + } + + @Override + public Properties getProperties() { + return (Properties) properties.clone(); + } + + @Override + public BillingProviderApi api() { + return new StripeApi(); + } +} diff --git a/functions/ecs-shutdown-services/src/main/resources/lambda-assembly.xml b/services/billing-service/src/main/resources/lambda-assembly.xml similarity index 100% rename from functions/ecs-shutdown-services/src/main/resources/lambda-assembly.xml rename to services/billing-service/src/main/resources/lambda-assembly.xml diff --git a/functions/onboarding-app-stack-listener/src/main/resources/log4j2.xml b/services/billing-service/src/main/resources/log4j2.xml similarity index 100% rename from functions/onboarding-app-stack-listener/src/main/resources/log4j2.xml rename to services/billing-service/src/main/resources/log4j2.xml diff --git a/services/billing-service/src/main/resources/spotbugs-exclude.xml b/services/billing-service/src/main/resources/spotbugs-exclude.xml new file mode 100644 index 00000000..ed844d27 --- /dev/null +++ b/services/billing-service/src/main/resources/spotbugs-exclude.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/resources/custom-resources/attach-ecs-capacity-provider/update.sh b/services/billing-service/update.sh similarity index 83% rename from resources/custom-resources/attach-ecs-capacity-provider/update.sh rename to services/billing-service/update.sh index 5dc47e80..b95f3742 100755 --- a/resources/custom-resources/attach-ecs-capacity-provider/update.sh +++ b/services/billing-service/update.sh @@ -26,7 +26,7 @@ LAMBDA_STAGE_FOLDER=$2 if [ -z $LAMBDA_STAGE_FOLDER ]; then LAMBDA_STAGE_FOLDER="lambdas" fi -LAMBDA_CODE=AttachEcsCapacityProvider-lambda.zip +LAMBDA_CODE=TierService-lambda.zip #set this for V2 AWS CLI to disable paging export AWS_PAGER="" @@ -48,9 +48,10 @@ fi # And copy it up to S3 aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ -eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-attach-capacity-provider\`)] | [].FunctionName' --output text"\) +# Find all the functions for this microservice +eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-tier-\`)] | [].FunctionName' --output text"\) FUNCTIONS=($FUNCTIONS) for FX in "${FUNCTIONS[@]}"; do - printf "Updating function code for %s\n" $FX - aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "$FX" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE + printf "Updating function code for %s\n" $FX + aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "$FX" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE done diff --git a/functions/core-stack-listener/pom.xml b/services/identity-service/pom.xml similarity index 71% rename from functions/core-stack-listener/pom.xml rename to services/identity-service/pom.xml index 8921bcbb..c638ea9f 100644 --- a/functions/core-stack-listener/pom.xml +++ b/services/identity-service/pom.xml @@ -19,10 +19,10 @@ limitations under the License. 4.0.0 com.amazon.aws.partners.saasfactory.saasboost - saasboost-functions + saasboost-services 1.0.0 - CoreStackListener + IdentityService 1.0.0 jar @@ -33,6 +33,7 @@ limitations under the License. + ${project.basedir}/../.. 0 @@ -46,6 +47,16 @@ limitations under the License. org.apache.maven.plugins maven-surefire-plugin + + + us-east-1 + test + + + + + org.jacoco + jacoco-maven-plugin org.apache.maven.plugins @@ -55,27 +66,29 @@ limitations under the License. io.github.git-commit-id git-commit-id-maven-plugin + + com.github.spotbugs + spotbugs-maven-plugin + + Max + medium + + + software.amazon.lambda.snapstart + aws-lambda-snapstart-java-rules + 0.1.0 + + + ${project.basedir}/src/main/resources/spotbugs-exclude.xml + +
    - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - - - com.amazon.aws.partners.saasfactory.saasboost - CloudFormationUtils - 1.0.0 - - provided - software.amazon.awssdk - cloudformation + cognitoidentityprovider ${aws.java.sdk.version} @@ -90,7 +103,7 @@ limitations under the License. software.amazon.awssdk - eventbridge + sts ${aws.java.sdk.version} @@ -105,7 +118,7 @@ limitations under the License. software.amazon.awssdk - ecr + secretsmanager ${aws.java.sdk.version} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TimeRange.java b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AbstractIdentityProviderApi.java similarity index 62% rename from services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TimeRange.java rename to services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AbstractIdentityProviderApi.java index 3c0c5554..9f8a263e 100644 --- a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TimeRange.java +++ b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AbstractIdentityProviderApi.java @@ -16,28 +16,18 @@ package com.amazon.aws.partners.saasfactory.saasboost; -public enum TimeRange { - HOUR_24(24), - HOUR_12(12), - HOUR_10(10), - HOUR_8(8), - HOUR_4(4), - HOUR_2(2), - HOUR_1(1), - TODAY(0), - DAY_7(7), - THIS_WEEK(0), - THIS_MONTH(0), - DAY_30(30); +public abstract class AbstractIdentityProviderApi implements IdentityProviderApi { - private final int valueToSubtract; + private IdentityProviderCredentials credentials; - TimeRange(int valueToSubtract) { - this.valueToSubtract = valueToSubtract; + public AbstractIdentityProviderApi(IdentityProviderCredentials credentials) { + this.credentials = credentials; } - public int getValueToSubtract() { - return this.valueToSubtract; + private AbstractIdentityProviderApi() { } -} \ No newline at end of file + public IdentityProviderCredentials getCredentials() { + return credentials; + } +} diff --git a/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoApi.java b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoApi.java new file mode 100644 index 00000000..8bdac873 --- /dev/null +++ b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoApi.java @@ -0,0 +1,188 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; +import software.amazon.awssdk.services.cognitoidentityprovider.CognitoIdentityProviderClient; +import software.amazon.awssdk.services.cognitoidentityprovider.model.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class CognitoApi extends AbstractIdentityProviderApi { + + private static final Logger LOGGER = LoggerFactory.getLogger(CognitoApi.class); + private final CognitoIdentityProviderClient cognito; + private final String userPoolId; + + public CognitoApi(IamCredentials credentials, String userPoolId) { + this(credentials, new DefaultDependencyFactory(userPoolId)); + } + + public CognitoApi(IamCredentials credentials, CognitoApiDependencyFactory init) { + super(credentials); + this.cognito = init.cognito(); + this.userPoolId = init.userPoolId(); + } + + @Override + public List getGroups() { + IamCredentials iam = (IamCredentials) getCredentials(); + ListGroupsResponse response = cognito.listGroups(request -> request + .userPoolId(userPoolId) + .overrideConfiguration(AwsRequestOverrideConfiguration.builder() + .credentialsProvider(iam.resolveCredentials()).build() + ) + ); + return response.groups().stream() + .map(groupType -> Group.builder().name(groupType.groupName()).build()) + .collect(Collectors.toList()); + } + + @Override + public void addUserAttribute(UserAttribute attribute) { + IamCredentials iam = (IamCredentials) getCredentials(); + AwsCredentialsProvider credentials = iam.resolveCredentials(); + DescribeUserPoolResponse response = cognito.describeUserPool(request -> request + .userPoolId(userPoolId) + .overrideConfiguration(AwsRequestOverrideConfiguration.builder() + .credentialsProvider(credentials).build()) + ); + String customAttributeName = "custom:" + attribute.getName(); + boolean exists = false; + for (SchemaAttributeType schemaAttribute : response.userPool().schemaAttributes()) { + if (customAttributeName.equals(schemaAttribute.name())) { + exists = true; + break; + } + } + if (!exists) { + cognito.addCustomAttributes(request -> request + .userPoolId(userPoolId) + .customAttributes(List.of( + SchemaAttributeType.builder() + .attributeDataType(AttributeDataType.STRING) + .name(attribute.getName()) + .build() + ) + ) + .overrideConfiguration(AwsRequestOverrideConfiguration.builder() + .credentialsProvider(credentials).build() + ) + ); + } + } + + @Override + public User createUser(User user) { + // Cognito doesn't like NULL attribute types. It throws an error + // instead of ignoring them. + List userAttributes = new ArrayList<>(); + if (Utils.isNotBlank(user.getEmail())) { + userAttributes.add(AttributeType.builder().name("email").value(user.getEmail()).build()); + } + if (Utils.isNotBlank(user.getPhoneNumber())) { + userAttributes.add(AttributeType.builder().name("phone_number").value(user.getPhoneNumber()).build()); + } + if (Utils.isNotBlank(user.getGivenName())) { + userAttributes.add(AttributeType.builder().name("given_name").value(user.getGivenName()).build()); + } + if (Utils.isNotBlank(user.getFamilyName())) { + userAttributes.add(AttributeType.builder().name("family_name").value(user.getFamilyName()).build()); + } + if (Utils.isNotBlank(user.getTenantId())) { + userAttributes.add(AttributeType.builder().name("custom:tenant_id").value(user.getTenantId()).build()); + } + if (Utils.isNotBlank(user.getTier())) { + userAttributes.add(AttributeType.builder().name("custom:tier").value(user.getTier()).build()); + } + IamCredentials iam = (IamCredentials) getCredentials(); + AdminCreateUserResponse response = cognito.adminCreateUser(request -> request + .userPoolId(userPoolId) + .username(user.getUsername()) + .temporaryPassword(user.getPassword()) + .userAttributes(userAttributes) + .overrideConfiguration(AwsRequestOverrideConfiguration.builder() + .credentialsProvider(iam.resolveCredentials()).build() + ) + ); + UserType cognitoUser = response.user(); + Map attributes = cognitoUser.attributes().stream() + .collect(Collectors.toMap(a -> a.name(), a -> a.value())); + return User.builder() + .id(attributes.getOrDefault("sub", null)) + .username(cognitoUser.username()) + .email(attributes.getOrDefault("email", null)) + .phoneNumber(attributes.getOrDefault("phone_number", null)) + .givenName(attributes.getOrDefault("given_name", null)) + .familyName(attributes.getOrDefault("family_name", null)) + .tenantId(attributes.getOrDefault("custom:tenant_id", null)) + .tier(attributes.getOrDefault("custom:tier", null)) + .build(); + + } + + @Override + public void addUserToGroup(User user, Group group) { + IamCredentials iam = (IamCredentials) getCredentials(); + cognito.adminAddUserToGroup(request -> request + .userPoolId(userPoolId) + .username(user.getUsername()) + .groupName(group.getName()) + .overrideConfiguration(AwsRequestOverrideConfiguration.builder() + .credentialsProvider(iam.resolveCredentials()).build() + ) + ); + } + + @Override + public List getUsers(String tenantId) { + return Collections.emptyList(); + } + + interface CognitoApiDependencyFactory { + + String userPoolId(); + + CognitoIdentityProviderClient cognito(); + } + + private static final class DefaultDependencyFactory implements CognitoApiDependencyFactory { + + private String userPoolId; + + public DefaultDependencyFactory(String userPoolId) { + this.userPoolId = userPoolId; + } + + @Override + public String userPoolId() { + return userPoolId; + } + + @Override + public CognitoIdentityProviderClient cognito() { + return Utils.sdkClient(CognitoIdentityProviderClient.builder(), CognitoIdentityProviderClient.SERVICE_NAME); + } + } +} diff --git a/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoIdentityProvider.java b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoIdentityProvider.java new file mode 100644 index 00000000..bcc71afb --- /dev/null +++ b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoIdentityProvider.java @@ -0,0 +1,72 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import software.amazon.awssdk.services.sts.StsClient; + +import java.util.Properties; + +public class CognitoIdentityProvider implements IdentityProvider { + + static final Properties DEFAULTS = new Properties(); + + static { + DEFAULTS.put("userPoolId", ""); + DEFAULTS.put("assumedRole", ""); + } + + private final Properties metadata; + private final StsClient sts; + + public CognitoIdentityProvider(Properties metadata) { + this(metadata, new DefaultDependencyFactory()); + } + + public CognitoIdentityProvider(Properties metadata, CognitoIdentityProviderDependencyFactory init) { + this.metadata = new Properties(DEFAULTS); + this.metadata.putAll(metadata); + this.sts = init.sts(); + } + + @Override + public ProviderType type() { + return ProviderType.COGNITO; + } + + public Properties getMetadata() { + return (Properties) metadata.clone(); + } + + public IdentityProviderApi getApi() { + IamCredentials credentials = new IamCredentials(sts, metadata.getProperty("assumedRole")); + CognitoApi api = new CognitoApi(credentials, metadata.getProperty("userPoolId")); + return api; + } + + interface CognitoIdentityProviderDependencyFactory { + + StsClient sts(); + } + + private static final class DefaultDependencyFactory implements CognitoIdentityProviderDependencyFactory { + + @Override + public StsClient sts() { + return Utils.sdkClient(StsClient.builder(), StsClient.SERVICE_NAME); + } + } +} diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/BillingProvider.java b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Group.java similarity index 64% rename from services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/BillingProvider.java rename to services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Group.java index dca8ecb2..68206cbb 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/BillingProvider.java +++ b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Group.java @@ -14,28 +14,29 @@ * limitations under the License. */ -package com.amazon.aws.partners.saasfactory.saasboost.appconfig; +package com.amazon.aws.partners.saasfactory.saasboost; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import java.util.Objects; -@JsonDeserialize(builder = BillingProvider.Builder.class) -public class BillingProvider { +@JsonDeserialize(builder = Group.Builder.class) +public class Group { - private String apiKey; + private final String name; - private BillingProvider(Builder builder) { - this.apiKey = builder.apiKey; + private Group(Builder builder) { + this.name = builder.name; } - public String getApiKey() { - return apiKey; + public String getName() { + return name; } - public boolean hasApiKey() { - return getApiKey() != null && !getApiKey().isEmpty(); + @Override + public String toString() { + return name; } @Override @@ -51,13 +52,13 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) { return false; } - final BillingProvider other = (BillingProvider) obj; - return ((apiKey == null && other.apiKey == null) || (apiKey != null && apiKey.equals(other.apiKey))); + final Group other = (Group) obj; + return Objects.equals(name, other.name); } @Override public int hashCode() { - return Objects.hash(apiKey); + return Objects.hash(name); } public static Builder builder() { @@ -67,18 +68,21 @@ public static Builder builder() { @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property] public static final class Builder { - private String apiKey; + private String name; private Builder() { } - public Builder apiKey(String apiKey) { - this.apiKey = apiKey; + public Builder name(String name) { + this.name = name; return this; } - public BillingProvider build() { - return new BillingProvider(this); + public Group build() { + if (name == null || name.isBlank()) { + throw new IllegalStateException("Can't build Group without name"); + } + return new Group(this); } } } diff --git a/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IamCredentials.java b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IamCredentials.java new file mode 100644 index 00000000..f4edc9c9 --- /dev/null +++ b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IamCredentials.java @@ -0,0 +1,83 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.exception.SdkServiceException; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; +import software.amazon.awssdk.services.sts.model.Credentials; + +public class IamCredentials implements IdentityProviderCredentials { + + private static final Logger LOGGER = LoggerFactory.getLogger(IamCredentials.class); + private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); + private StsClient sts; + private String assumedRole; + + public IamCredentials(StsClient sts, String assumedRole) { + this.sts = sts; + this.assumedRole = assumedRole; + } + + private IamCredentials() { + } + + public String getAssumedRole() { + return assumedRole; + } + + @Override + public final CredentialType type() { + return CredentialType.IAM; + } + + @Override + public AwsCredentialsProvider resolveCredentials() { + StaticCredentialsProvider credentialsProvider; + try { + AssumeRoleResponse response = sts.assumeRole(request -> request + .roleArn(assumedRole) + .durationSeconds(900) + .roleSessionName("sb-" + SAAS_BOOST_ENV + "-identity-service") + ); + //AssumedRoleUser assumedUser = response.assumedRoleUser(); + //LOGGER.info("Assumed IAM User {}", assumedUser.arn()); + //LOGGER.info("Assumed IAM Role {}", assumedUser.assumedRoleId()); + + // Could use STSAssumeRoleSessionCredentialsProvider here, but this + // lambda will timeout before we need to refresh the temporary creds + Credentials temporaryCredentials = response.credentials(); + credentialsProvider = StaticCredentialsProvider.create( + AwsSessionCredentials.create( + temporaryCredentials.accessKeyId(), + temporaryCredentials.secretAccessKey(), + temporaryCredentials.sessionToken() + ) + ); + } catch (SdkServiceException stsError) { + LOGGER.error("sts::AssumeRole error {}", stsError.getMessage()); + LOGGER.error(Utils.getFullStackTrace(stsError)); + throw stsError; + } + return credentialsProvider; + } +} diff --git a/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityDataAccessLayer.java b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityDataAccessLayer.java new file mode 100644 index 00000000..4c9bf262 --- /dev/null +++ b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityDataAccessLayer.java @@ -0,0 +1,61 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; + +import java.util.ArrayList; +import java.util.List; + +public class IdentityDataAccessLayer { + + private final String providerConfigSecretId; + private final SecretsManagerClient secrets; + + public IdentityDataAccessLayer(SecretsManagerClient secrets, String providerConfigSecretId) { + this.secrets = secrets; + this.providerConfigSecretId = providerConfigSecretId; + } + + public List getAvailableProviders() { + List providers = new ArrayList<>(); + IdentityProviderConfig activeProvider = getProviderConfig(); + for (IdentityProvider.ProviderType type : IdentityProvider.ProviderType.values()) { + if (activeProvider != null && type == activeProvider.getType()) { + providers.add(activeProvider); + } else { + providers.add(new IdentityProviderConfig(type)); + } + } + return providers; + } + + public IdentityProviderConfig getProviderConfig() { + GetSecretValueResponse response = secrets.getSecretValue(request -> request + .secretId(providerConfigSecretId) + ); + return Utils.fromJson(response.secretString(), IdentityProviderConfig.class); + } + + public void setProviderConfig(IdentityProviderConfig providerConfig) { + secrets.putSecretValue(request -> request + .secretId(providerConfigSecretId) + .secretString(Utils.toJson(providerConfig)) + ); + } +} diff --git a/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProvider.java b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProvider.java new file mode 100644 index 00000000..8cc18d14 --- /dev/null +++ b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import java.util.Properties; + +public interface IdentityProvider { + + enum ProviderType { + COGNITO, + KEYCLOAK, + AUTH0 + } + + ProviderType type(); + + Properties getMetadata(); + + IdentityProviderApi getApi(); + +} diff --git a/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProviderApi.java b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProviderApi.java new file mode 100644 index 00000000..9c1537e7 --- /dev/null +++ b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProviderApi.java @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import java.util.List; + +public interface IdentityProviderApi { + + List getGroups(); + + void addUserAttribute(UserAttribute attribute); + + User createUser(User user); + + void addUserToGroup(User user, Group group); + + List getUsers(String tenantId); +} diff --git a/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProviderConfig.java b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProviderConfig.java new file mode 100644 index 00000000..06f3c248 --- /dev/null +++ b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProviderConfig.java @@ -0,0 +1,56 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; +import java.util.Properties; + +public class IdentityProviderConfig { + + private final IdentityProvider.ProviderType type; + private final Properties metadata; + + @JsonCreator + public IdentityProviderConfig(@JsonProperty("type") IdentityProvider.ProviderType type) { + this.type = type; + this.metadata = new Properties(); + switch (this.type) { + case COGNITO: + this.metadata.putAll(CognitoIdentityProvider.DEFAULTS); + break; + default: + break; + } + } + + public IdentityProvider.ProviderType getType() { + return this.type; + } + + public Properties getMetadata() { + return (Properties) metadata.clone(); + } + + public void setMetadata(Map properties) { + if (properties != null) { + this.metadata.putAll(properties); + } + } +} diff --git a/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProviderCredentials.java b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProviderCredentials.java new file mode 100644 index 00000000..64c873ec --- /dev/null +++ b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProviderCredentials.java @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +public interface IdentityProviderCredentials { + + enum CredentialType { + IAM, + JWT + } + + CredentialType type(); + + Object resolveCredentials(); +} diff --git a/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProviderFactory.java b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProviderFactory.java new file mode 100644 index 00000000..56da227a --- /dev/null +++ b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProviderFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class IdentityProviderFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(IdentityProviderFactory.class); + + private IdentityProviderFactory() { + } + + public static IdentityProviderFactory getInstance() { + return IdentityProviderFactoryInstance.instance; + } + + public IdentityProvider getProvider(IdentityProviderConfig providerConfig) { + switch (providerConfig.getType()) { + case COGNITO: + return new CognitoIdentityProvider(providerConfig.getMetadata()); + default: + return null; + } + } + + private static class IdentityProviderFactoryInstance { + public static final IdentityProviderFactory instance = new IdentityProviderFactory(); + } +} diff --git a/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityService.java b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityService.java new file mode 100644 index 00000000..57327149 --- /dev/null +++ b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityService.java @@ -0,0 +1,213 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; + +import java.net.HttpURLConnection; +import java.util.List; +import java.util.Map; + +public class IdentityService { + + private static final Logger LOGGER = LoggerFactory.getLogger(IdentityService.class); + private static final Map CORS = Map.of("Access-Control-Allow-Origin", "*"); + private static final String AWS_REGION = System.getenv("AWS_REGION"); + private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); + private static final String IDENTITY_PROVIDER_CONFIG = System.getenv("IDENTITY_PROVIDER_CONFIG"); + private final IdentityDataAccessLayer dal; + + public IdentityService() { + this(new DefaultDependencyFactory()); + } + + public IdentityService(IdentityServiceDependencyFactory init) { + if (Utils.isBlank(AWS_REGION)) { + throw new IllegalStateException("Missing required environment variable AWS_REGION"); + } + if (Utils.isBlank(SAAS_BOOST_ENV)) { + throw new IllegalStateException("Missing environment variable SAAS_BOOST_ENV"); + } + LOGGER.info("Version Info: {}", Utils.version(this.getClass())); + this.dal = init.dal(); + } + + /** + * Get available identity providers as config templates. Integration for GET /identity/providers endpoint + * @param event API Gateway proxy request event + * @param context Lambda function context + * @return List of onboarding objects + */ + public APIGatewayProxyResponseEvent getProviders(APIGatewayProxyRequestEvent event, Context context) { + if (Utils.warmup(event)) { + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); + } + + //Utils.logRequestEvent(event); + APIGatewayProxyResponseEvent response; + List providers = dal.getAvailableProviders(); + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_OK) + .withBody(Utils.toJson(providers)); + + return response; + } + + /** + * Returns the active Identity Provider configuration. Integration for GET /identity endpoint. + * @param event API Gateway proxy request event + * @param context Lambda function context + * @return + */ + public APIGatewayProxyResponseEvent getProvider(APIGatewayProxyRequestEvent event, Context context) { + if (Utils.warmup(event)) { + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); + } + + //Utils.logRequestEvent(event); + APIGatewayProxyResponseEvent response; + IdentityProviderConfig providerConfig = dal.getProviderConfig(); + if (providerConfig != null) { + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_OK) + .withBody(Utils.toJson(providerConfig)); + } else { + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_NOT_FOUND); + } + + return response; + } + + /** + * Sets the active Identity Provider configuration. Integration for POST /identity endpoint. + * @param event API Gateway proxy request event + * @param context Lambda function context + * @return + */ + public APIGatewayProxyResponseEvent setProvider(APIGatewayProxyRequestEvent event, Context context) { + if (Utils.warmup(event)) { + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); + } + + //Utils.logRequestEvent(event); + IdentityProviderConfig providerConfig = Utils.fromJson(event.getBody(), IdentityProviderConfig.class); + APIGatewayProxyResponseEvent response; + if (providerConfig == null) { + response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) + .withHeaders(CORS) + .withBody(Utils.toJson(Map.of("message", "Invalid request body"))); + } else { + dal.setProviderConfig(providerConfig); + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_CREATED); + } + return response; + } + + /** + * Event listener (EventBridge Rule target) for Events. The single public + * entry point both reduces the number of EventBridge rules and simplifies debug logging. + * @param event the EventBridge event + */ + public void handleEvent(Map event) { + Utils.logRequestEvent(event); + String detailType = (String) event.get("detail-type"); + if (detailType.startsWith("Onboarding ")) { + // Onboarding events that trigger identity system workflows + // Use this entry point for consolidated logging of the identity service + LOGGER.info("Handling Onboarding Event"); + handleOnboardingEvent(event); + } else { + LOGGER.error("Unknown event for detail-type {}", event.get("detail-type")); + } + } + + protected void handleOnboardingEvent(Map event) { + String detailType = (String) event.get("detail-type"); + if ("Onboarding Tenant Assigned".equals(detailType)) { + LOGGER.info("Handling Onboarding Tenant Assigned Event"); + handleOnboardingTenantAssignedEvent(event); + } + } + + protected void handleOnboardingTenantAssignedEvent(Map event) { + IdentityProviderConfig providerConfig = dal.getProviderConfig(); + if (providerConfig != null) { + IdentityProvider provider = IdentityProviderFactory.getInstance().getProvider(providerConfig); + try { + // First make sure our custom user attributes are set + provider.getApi().addUserAttribute(UserAttribute.builder().name("tenant_id").build()); + provider.getApi().addUserAttribute(UserAttribute.builder().name("tier").build()); + + Map detail = (Map) event.get("detail"); + Map tenant = (Map) detail.get("tenant"); + String tenantId = (String) tenant.getOrDefault("id", ""); + String tier = (String) tenant.getOrDefault("tier", ""); + List> tenantUsers = (List>) tenant.get("adminUsers"); + for (Map tenantUser : tenantUsers) { + User adminUser = provider.getApi().createUser(User.builder() + .tenantId(tenantId) + .tier(tier) + .username((String) tenantUser.get("username")) + .email((String) tenantUser.getOrDefault("email", null)) + .phoneNumber((String) tenantUser.getOrDefault("phoneNumber", null)) + .givenName((String) tenantUser.getOrDefault("givenName", null)) + .familyName((String) tenantUser.getOrDefault("familyName", null)) + .password(Utils.generatePassword(12)) + .build() + ); + LOGGER.info("Tenant Admin User Created"); + LOGGER.info(Utils.toJson(adminUser)); + } + } catch (Exception e) { + LOGGER.error(Utils.getFullStackTrace(e)); + throw new IllegalArgumentException("Can't process tenant assigned event"); + } + } else { + LOGGER.warn("No Identity Provider has been set"); + } + } + + interface IdentityServiceDependencyFactory { + + IdentityDataAccessLayer dal(); + } + + private static final class DefaultDependencyFactory implements IdentityServiceDependencyFactory { + + @Override + public IdentityDataAccessLayer dal() { + return new IdentityDataAccessLayer(Utils.sdkClient(SecretsManagerClient.builder(), + SecretsManagerClient.SERVICE_NAME), IDENTITY_PROVIDER_CONFIG); + } + + } +} \ No newline at end of file diff --git a/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/JwtCredentials.java b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/JwtCredentials.java new file mode 100644 index 00000000..0b8e5f36 --- /dev/null +++ b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/JwtCredentials.java @@ -0,0 +1,30 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +public class JwtCredentials implements IdentityProviderCredentials { + + @Override + public final CredentialType type() { + return CredentialType.JWT; + } + + @Override + public String resolveCredentials() { + return null; + } +} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricValue.java b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Role.java similarity index 51% rename from services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricValue.java rename to services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Role.java index 3ef2cc2c..5526553c 100644 --- a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricValue.java +++ b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Role.java @@ -16,37 +16,27 @@ package com.amazon.aws.partners.saasfactory.saasboost; -import java.util.Objects; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; -public class MetricValue implements Comparable { +import java.util.Objects; - private double value; - private String id; +@JsonDeserialize(builder = Role.Builder.class) +public class Role { - public MetricValue(double value, String id) { - this.value = value; - this.id = id; - } - - public double getValue() { - return value; - } + private final String name; - public String getId() { - return id; + private Role(Builder builder) { + this.name = builder.name; } - public int compareTo(MetricValue o) { - return Double.compare(value, o.value); - } - - public void setValue(double value) { - this.value = value; + public String getName() { + return name; } @Override public String toString() { - return Utils.toJson(this); + return name; } @Override @@ -62,12 +52,37 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) { return false; } - final MetricValue other = (MetricValue) obj; - return (Utils.nullableEquals(id, other.id) && (value == other.value)); + final Role other = (Role) obj; + return Objects.equals(name, other.name); } @Override public int hashCode() { - return Objects.hash(id, value); + return Objects.hash(name); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property] + public static final class Builder { + + private String name; + + private Builder() { + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Role build() { + if (name == null || name.isBlank()) { + throw new IllegalStateException("Can't build Role without name"); + } + return new Role(this); + } } } diff --git a/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/User.java b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/User.java new file mode 100644 index 00000000..267c670e --- /dev/null +++ b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/User.java @@ -0,0 +1,190 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@JsonDeserialize(builder = User.Builder.class) +public class User { + + private final String id; + private final String username; + private final String email; + private final String phoneNumber; + private final String password; + private final String givenName; + private final String familyName; + private final List groups; + private final List roles; + private final String tenantId; + private final String tier; + + private User(Builder builder) { + this.id = builder.id; + this.username = builder.username; + this.email = builder.email; + this.phoneNumber = builder.phoneNumber; + this.password = builder.password; + this.givenName = builder.givenName; + this.familyName = builder.familyName; + this.groups = builder.groups; + this.roles = builder.roles; + this.tenantId = builder.tenantId; + this.tier = builder.tier; + } + + public String getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getEmail() { + return email; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public String getPassword() { + return password; + } + + public String getGivenName() { + return givenName; + } + + public String getFamilyName() { + return familyName; + } + + public List getGroups() { + return List.copyOf(groups); + } + + public List getRoles() { + return List.copyOf(roles); + } + + public String getTenantId() { + return tenantId; + } + + public String getTier() { + return tier; + } + + public static Builder builder() { + return new Builder(); + } + + @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property] + public static final class Builder { + + private String id; + private String username; + private String email; + private String phoneNumber; + private String password; + private String givenName; + private String familyName; + private List groups = new ArrayList<>(); + private List roles = new ArrayList<>(); + private String tenantId; + private String tier; + + private Builder() { + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder username(String username) { + this.username = username; + return this; + } + + public Builder email(String email) { + this.email = email; + return this; + } + + public Builder phoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + return this; + } + + public Builder password(String password) { + this.password = password; + return this; + } + + public Builder givenName(String givenName) { + this.givenName = givenName; + return this; + } + + public Builder familyName(String familyName) { + this.familyName = familyName; + return this; + } + + public Builder groups(Collection groups) { + if (groups != null) { + this.groups.addAll(groups); + } + return this; + } + + public Builder roles(Collection roles) { + if (roles != null) { + this.roles.addAll(roles); + } + return this; + } + + public Builder tenantId(String tenantId) { + this.tenantId = tenantId; + return this; + } + + public Builder tier(String tier) { + this.tier = tier; + return this; + } + + public User build() { + if (username == null || username.isBlank()) { + throw new IllegalStateException("Can't build User without username"); + } + if (tenantId == null || tenantId.isBlank()) { + throw new IllegalStateException("Can't build User without tenant id"); + } + return new User(this); + } + } +} diff --git a/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/UserAttribute.java b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/UserAttribute.java new file mode 100644 index 00000000..3cc8cf71 --- /dev/null +++ b/services/identity-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/UserAttribute.java @@ -0,0 +1,88 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; + +import java.util.Objects; + +@JsonDeserialize(builder = UserAttribute.Builder.class) +public class UserAttribute { + + private final String name; + + private UserAttribute(Builder builder) { + this.name = builder.name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return name; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + // Same reference? + if (this == obj) { + return true; + } + // Same type? + if (getClass() != obj.getClass()) { + return false; + } + final UserAttribute other = (UserAttribute) obj; + return Objects.equals(name, other.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property] + public static final class Builder { + + private String name; + + private Builder() { + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public UserAttribute build() { + if (name == null || name.isBlank()) { + throw new IllegalStateException("Can't build UserAttribute without name"); + } + return new UserAttribute(this); + } + } +} diff --git a/functions/ecs-startup-services/src/main/resources/lambda-assembly.xml b/services/identity-service/src/main/resources/lambda-assembly.xml similarity index 100% rename from functions/ecs-startup-services/src/main/resources/lambda-assembly.xml rename to services/identity-service/src/main/resources/lambda-assembly.xml diff --git a/functions/onboarding-stack-listener/src/main/resources/log4j2.xml b/services/identity-service/src/main/resources/log4j2.xml similarity index 100% rename from functions/onboarding-stack-listener/src/main/resources/log4j2.xml rename to services/identity-service/src/main/resources/log4j2.xml diff --git a/services/identity-service/src/main/resources/spotbugs-exclude.xml b/services/identity-service/src/main/resources/spotbugs-exclude.xml new file mode 100644 index 00000000..f630bb3c --- /dev/null +++ b/services/identity-service/src/main/resources/spotbugs-exclude.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + diff --git a/services/identity-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProviderConfigTest.java b/services/identity-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProviderConfigTest.java new file mode 100644 index 00000000..0825f9c9 --- /dev/null +++ b/services/identity-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/IdentityProviderConfigTest.java @@ -0,0 +1,22 @@ +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.junit.jupiter.api.Test; + +public class IdentityProviderConfigTest { + + @Test + public void testToJson() { + IdentityProviderConfig config = new IdentityProviderConfig(IdentityProvider.ProviderType.COGNITO); + //System.out.println(config.getMetadata().stringPropertyNames()); + System.out.println(Utils.toJson(config)); + + String json = "{\"type\": \"COGNITO\", \"metadata\": {\"assumedRole\": \"arn:aws:iam::1234:role/foo\", \"userPoolId\": \"foobar\"}}"; + IdentityProviderConfig fromJson = Utils.fromJson(json, IdentityProviderConfig.class); + System.out.println(Utils.toJson(fromJson)); + + String emptyJson = "{}"; + IdentityProviderConfig fromEmptyJson = Utils.fromJson(emptyJson, IdentityProviderConfig.class); + System.out.println(Utils.toJson(fromEmptyJson)); + + } +} \ No newline at end of file diff --git a/resources/custom-resources/fsx-dns-name/update.sh b/services/identity-service/update.sh similarity index 81% rename from resources/custom-resources/fsx-dns-name/update.sh rename to services/identity-service/update.sh index b944d37d..49ebee5c 100755 --- a/resources/custom-resources/fsx-dns-name/update.sh +++ b/services/identity-service/update.sh @@ -26,7 +26,7 @@ LAMBDA_STAGE_FOLDER=$2 if [ -z $LAMBDA_STAGE_FOLDER ]; then LAMBDA_STAGE_FOLDER="lambdas" fi -LAMBDA_CODE=FsxDnsName-lambda.zip +LAMBDA_CODE=IdentityService-lambda.zip #set this for V2 AWS CLI to disable paging export AWS_PAGER="" @@ -49,10 +49,11 @@ fi aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ # Find all the functions for this microservice -# We must list in the fsx-dns-name case since functions are created with a tenant ID suffix -eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-fsx-dns-\`)] | [].FunctionName' --output text"\) +eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-identity-\`)] | [].FunctionName' --output text"\) FUNCTIONS=($FUNCTIONS) for FX in "${FUNCTIONS[@]}"; do - printf "Updating function code for %s\n" $FX - aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "$FX" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE -done \ No newline at end of file + if [[ ! $FX == *"listener"* ]]; then + printf "Updating function code for %s\n" $FX + aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "$FX" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE + fi +done diff --git a/services/metrics-service/pom.xml b/services/metrics-service/pom.xml index d16fc8d8..681fc743 100644 --- a/services/metrics-service/pom.xml +++ b/services/metrics-service/pom.xml @@ -33,7 +33,8 @@ limitations under the License. - 165 + ${project.basedir}/../.. + 0 @@ -46,6 +47,16 @@ limitations under the License. org.apache.maven.plugins maven-surefire-plugin + + + us-east-1 + test + + + + + org.jacoco + jacoco-maven-plugin org.apache.maven.plugins @@ -55,61 +66,34 @@ limitations under the License. io.github.git-commit-id git-commit-id-maven-plugin + + com.github.spotbugs + spotbugs-maven-plugin + + Max + medium + + + software.amazon.lambda.snapstart + aws-lambda-snapstart-java-rules + 0.1.0 + + + ${project.basedir}/src/main/resources/spotbugs-exclude.xml + + - junit - junit - - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - - - com.amazon.aws.partners.saasfactory.saasboost - ApiGatewayHelper - 1.0.0 - - provided - - - software.amazon.awssdk - cloudwatch - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - software.amazon.awssdk - applicationautoscaling - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - + software.amazon.cloudwatchlogs + aws-embedded-metrics + 4.1.1 software.amazon.awssdk - athena + sqs ${aws.java.sdk.version} @@ -123,19 +107,9 @@ limitations under the License. - software.amazon.awssdk - s3 - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - + com.fasterxml.jackson.core + jackson-databind + 2.14.2 diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CloudWatchApi.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CloudWatchApi.java new file mode 100644 index 00000000..408b9511 --- /dev/null +++ b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CloudWatchApi.java @@ -0,0 +1,97 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.cloudwatchlogs.emf.exception.InvalidTimestampException; +import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; +import software.amazon.cloudwatchlogs.emf.model.DimensionSet; +import software.amazon.cloudwatchlogs.emf.model.Unit; + +import java.util.Collection; +import java.util.Objects; + +public class CloudWatchApi implements MetricsProviderApi { + + private static final Logger LOGGER = LoggerFactory.getLogger(CloudWatchApi.class); + private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); + private final MetricsLogger emf; + private final String logGroupName; + private final String logStreamName; + + public CloudWatchApi() { + this(null, null); + } + + public CloudWatchApi(String logGroupName, String logStreamName) { + this(logGroupName, logStreamName, new DefaultDependencyFactory()); + } + + public CloudWatchApi(String logGroupName, String logStreamName, CloudWatchApiDependencyFactory init) { + this.emf = init.emf(); + this.logGroupName = logGroupName; + this.logStreamName = logStreamName; + } + + @Override + public void putMetrics(Collection metrics) { + LOGGER.info("Generating EMF data for {} metrics", metrics.size()); + try { + emf.setNamespace("sb-" + SAAS_BOOST_ENV); + //emf.setFlushPreserveDimensions(false); + for (Metric metric : metrics) { + try { + emf.setTimestamp(metric.getTimestamp()); + } catch (InvalidTimestampException ite) { + LOGGER.error("Invalid timestamp", ite); + LOGGER.error(Utils.toJson(metric)); + continue; + } + emf.putProperty("TenantId", metric.getContext().getTenantId()); + emf.putProperty("UserId", metric.getContext().getUserId()); + emf.putProperty("RequestId", metric.getContext().getRequestId()); + emf.putProperty("Application", metric.getContext().getApplication()); + emf.putProperty("Action", metric.getContext().getAction()); + if (metric.getMeasure() != null) { + emf.setDimensions(DimensionSet.of( + metric.getName() + " By Tenant", metric.getContext().getTenantId() + )); + Unit unit = Unit.fromValue(Objects.toString(metric.getMeasure().getType(), "None")); + emf.putMetric(metric.getName(), metric.getMeasure().getValue().doubleValue(), unit); + } + emf.flush(); + //emf.resetDimensions(false); + } + } catch (Exception e) { + LOGGER.error("CloudWatch EMF error", e); + } + } + + interface CloudWatchApiDependencyFactory { + + MetricsLogger emf(); + } + + private static final class DefaultDependencyFactory implements CloudWatchApiDependencyFactory { + + @Override + public MetricsLogger emf() { + return new MetricsLogger(); + } + } +} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CloudWatchMetricsProvider.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CloudWatchMetricsProvider.java new file mode 100644 index 00000000..aee6384b --- /dev/null +++ b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CloudWatchMetricsProvider.java @@ -0,0 +1,51 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import java.util.Properties; + +public class CloudWatchMetricsProvider implements MetricsProvider { + + static final Properties DEFAULTS = new Properties(); + + static { + //DEFAULTS.put("logGroupName", ""); + //DEFAULTS.put("logStreamName", ""); + } + + private final Properties properties; + + public CloudWatchMetricsProvider(Properties properties) { + this.properties = new Properties(DEFAULTS); + this.properties.putAll(properties); + } + + @Override + public ProviderType type() { + return ProviderType.CLOUDWATCH; + } + + @Override + public Properties getProperties() { + return (Properties) properties.clone(); + } + + @Override + public MetricsProviderApi api() { + return new CloudWatchApi(); + } +} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Measure.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Measure.java new file mode 100644 index 00000000..3e0711e6 --- /dev/null +++ b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Measure.java @@ -0,0 +1,93 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; + +@JsonDeserialize(builder = Measure.Builder.class) +public class Measure { + + enum Type { + count, + total, + min, + max + } + + private final Type type; + private final Number value; + private final String unit; + + private Measure(Builder builder) { + this.type = builder.type; + this.value = builder.value; + this.unit = builder.unit; + } + + public Type getType() { + return type; + } + + public Number getValue() { + return value; + } + + public String getUnit() { + return unit; + } + + @JsonIgnore + public boolean isEmpty() { + return type == null || value == null; + } + + public static Builder builder() { + return new Builder(); + } + + @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property] + public static final class Builder { + + private Type type; + private Number value; + private String unit; + + private Builder() { + } + + public Builder type(Type type) { + this.type = type; + return this; + } + + public Builder value(Number value) { + this.value = value; + return this; + } + + public Builder unit(String unit) { + this.unit = unit; + return this; + } + + public Measure build() { + return new Measure(this); + } + } +} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Metric.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Metric.java index 78ff739c..0464b28d 100644 --- a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Metric.java +++ b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Metric.java @@ -16,79 +16,111 @@ package com.amazon.aws.partners.saasfactory.saasboost; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; + import java.time.Instant; -import java.util.*; +import java.util.Map; +import java.util.Objects; +@JsonDeserialize(builder = Metric.Builder.class) public class Metric { - private String stat; - private String nameSpace; - private SortedMap> timeValMap = new TreeMap<>(); - private String metricName; - private double period; - private List metricValues = new ArrayList<>(); - private List metricTimes = new ArrayList<>(); - - public void addMetricValue(Double val) { - this.metricValues.add(val); - } - - public List getMetricValues() { - return List.copyOf(this.metricValues); - } - - public List getMetricTimes() { - return List.copyOf(metricTimes); - } - - public void addSortTime(Instant sortTime) { - this.metricTimes.add(sortTime); - } + private final String name; + private final Instant timestamp; + private final Measure measure; + private final MetricContext context; - public double getPeriod() { - return period; + private Metric(Builder builder) { + this.name = builder.name; + this.timestamp = builder.timestamp; + this.measure = builder.measure; + this.context = builder.context; } - public void setPeriod(double period) { - this.period = period; + public String getName() { + return name; } - public String getStat() { - return stat; + public Instant getTimestamp() { + return timestamp; } - public void setStat(String stat) { - this.stat = stat; + public Measure getMeasure() { + return measure; } - public String getNameSpace() { - return nameSpace; + public MetricContext getContext() { + return (MetricContext) context.clone(); } - public void setNameSpace(String nameSpace) { - this.nameSpace = nameSpace; - } - - public void setMetricName(String metricName) { - this.metricName = metricName; - } - - public String getMetricName() { - return metricName; + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + // Same reference? + if (this == obj) { + return true; + } + // Same type? + if (getClass() != obj.getClass()) { + return false; + } + final Metric other = (Metric) obj; + return (Objects.equals(name, other.name) + && Objects.equals(timestamp, other.timestamp) + && Objects.equals(measure, other.measure) + && Objects.equals(context, other.context)); } - public void addQueueValue(Instant time, MetricValue mv) { - PriorityQueue pq = timeValMap.computeIfAbsent(time, k -> new PriorityQueue<>()); - pq.add(mv); + @Override + public int hashCode() { + return Objects.hash(name, timestamp, measure, context); } - public SortedMap> getTimeValMap() { - return new TreeMap<>(timeValMap); + public static Builder builder() { + return new Builder(); } - @Override - public String toString() { - return Utils.toJson(this); + @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property] + public static final class Builder { + + private String name; + private Instant timestamp = Instant.now(); + private Measure measure; + private MetricContext context = new MetricContext(); + + private Builder() { + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder timestamp(Instant timestamp) { + this.timestamp = timestamp; + return this; + } + + public Builder measure(Measure measure) { + this.measure = measure; + return this; + } + + public Builder context(Map context) { + if (context != null) { + this.context.putAll(context); + } + return this; + } + + public Metric build() { + if (name == null || name.isBlank()) { + throw new IllegalStateException("Can't build Metric without name"); + } + return new Metric(this); + } } - } diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricContext.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricContext.java new file mode 100644 index 00000000..593b9852 --- /dev/null +++ b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricContext.java @@ -0,0 +1,111 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +public class MetricContext extends HashMap { + + private static final Logger LOGGER = LoggerFactory.getLogger(MetricContext.class); + private static final String TENANT_ID = "TenantId"; + private static final String USER_ID = "UserId"; + private static final String REQUEST_ID = "RequestId"; + private static final String ACTION = "Action"; + private static final String APPLICATION = "Application"; + private static final Map DEFAULTS = new HashMap<>(); + + static { + DEFAULTS.put(TENANT_ID, ""); + DEFAULTS.put(USER_ID, ""); + DEFAULTS.put(REQUEST_ID, ""); + DEFAULTS.put(ACTION, ""); + DEFAULTS.put(APPLICATION, ""); + } + + public MetricContext() { + super(DEFAULTS); + } + + public String getTenantId() { + return get(TENANT_ID); + } + + public void setTenantId(String tenantId) { + put(TENANT_ID, tenantId); + System.out.println("Default tenantId = " + DEFAULTS.get(TENANT_ID)); + } + + public String getUserId() { + return get(USER_ID); + } + + public void setUserId(String userId) { + put(USER_ID, userId); + } + + public String getRequestId() { + return get(REQUEST_ID); + } + + public void setRequestId(String requestId) { + put(REQUEST_ID, requestId); + } + + public String getAction() { + return get(ACTION); + } + + public void setAction(String action) { + put(ACTION, action); + } + + public String getApplication() { + return get(APPLICATION); + } + + public void setApplication(String application) { + put(APPLICATION, application); + } + + @Override + @SuppressWarnings("unchecked") + public Object clone() { + MetricContext clone = new MetricContext(); + clone.putAll(Map.copyOf((Map) super.clone())); + return clone; + } + + @Override + public String remove(Object key) { + if (DEFAULTS.containsKey(key)) { + throw new UnsupportedOperationException("Can't remove key " + key); + } + return super.remove(key); + } + + @Override + public boolean remove(Object key, Object value) { + if (DEFAULTS.containsKey(key)) { + throw new UnsupportedOperationException("Can't remove key " + key); + } + return super.remove(key, value); + } +} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricDimension.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricDimension.java deleted file mode 100644 index 347ab53a..00000000 --- a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricDimension.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import java.util.Objects; - -public class MetricDimension { - - private String nameSpace; - private String metricName; - //note that tenantId is used during processing and not passed - @JsonIgnore - private String tenantId; - - public MetricDimension(String nameSpace, String metricName) { - this(nameSpace, metricName, null); - } - - public MetricDimension(String nameSpace, String metricName, String tenantId) { - this.nameSpace = nameSpace; - this.metricName = metricName; - this.tenantId = tenantId; - } - - public String getNameSpace() { - return nameSpace; - } - - public void setNameSpace(String nameSpace) { - this.nameSpace = nameSpace; - } - - public String getMetricName() { - return metricName; - } - - public void setMetricName(String metricName) { - this.metricName = metricName; - } - - - public String getTenantId() { - return tenantId; - } - - public void setTenantId(String tenantId) { - this.tenantId = tenantId; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - // Same reference? - if (this == obj) { - return true; - } - // Same type? - if (getClass() != obj.getClass()) { - return false; - } - final MetricDimension other = (MetricDimension) obj; - return Utils.nullableEquals(nameSpace, other.nameSpace) && Utils.nullableEquals(metricName, other.metricName); - } - - @Override - public int hashCode() { - return Objects.hash(nameSpace, metricName); - } - - @Override - public String toString() { - return Utils.toJson(this); - } -} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricHelper.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricHelper.java deleted file mode 100644 index cb473d3e..00000000 --- a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricHelper.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import software.amazon.awssdk.services.athena.AthenaClient; -import software.amazon.awssdk.services.athena.model.AthenaException; -import software.amazon.awssdk.services.athena.model.ColumnInfo; -import software.amazon.awssdk.services.athena.model.Datum; -import software.amazon.awssdk.services.athena.model.GetQueryExecutionRequest; -import software.amazon.awssdk.services.athena.model.GetQueryExecutionResponse; -import software.amazon.awssdk.services.athena.model.GetQueryResultsRequest; -import software.amazon.awssdk.services.athena.model.GetQueryResultsResponse; -import software.amazon.awssdk.services.athena.model.QueryExecutionContext; -import software.amazon.awssdk.services.athena.model.QueryExecutionState; -import software.amazon.awssdk.services.athena.model.ResultConfiguration; -import software.amazon.awssdk.services.athena.model.Row; -import software.amazon.awssdk.services.athena.model.StartQueryExecutionRequest; -import software.amazon.awssdk.services.athena.model.StartQueryExecutionResponse; -import software.amazon.awssdk.services.athena.paginators.GetQueryResultsIterable; -import software.amazon.awssdk.utils.StringUtils; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.math.RoundingMode; -import java.time.DayOfWeek; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.temporal.ChronoUnit; -import java.time.temporal.TemporalAdjusters; -import java.util.*; - -public class MetricHelper { - - public static final long SLEEP_AMOUNT_IN_MS = 500; - - // Method is used to build the P90, P70, and P50 for graphing where - // P90 means 90% of the values were below this value. - public static Map getPercentiles(final List metricValueList) { - final Map retMap = new HashMap(); - final int count = metricValueList.size(); - if (count > 0) { - retMap.put("p90", getPxx(metricValueList, .95)); - retMap.put("p70", getPxx(metricValueList, .70)); - retMap.put("p50", getPxx(metricValueList, .50)); - BigDecimal sum = new BigDecimal(BigInteger.ZERO); - for (MetricValue mv : metricValueList) { - sum = sum.add(new BigDecimal(mv.getValue())); - } - BigDecimal average = sum.divide(new BigDecimal(count), 3, RoundingMode.HALF_UP); - retMap.put("Average", average.doubleValue()); - retMap.put("Sum", sum.setScale(3, RoundingMode.HALF_UP).doubleValue()); - } else { - retMap.put("p90", 0d); - retMap.put("p70", 0d); - retMap.put("p50", 0d); - retMap.put("Average", 0d); - retMap.put("Sum", 0d); - } - return retMap; - } - - public static Double getPxx(final List metricValueList, double index) { - int pIndex = (int) Math.round(index * metricValueList.size()); - double pX = metricValueList.get(pIndex - 1).getValue(); - //LOGGER.debug("getPXX: PX calculated for Percentile: " + index + " is " + pX + " at index: " + pIndex); - return pX; - } - - public static Instant[] getTimeRangeForQuery(String timeRangeName, int offSet, Instant startTime, Instant endTime) { - final Instant curDateTime = Instant.ofEpochMilli(new Date().getTime()); - LocalDateTime localStartDateTime = LocalDateTime.ofInstant(curDateTime.now(), ZoneId.systemDefault()); - if (Utils.isNotBlank(timeRangeName)) { - //LOGGER.debug("getStartDateTime: Using provided query TimeRangeName: " + query.getTimeRangeName()); - try { - TimeRange timeRange = TimeRange.valueOf(timeRangeName); - switch (timeRange) { - case HOUR_1: - case HOUR_2: - case HOUR_4: - case HOUR_8: - case HOUR_10: - case HOUR_12: - case HOUR_24: - startTime = curDateTime.minus(timeRange.getValueToSubtract(), ChronoUnit.HOURS); - break; - case THIS_WEEK: - //get Monday at midnight - localStartDateTime = localStartDateTime - .with(TemporalAdjusters.previous(DayOfWeek.MONDAY)) - .withHour(0) - .withMinute(0) - .withSecond(0) - .withNano(0); - localStartDateTime = localStartDateTime - .minusMinutes(offSet); - startTime = localStartDateTime.atZone(ZoneId.systemDefault()).toInstant(); - break; - case THIS_MONTH: - localStartDateTime = localStartDateTime - .withDayOfMonth(1) - .withHour(0) - .withMinute(0) - .withSecond(0) - .withNano(0); - localStartDateTime = localStartDateTime - .minusMinutes(offSet); - startTime = localStartDateTime.atZone(ZoneId.systemDefault()).toInstant(); - break; - case DAY_7: - case DAY_30: - localStartDateTime = localStartDateTime - .minusDays(timeRange.getValueToSubtract()) - .withHour(0) - .withMinute(0) - .withSecond(0) - .withNano(0); - localStartDateTime = localStartDateTime - .minusMinutes(offSet); - startTime = localStartDateTime.atZone(ZoneId.systemDefault()).toInstant(); - break; - case TODAY: - localStartDateTime = localStartDateTime - .withHour(0) - .withMinute(0) - .withSecond(0) - .withNano(0); - localStartDateTime = localStartDateTime - .minusMinutes(offSet); - startTime = localStartDateTime.atZone(ZoneId.systemDefault()).toInstant(); - break; - } - endTime = curDateTime; - - } catch (Exception e) { - throw new RuntimeException("getTimes: Invalid value for timeRangeName in query"); - } - } - //LOGGER.debug(("getTimes: start: " + startTime + ", finish: " + finishTime)); - Instant[] retTimes = {startTime, endTime}; - return retTimes; - } - - // Submits a sample query to Athena and returns the execution ID of the query. - public static String submitAthenaQuery(AthenaClient athenaClient, String query, String outputBucket, String athenaDatabase) { - try { - // The QueryExecutionContext allows us to set the Database. - QueryExecutionContext queryExecutionContext = QueryExecutionContext.builder() - .database(athenaDatabase).build(); - - // The result configuration specifies where the results of the query should go in S3 and encryption options - ResultConfiguration resultConfiguration = ResultConfiguration.builder() - // You can provide encryption options for the output that is written. - // .withEncryptionConfiguration(encryptionConfiguration) - .outputLocation(outputBucket).build(); - - // Create the StartQueryExecutionRequest to send to Athena which will start the query. - StartQueryExecutionRequest startQueryExecutionRequest = StartQueryExecutionRequest.builder() - .queryString(query) - .queryExecutionContext(queryExecutionContext) - .resultConfiguration(resultConfiguration).build(); - - StartQueryExecutionResponse startQueryExecutionResponse = athenaClient.startQueryExecution(startQueryExecutionRequest); - return startQueryExecutionResponse.queryExecutionId(); - } catch (AthenaException e) { - //LOGGER.error(Utils.getFullStackTrace(e)); - throw e; - } - } - - /** - * Wait for an Athena query to complete, fail or to be cancelled. This is done by polling Athena over an - * interval of time. If a query fails or is cancelled, then it will throw an exception. - */ - public static void waitForQueryToComplete(AthenaClient athenaClient, String queryExecutionId) throws InterruptedException { - GetQueryExecutionRequest getQueryExecutionRequest = GetQueryExecutionRequest.builder() - .queryExecutionId(queryExecutionId).build(); - - GetQueryExecutionResponse getQueryExecutionResponse; - boolean isQueryStillRunning = true; - while (isQueryStillRunning) { - getQueryExecutionResponse = athenaClient.getQueryExecution(getQueryExecutionRequest); - String queryState = getQueryExecutionResponse.queryExecution().status().state().toString(); - if (queryState.equals(QueryExecutionState.FAILED.toString())) { - throw new RuntimeException("Query Failed to run with Error Message: " + getQueryExecutionResponse - .queryExecution().status().stateChangeReason()); - } else if (queryState.equals(QueryExecutionState.CANCELLED.toString())) { - throw new RuntimeException("Query was cancelled."); - } else if (queryState.equals(QueryExecutionState.SUCCEEDED.toString())) { - isQueryStillRunning = false; - } else { - // Sleep an amount of time before retrying again. - Thread.sleep(SLEEP_AMOUNT_IN_MS); - } - } - } - - /** - * This code calls Athena and retrieves the results of a query. - * The query must be in a completed state before the results can be retrieved and - * paginated. The first row of results are the column headers. - */ - public static List processResultRows(AthenaClient athenaClient, String queryExecutionId) { - List metricValueList = null; - try { - // 1. Counts by PATH with status 200. - // 2. Latency by Paths with status 200 - GetQueryResultsRequest getQueryResultsRequest = GetQueryResultsRequest.builder() - // Max Results can be set but if its not set, - // it will choose the maximum page size - // As of the writing of this code, the maximum value is 1000 - // .withMaxResults(1000) - .queryExecutionId(queryExecutionId).build(); - - GetQueryResultsIterable getQueryResultsResults = athenaClient.getQueryResultsPaginator(getQueryResultsRequest); - - for (GetQueryResultsResponse result : getQueryResultsResults) { - List columnInfoList = result.resultSet().resultSetMetadata().columnInfo(); - List results = result.resultSet().rows(); - metricValueList = processRow(results, columnInfoList); - } - - } catch (AthenaException e) { - Utils.getFullStackTrace(e); - throw e; - } - return metricValueList; - } - - private static List processRow(List row, List columnInfoList) { - //Write out the data - List metricValueList = new ArrayList<>(); - boolean first = true; - for (Row myRow : row) { - if (first) { - first = false; - continue; - } - List allData = myRow.data(); - //data is read by position. Position 0 is the path and Position 1 is the value - BigDecimal bd = new BigDecimal(allData.get(1).varCharValue()); - //bd = bd.multiply(new BigDecimal(1000)); //convert to milli seconds - MetricValue val = new MetricValue(bd.setScale(5, RoundingMode.HALF_UP).doubleValue(), - allData.get(0).varCharValue()); - metricValueList.add(val); - } - return metricValueList; - } -} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricQuery.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricQuery.java deleted file mode 100644 index 083dfd4d..00000000 --- a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricQuery.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import java.time.Instant; -import java.util.*; - -public class MetricQuery { - private String id; - private String stat; - private List dimensions = new ArrayList<>(); - private Integer period; - private Instant startDate; - private Instant endDate; - private String timeRangeName; - private int tzOffset = 0; - private List tenants = new ArrayList<>(); - private boolean singleTenant = false; - private boolean topTenants = false; - private boolean statsMap = false; - private boolean tenantTaskMaxCapacity = false; - - @Override - public String toString() { - return Utils.toJson(this); - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getStat() { - return stat; - } - - public void setStat(String stat) { - this.stat = stat; - } - - public List getDimensions() { - return List.copyOf(dimensions); - } - - public void setDimensions(List dimensions) { - this.dimensions = dimensions != null ? dimensions : new ArrayList<>(); - } - - public Integer getPeriod() { - return period; - } - - public void setPeriod(Integer period) { - this.period = period; - } - - public Instant getStartDate() { - if (startDate == null) { - return null; - } else { - return Instant.from(startDate); - } - } - - public void setStartDate(Instant startDate) { - this.startDate = startDate != null ? Instant.from(startDate) : null; - } - - public Instant getEndDate() { - if (endDate == null) { - return null; - } else { - return Instant.from(endDate); - } - } - - public void setEndDate(Instant endDate) { - this.endDate = endDate != null ? Instant.from(endDate) : null; - } - - public String getTimeRangeName() { - return timeRangeName; - } - - public void setTimeRangeName(String timeRangeName) { - this.timeRangeName = timeRangeName; - } - - public int getTzOffset() { - return tzOffset; - } - - public void setTzOffset(int tzOffset) { - this.tzOffset = tzOffset; - } - - public List getTenants() { - return List.copyOf(tenants); - } - - public void setTenants(List tenants) { - this.tenants = tenants != null ? tenants : new ArrayList<>(); - } - - public void addTenant(String tenantId) { - if (!this.tenants.contains(tenantId)) { - this.tenants.add(tenantId); - } - } - - public boolean isSingleTenant() { - return singleTenant; - } - - public void setSingleTenant(boolean singleTenant) { - this.singleTenant = singleTenant; - } - - public boolean isTopTenants() { - return topTenants; - } - - public void setTopTenants(boolean topTenants) { - this.topTenants = topTenants; - } - - public boolean isStatsMap() { - return statsMap; - } - - public void setStatsMap(boolean statsMap) { - this.statsMap = statsMap; - } - - public boolean isTenantTaskMaxCapacity() { - return tenantTaskMaxCapacity; - } - - public void setTenantTaskMaxCapacity(boolean tenantTaskMaxCapacity) { - this.tenantTaskMaxCapacity = tenantTaskMaxCapacity; - } - - public static class Dimension { - private String metricName; - private String nameSpace; - - //need this for JSON deserializer - public Dimension() { - } - - public Dimension(String metricName, String nameSpace) { - this.metricName = metricName; - this.nameSpace = nameSpace; - } - - public String getMetricName() { - return metricName; - } - - public void setMetricName(String metricName) { - this.metricName = metricName; - } - - public String getNameSpace() { - return nameSpace; - } - - public void setNameSpace(String nameSpace) { - this.nameSpace = nameSpace; - } - } -} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricResultItem.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricResultItem.java deleted file mode 100644 index cf7ab7cd..00000000 --- a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricResultItem.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -public class MetricResultItem { - - private MetricDimension dimension; - private Map> stats = new LinkedHashMap<>(); - private List topTenants = new ArrayList<>(); - - public MetricDimension getDimension() { - if (dimension == null) { - return null; - } else { - return new MetricDimension(dimension.getNameSpace(), dimension.getMetricName(), dimension.getTenantId()); - } - } - - public void setDimension(MetricDimension dimension) { - if (dimension == null) { - this.dimension = null; - } else { - this.dimension = new MetricDimension(dimension.getNameSpace(), dimension.getMetricName(), dimension.getTenantId()); - } - } - - public List getStat(String key) { - return List.copyOf(stats.get(key)); - } - - public Map> getStats() { - return Map.copyOf(stats); - } - - public void putStat(String key, List valueList) { - stats.put(key, valueList); - } - - public List getTopTenants() { - return List.copyOf(topTenants); - } - - public void setTopTenant(List topTenants) { - this.topTenants = topTenants != null ? topTenants : new ArrayList<>(); - } - -} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricService.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricService.java deleted file mode 100644 index 14d71b56..00000000 --- a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricService.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; -import java.util.*; - -public class MetricService implements RequestHandler, APIGatewayProxyResponseEvent> { - - private static final Logger LOGGER = LoggerFactory.getLogger(MetricService.class); - private static final Map CORS = Map.of("Access-Control-Allow-Origin", "*"); - private static final String API_GATEWAY_HOST = System.getenv("API_GATEWAY_HOST"); - private static final String API_GATEWAY_STAGE = System.getenv("API_GATEWAY_STAGE"); - private static final String API_TRUST_ROLE = System.getenv("API_TRUST_ROLE"); - private static final String PATH_REQUEST_COUNT_1_HOUR_FILE = "datasets/pathRequestCount01Hour.js"; - private static final String PATH_REQUEST_COUNT_24_HOUR_FILE = "datasets/pathRequestCount24Hour.js"; - private static final String PATH_REQUEST_COUNT_7_DAY_FILE = "datasets/pathRequestCount07Day.js"; - private static final String PATH_RESPONSE_TIME_1_HOUR_FILE = "datasets/pathResponseTime01Hour.js"; - private static final String PATH_RESPONSE_TIME_24_HOUR_FILE = "datasets/pathResponseTime24Hour.js"; - private static final String PATH_RESPONSE_TIME_7_DAY_FILE = "datasets/pathResponseTime07Day.js"; - private static final String PATH_REQUEST_COUNT = "PATH_REQUEST_COUNT"; - private static final String PATH_RESPONSE_TIME = "PATH_RESPONSE_TIME"; - private final MetricServiceDAL dal; - static Map> tenantCache = new HashMap<>(); - - public MetricService() { - LOGGER.info("Version Info: {}", Utils.version(this.getClass())); - this.dal = new MetricServiceDAL(); - } - - protected static void refreshTenantCache(Context context) { - tenantCache = getTenants(context); - } - - @Override - public APIGatewayProxyResponseEvent handleRequest(Map event, Context context) { - //Utils.logRequestEvent(event); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); - } - - public APIGatewayProxyResponseEvent queryMetrics(Map event, Context context) { - final long startTimeMillis = System.currentTimeMillis(); - if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); - } - - Utils.logRequestEvent(event); - - MetricQuery query = Utils.fromJson((String) event.get("body"), MetricQuery.class); - if (query == null) { - return new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(400) - .withBody("{\"message\" : \"Invalid request body\"}"); - } - - boolean refreshTenantCache = tenantCache.isEmpty(); - for (String tenantId : query.getTenants()) { - if (!tenantCache.containsKey(tenantId)) { - refreshTenantCache = true; - break; - } - } - if (refreshTenantCache) { - refreshTenantCache(context); - } - - APIGatewayProxyResponseEvent response; - try { - List result; - if (query.isSingleTenant()) { - LOGGER.info("queryMetrics: Execute Tenant metrics"); - result = dal.queryTenantMetrics(query); - } else { - LOGGER.info("queryMetrics: Execute across all tenants"); - result = dal.queryMetrics(query); - } - response = new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(200) - .withBody(Utils.toJson(result)); - } catch (Exception e) { - LOGGER.error("queryMetrics: Error " + e.getMessage()); - LOGGER.error("queryMetrics: " + Utils.getFullStackTrace(e)); - response = new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(404) - .withBody("{\"message\" : \"" + e.getMessage() + "\"}"); - } - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("queryMetrics: exec " + totalTimeMillis); - return response; - } - - public APIGatewayProxyResponseEvent queryAccessLogs(Map event, Context context) { - final long startTimeMillis = System.currentTimeMillis(); - if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); - } - - Utils.logRequestEvent(event); - if (tenantCache.isEmpty()) { - refreshTenantCache(context); - } - - Map params = (Map) event.get("pathParameters"); - //get the Time Range - String timeRangeParam = params.get("timerange"); - //get metric type of PATH_REQUEST_COUNT or PATH_RESPONSE_TIME - String metricParam = params.get("metric"); - - if (Utils.isBlank(timeRangeParam) || Utils.isBlank(metricParam)) { - return new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(400) - .withBody("{\"message\" : \"Must specify timeRange and metric parameters!\"}"); - } - - try { - TimeRange.valueOf(timeRangeParam); - } catch (IllegalArgumentException e) { - return new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(400) - .withBody("{\"message\" : \"Invalid value for timeRange!\"}"); - } - - if (!(metricParam.equals(PATH_REQUEST_COUNT) || metricParam.equals(PATH_RESPONSE_TIME))) { - return new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(400) - .withBody("{\"message\" : \"Invalid value for metric. Expecting PATH_REQUEST_COUNT or PATH_RESPONSE_TIME.\"}"); - } - - APIGatewayProxyResponseEvent response; - try { - List result = dal.queryAccessLogs(timeRangeParam, metricParam, params.get("id")); - if (result != null) { - response = new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(200) - .withBody(Utils.toJson(result)); - } else { - response = new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(404); - } - } catch (Exception e) { - LOGGER.error("queryAccessLogs: Error " + e.getMessage()); - LOGGER.error(Utils.getFullStackTrace(e)); - response = new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(400) - .withBody("{\"message\" : \"" + e.getMessage() + "\"}"); - } - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("queryAccessLogs: exec " + totalTimeMillis); - return response; - } - - // publish files to S3 web bucket with access log data for graphing to speed up UI - // This is called from scheduled Cloudwatch event. - public void publishRequestCountMetrics(InputStream inputStream, OutputStream outputStream, Context context) { - dal.publishAccessLogMetrics(PATH_REQUEST_COUNT_1_HOUR_FILE, TimeRange.HOUR_1, PATH_REQUEST_COUNT); - dal.publishAccessLogMetrics(PATH_REQUEST_COUNT_24_HOUR_FILE, TimeRange.HOUR_24, PATH_REQUEST_COUNT); - dal.publishAccessLogMetrics(PATH_REQUEST_COUNT_7_DAY_FILE, TimeRange.DAY_7, PATH_REQUEST_COUNT); - } - - public void publishResponseTimeMetrics(InputStream inputStream, OutputStream outputStream, Context context) { - dal.publishAccessLogMetrics(PATH_RESPONSE_TIME_1_HOUR_FILE, TimeRange.HOUR_1, PATH_RESPONSE_TIME); - dal.publishAccessLogMetrics(PATH_RESPONSE_TIME_24_HOUR_FILE, TimeRange.HOUR_24, PATH_RESPONSE_TIME); - dal.publishAccessLogMetrics(PATH_RESPONSE_TIME_7_DAY_FILE, TimeRange.DAY_7, PATH_RESPONSE_TIME); - } - - // Creates a new partition for the day - public void addAthenaPartition(InputStream inputStream, OutputStream outputStream, Context context) { - try { - dal.addAthenaPartition(); - } catch (Exception e) { - LOGGER.error("addAthenaPartition: Error with function. {}", e.getMessage()); - LOGGER.error(Utils.getFullStackTrace(e)); - //*TODO: should we send a SNS message - } - } - - public APIGatewayProxyResponseEvent getAccessMetricsSignedUrls(Map event, Context context) { - final long startTimeMillis = System.currentTimeMillis(); - if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); - } - - //Utils.logRequestEvent(event); - LOGGER.info("getAccessLogSignedUrls: starting"); - - Map signedUrls = new LinkedHashMap<>(); - signedUrls.put("PATH_REQUEST_COUNT_1_HOUR_FILE", dal.getPreSignedUrl(PATH_REQUEST_COUNT_1_HOUR_FILE)); - signedUrls.put("PATH_REQUEST_COUNT_24_HOUR_FILE", dal.getPreSignedUrl(PATH_REQUEST_COUNT_24_HOUR_FILE)); - signedUrls.put("PATH_REQUEST_COUNT_7_DAY_FILE", dal.getPreSignedUrl(PATH_REQUEST_COUNT_7_DAY_FILE)); - signedUrls.put("PATH_RESPONSE_TIME_1_HOUR_FILE", dal.getPreSignedUrl(PATH_RESPONSE_TIME_1_HOUR_FILE)); - signedUrls.put("PATH_RESPONSE_TIME_24_HOUR_FILE", dal.getPreSignedUrl(PATH_RESPONSE_TIME_24_HOUR_FILE)); - signedUrls.put("PATH_RESPONSE_TIME_7_DAY_FILE", dal.getPreSignedUrl(PATH_RESPONSE_TIME_7_DAY_FILE)); - - String responseBody = Utils.toJson(List.copyOf(signedUrls.entrySet())); - LOGGER.info("Presigned URLS = {}", responseBody); - - APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(200) - .withBody(responseBody); - - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("getAccessLogSignedUrls: exec " + totalTimeMillis); - - return response; - } - - protected static Map> getTenants(Context context) { - final long startMillis = System.currentTimeMillis(); - if (Utils.isBlank(API_GATEWAY_HOST)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_HOST"); - } - if (Utils.isBlank(API_GATEWAY_STAGE)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_STAGE"); - } - if (Utils.isBlank(API_TRUST_ROLE)) { - throw new IllegalStateException("Missing required environment variable API_TRUST_ROLE"); - } - LOGGER.info("Calling tenant service to fetch tenants"); - String getTenantResponseBody = ApiGatewayHelper.signAndExecuteApiRequest( - ApiGatewayHelper.getApiRequest( - API_GATEWAY_HOST, - API_GATEWAY_STAGE, - ApiRequest.builder() - .resource("tenants") - .method("GET") - .build() - ), - API_TRUST_ROLE, - context.getAwsRequestId() - ); - List> tenants = Utils.fromJson(getTenantResponseBody, ArrayList.class); - if (tenants == null) { - tenants = new ArrayList<>(); - } - LOGGER.info("getTenants: Total time to get list of tenants: {}", (System.currentTimeMillis() - startMillis)); - LOGGER.info("Caching {} tenants", tenants.size()); - Map> tenantMap = new HashMap<>(); - for (Map tenant : tenants) { - tenantMap.put((String) tenant.get("id"), tenant); - } - return tenantMap; - } -} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricServiceDAL.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricServiceDAL.java deleted file mode 100644 index 4e3b3e15..00000000 --- a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricServiceDAL.java +++ /dev/null @@ -1,742 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; -import software.amazon.awssdk.core.sync.RequestBody; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.applicationautoscaling.ApplicationAutoScalingClient; -import software.amazon.awssdk.services.applicationautoscaling.model.*; -import software.amazon.awssdk.services.athena.AthenaClient; -import software.amazon.awssdk.services.cloudwatch.CloudWatchClient; -import software.amazon.awssdk.services.cloudwatch.model.*; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.GetObjectRequest; -import software.amazon.awssdk.services.s3.model.PutObjectRequest; -import software.amazon.awssdk.services.s3.presigner.S3Presigner; -import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; -import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.time.Duration; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.stream.Collectors; - -public class MetricServiceDAL { - - private static final Logger LOGGER = LoggerFactory.getLogger(MetricServiceDAL.class); - private static final String AWS_REGION = System.getenv("AWS_REGION"); - private static final String ATHENA_DATABASE = System.getenv("ATHENA_DATABASE"); - private static final String S3_ATHENA_OUTPUT_PATH = System.getenv("S3_ATHENA_OUTPUT_PATH"); - private static final String S3_ATHENA_BUCKET = System.getenv("S3_ATHENA_BUCKET"); - private static final String ACCESS_LOGS_TABLE = System.getenv("ACCESS_LOGS_TABLE"); - private static final String ACCESS_LOGS_PATH = System.getenv("ACCESS_LOGS_PATH"); - private final ApplicationAutoScalingClient autoScaling; - private final CloudWatchClient cloudWatch; - private final S3Client s3; - private final S3Presigner presigner; - private final AthenaClient athenaClient; - private final Map dataQueryDimMap = new LinkedHashMap<>(); - - public MetricServiceDAL() { - if (Utils.isBlank(AWS_REGION)) { - throw new IllegalStateException("Missing environment variable AWS_REGION"); - } - if (Utils.isBlank(ATHENA_DATABASE)) { - throw new IllegalStateException("Missing required environment variable ATHENA_DATABASE"); - } - if (Utils.isBlank(S3_ATHENA_BUCKET)) { - throw new IllegalStateException("Missing required environment variable S3_ATHENA_BUCKET"); - } - if (Utils.isBlank(S3_ATHENA_OUTPUT_PATH)) { - throw new IllegalStateException("Missing required environment variable S3_ATHENA_OUTPUT_PATH"); - } - if (Utils.isBlank(ACCESS_LOGS_TABLE)) { - throw new IllegalStateException("Missing required environment variable ACCESS_LOGS_TABLE"); - } - this.s3 = Utils.sdkClient(S3Client.builder(), S3Client.SERVICE_NAME); - this.athenaClient = Utils.sdkClient(AthenaClient.builder(), AthenaClient.SERVICE_NAME); - this.cloudWatch = Utils.sdkClient(CloudWatchClient.builder(), CloudWatchClient.SERVICE_NAME); - this.autoScaling = Utils.sdkClient(ApplicationAutoScalingClient.builder(), ApplicationAutoScalingClient.SERVICE_NAME); - try { - String presignerEndpoint = "https://" + s3.serviceName() + "." - + Region.of(AWS_REGION) - + "." - + Utils.endpointSuffix(AWS_REGION); - this.presigner = S3Presigner.builder() - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .region(Region.of(AWS_REGION)) - .endpointOverride(new URI(presignerEndpoint)) - .build(); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - } - - // Used to query CW metrics across tenants and aggregate the data - public List queryMetrics(final MetricQuery query) { - final long startTimeMillis = System.currentTimeMillis(); - LOGGER.info("queryMetrics: start"); - - List listResult = new ArrayList<>(); - List periodList = new ArrayList<>(); - List queryResultList = new ArrayList<>(); - QueryResult mrs = new QueryResult(); - mrs.setId(query.getId()); - try { - List tenants; - if (!query.getTenants().isEmpty()) { - LOGGER.info("queryMetrics: use tenants from query"); - tenants = new ArrayList<>(query.getTenants()); - } else { - tenants = new ArrayList<>(MetricService.tenantCache.keySet()); - } - - if (tenants.size() > 500) { - throw new RuntimeException("queryMetrics: Cannot process more than 500 tenants"); - } else if (tenants.isEmpty()) { - throw new RuntimeException("queryMetrics: No tenants to process"); - } - - //build query - final List dq = cloudWatchMetricsQueries(query, tenants); - - //now that query is built let's execute and get resultant data - //the data will be stored in Metric object and placed in map by MetricDimension. - Map metricMap = loadCloudWatchMetricsData(query, dq); - LOGGER.info("queryMetrics: metricMap item count: " + metricMap.size()); - - for (final Map.Entry metricEntry : metricMap.entrySet()) { - final Metric metric = metricEntry.getValue(); - final MetricDimension metricDimension = metricEntry.getKey(); - LOGGER.info("queryMetrics: Dimension: {} {} Count: {}", metricDimension.getNameSpace(), - metric.getMetricName(), metric.getMetricValues().size()); - //construct a MetricDimension without a Tenant Id as the metrics are not by tenant - MetricDimension md = new MetricDimension(metricDimension.getNameSpace(), metricDimension.getMetricName()); - MetricResultItem mr = new MetricResultItem(); - mr.setDimension(md); - - List p90List = new ArrayList<>(); - List p70List = new ArrayList<>(); - List p50List = new ArrayList<>(); - List avgList = new ArrayList<>(); - List sumList = new ArrayList<>(); - Map tenantSumMap = new LinkedHashMap<>(); - - //now process the metric entries - for (Map.Entry> entry : metric.getTimeValMap().entrySet()) { - //add entry for the period key - final String period = DateTimeFormatter - .ofPattern("MM-dd HH:mm") - .withZone(ZoneId.systemDefault()) - .format(entry.getKey()); - if (!periodList.contains(period)) { - periodList.add(period); - } - - //build array list in order of least to high - final List metricValueList = new ArrayList<>(entry.getValue().size()); - while (!entry.getValue().isEmpty()) { - //poll retrieve least item first - MetricValue tenantVal = entry.getValue().poll(); - metricValueList.add(tenantVal); - if (query.isTopTenants()) { - //Note: The -1 is to get the reverse order from greatest to least when we later sort - if (tenantSumMap.containsKey(tenantVal.getId())) { - tenantSumMap.put(tenantVal.getId(), tenantSumMap.get(tenantVal.getId()) - tenantVal.getValue()); - } else { - tenantSumMap.put(tenantVal.getId(), -1 * tenantVal.getValue()); - } - } - } - - if (query.isStatsMap()) { - final Map percentilesMap = MetricHelper.getPercentiles(metricValueList); - p90List.add(percentilesMap.get("p90")); - p70List.add(percentilesMap.get("p70")); - p50List.add(percentilesMap.get("p50")); - avgList.add(percentilesMap.get("Average")); - sumList.add(percentilesMap.get("Sum")); - } - - } - //let's store the lists - if (query.isStatsMap()) { - mr.putStat("P90", p90List); - mr.putStat("P70", p70List); - mr.putStat("P50", p50List); - mr.putStat("Average", avgList); - mr.putStat("Sum", sumList); - } - - //now compute the top 10 tenants - if (query.isTopTenants()) { - Map sortedTenantValMap = tenantSumMap.entrySet() - .stream() - .sorted(Map.Entry.comparingByValue()) - .collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - (oldValue, newValue) -> oldValue, LinkedHashMap::new)); - //now iterate through and get top 10. - List topTenantList = new ArrayList<>(); - int i = 0; - for (Map.Entry entry : sortedTenantValMap.entrySet()) { - //if the stat is average then divide by number of periods. - MetricValue mv = new MetricValue(-1 * entry.getValue(), entry.getKey()); - if ("Average".equalsIgnoreCase(query.getStat())) { - BigDecimal bd = BigDecimal.valueOf(-1 * entry.getValue() / periodList.size()) - .setScale(3, RoundingMode.HALF_UP); - mv.setValue(bd.doubleValue()); - } - topTenantList.add(mv); - i++; - //only output first 10 - if (10 == i) { - break; - } - } - - //**TODO: now we have to reverse the order. - mr.setTopTenant(topTenantList); - } - - listResult.add(mr); - } - - if (query.isTenantTaskMaxCapacity()) { - Map tenantTaskMaxCapacityMap = getTaskMaxCapacity(tenants); - //build array of metric value to return - List tenantTaskMaxCapacityList = new ArrayList<>(); - for (Map.Entry entry : tenantTaskMaxCapacityMap.entrySet()) { - MetricValue mv = new MetricValue(entry.getValue(), entry.getKey()); - tenantTaskMaxCapacityList.add(mv); - } - mrs.setTenantTaskMaxCapacity(tenantTaskMaxCapacityList); - } - - mrs.setMetrics(listResult); - mrs.setPeriods(periodList); - queryResultList.add(mrs); - - } catch (CloudWatchException e) { - LOGGER.error("queryMetrics: " + e.awsErrorDetails().errorMessage()); - LOGGER.error("queryMetrics: ", e); - LOGGER.error(Utils.getFullStackTrace(e)); - throw e; - } - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("queryMetrics: exec " + totalTimeMillis); - return queryResultList; - } - - // Used to query metrics for a specified tenant - public List queryTenantMetrics(final MetricQuery query) { - final long startTimeMillis = System.currentTimeMillis(); - LOGGER.info("queryTenantMetrics: start"); - List queryResults = new ArrayList<>(); - List metrics = new ArrayList<>(); - - QueryResult queryResult = new QueryResult(); - queryResult.setId(query.getId()); - try { - if (query.getTenants().size() != 1) { - throw new RuntimeException(("queryTenantMetrics: query JSON must have single item in tenants!")); - } - - //build query - final List dataQueries = cloudWatchMetricsQueries(query, query.getTenants()); - - //now that query is built let's execute and get resultant data - //the data will be stored in Metric object and placed in map by MetricDimension. - Map metricMap = loadCloudWatchMetricsData(query, dataQueries); - LOGGER.info("queryTenantMetrics: metricMap Size: {}", metricMap.size()); - - boolean firstTime = true; - for (final Map.Entry entry : metricMap.entrySet()) { - final Metric metric = entry.getValue(); - final MetricDimension dimension = entry.getKey(); - LOGGER.info("queryTenantMetrics: dimension: {}, metricValues: {}", dimension.toString(), - metric.getMetricValues().size()); - - //construct a MetricDimension without a Tenant Id as the metrics are not by tenant - MetricDimension metricDimension = new MetricDimension(dimension.getNameSpace(), dimension.getMetricName()); - MetricResultItem metricResultItem = new MetricResultItem(); - metricResultItem.setDimension(metricDimension); - - //reverse the values - List values = new ArrayList<>(metric.getMetricValues()); - Collections.reverse(values); - metricResultItem.putStat("Values", values); - - // Need a single copy of the time periods for this query - if (firstTime) { - List periodsList = new ArrayList<>(); - for (final Instant timeVal : metric.getMetricTimes()) { - //add entry for the period key - periodsList.add(DateTimeFormatter - .ofPattern("MM-dd HH:mm") - .withZone(ZoneId.systemDefault()) - .format(timeVal) - ); - } - Collections.reverse(periodsList); - queryResult.setPeriods(periodsList); - firstTime = false; - } - - metrics.add(metricResultItem); - } - - queryResult.setMetrics(metrics); - queryResults.add(queryResult); - - } catch (CloudWatchException e) { - LOGGER.error("queryTenantMetrics: " + e.awsErrorDetails().errorMessage()); - LOGGER.error("queryTenantMetrics: ", e); - LOGGER.error(Utils.getFullStackTrace(e)); - throw e; - } - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("queryTenantMetrics: exec " + totalTimeMillis); - return queryResults; - } - - /* - Used to query ALB access log metrics from Athena and S3 logs - */ - public List queryAccessLogs(String timeRange, String metricType, String tenantId) { - final long startTimeMillis = System.currentTimeMillis(); - LOGGER.info("queryMetrics: start"); - List metricValueList; - try { - //Query based on Access Logs requested. REQUEST_COUNT or RESPONSE_TIME - //get time range - Instant[] times = MetricHelper.getTimeRangeForQuery(timeRange, 0,null, null); - String where = "WHERE target_status_code = '200' AND time >= '" + times[0] + "' AND time <= '" + times[1] + "'\n"; - - if (tenantId != null) { - String tenantAlb = getTenantLoadBalancerId(tenantId); - if (Utils.isEmpty(tenantAlb)) { - throw new RuntimeException("queryAccessLogs: No ALB found for tenantId: " + tenantId); - } - where += " AND elb = '" + tenantAlb + "'\n"; - } - - String metricCol; - if ("PATH_REQUEST_COUNT".equalsIgnoreCase(metricType)) { - metricCol = ", count(1) AS request_count\n"; - } else if ("PATH_RESPONSE_TIME".equalsIgnoreCase(metricType)) { - metricCol = ", avg(target_processing_time) AS avg_target_time\n"; - } else { - LOGGER.warn("Unknown metricType {}", metricType); - metricCol = "\n"; - } - - String query = new StringBuilder().append("SELECT\n") - .append("concat(url_extract_path(request_url), '+', request_verb) AS url") - .append(metricCol) - .append("FROM \"") - .append(ACCESS_LOGS_TABLE) - .append("\"\n") - .append(where) - .append("GROUP BY concat(url_extract_path(request_url), '+', request_verb)\n") - .append("ORDER BY 2 DESC\n") - .append("LIMIT 10;") - .toString(); - - LOGGER.info("queryAccessLogs: athena query \n" + query); - - //now that query is built let's execute and get resultant data - //the data will be stored in Metric object and placed in map by MetricDimension. - metricValueList = getAthenaData(query); - } catch (Exception e) { - LOGGER.error("queryAccessLogs error: ", e); - LOGGER.error(Utils.getFullStackTrace(e)); - throw new RuntimeException(e); - } - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("queryAccessLogs: exec time " + totalTimeMillis); - return metricValueList; - } - - // Build the CloudWatch query based on the dimensions from the query - private List cloudWatchMetricsQueries(MetricQuery query, final List tenants) { - List dq = new ArrayList<>(); - int dimIndex = 0; - - //lets get the period based on the time range - int period = getPeriod(query); - //store the period into query - query.setPeriod(period); - LOGGER.info("buildDataQuery: period value: " + period + " for timeRangeName: " + query.getTimeRangeName()); - - for (String tenantId : tenants) { - Set dimList = new HashSet<>(); - //build the dataquery with the dimensions - for (final MetricQuery.Dimension queryDimension : query.getDimensions()) { - if ("AWS/ECS".equalsIgnoreCase(queryDimension.getNameSpace())) { - String cluster = getTenantEcsCluster(tenantId); - if (Utils.isEmpty(cluster)) { - throw new RuntimeException("queryMetrics: No ECS cluster found for tenant: " + tenantId); - } - // We don't know how many ECS services there are, so ask CloudWatch for all of the - // dimensions we can use for this metric in the tenant's cluster. - ListMetricsResponse availableMetrics = cloudWatch.listMetrics(request -> request - .namespace(queryDimension.getNameSpace()) - .metricName(queryDimension.getMetricName()) - .dimensions(DimensionFilter.builder().name("ClusterName").value(cluster).build()) - ); - if (availableMetrics.hasMetrics()) { - for (software.amazon.awssdk.services.cloudwatch.model.Metric availableMetric : availableMetrics.metrics()) { - dimList.addAll(availableMetric.dimensions()); - } - } - //} else if ("ECS/ContainerInsights".equalsIgnoreCase(queryDimension.getNameSpace())) { - } else if ("AWS/ApplicationELB".equalsIgnoreCase(queryDimension.getNameSpace())) { - final String albId = getTenantLoadBalancerId(tenantId); - if (Utils.isEmpty(albId)) { - throw new RuntimeException("queryMetrics: No ALB Id found for tenant: " + tenantId); - } - Dimension dimension = Dimension.builder() - .name("LoadBalancer") - .value(albId) - .build(); - dimList.add(dimension); - } else { - throw new RuntimeException("queryMetrics: Namespace: " + queryDimension.getNameSpace() - + " not currently implemented"); - } - - software.amazon.awssdk.services.cloudwatch.model.Metric met = software.amazon.awssdk.services.cloudwatch.model.Metric.builder() - .namespace(queryDimension.getNameSpace()) - .metricName(queryDimension.getMetricName()) - .dimensions(dimList) - .build(); - - MetricStat stat = MetricStat.builder() - .stat(query.getStat()) - .period(period) - .metric(met) - .build(); - - //store dim in map so we can match with result data later - MetricDimension metricDimension = new MetricDimension( - queryDimension.getNameSpace(), - queryDimension.getMetricName(), - tenantId - ); - this.dataQueryDimMap.put("query_" + dimIndex, metricDimension); - - MetricDataQuery dataQuery = MetricDataQuery.builder() - .metricStat(stat) - .id("query0_" + dimIndex) - .returnData(false) - .build(); - dq.add(dataQuery); - - //Tell CW to fill with zeros for gaps - dataQuery = MetricDataQuery.builder() - .id("query_" + dimIndex) - .expression("FILL(query0_" + dimIndex + ", 0)") - .returnData(true) - .build(); - - //Max of 500 MetricDataQuery can be in a single call - dq.add(dataQuery); - if (dq.size() > 500) { - throw new RuntimeException("Can only process up to 500 data query items in GetMetricsData API"); - } - dimIndex++; - } //end for of metric dimensions - } - return dq; - } - - private int getPeriod(MetricQuery query) { - // If query has the period then it overrides the the time range - if (query.getPeriod() != null) { - return query.getPeriod(); - } - - if (Utils.isNotBlank(query.getTimeRangeName())) { - LOGGER.info("getStartDateTime: Using provided query TimeRangeName: " + query.getTimeRangeName()); - try { - TimeRange timeRange = TimeRange.valueOf(query.getTimeRangeName()); - switch (timeRange) { - case TODAY: - case THIS_WEEK: - return 60 * 60; // every hour - case DAY_7: - case DAY_30: - case THIS_MONTH: - return 60 * 60 * 3; // every 3 hours - case HOUR_8: - case HOUR_10: - case HOUR_12: - case HOUR_24: - return 60 * 15; // every 15 minutes - default: - return 60 * 5; // every 5 minutes - } - } catch (Exception e) { - throw new RuntimeException("getPeriod: Invalid TimeRangeName provided: " + query.getTimeRangeName()); - } - } - return query.getPeriod(); - } - - // Loads data from AWS Cloudwatch and builds a Priority queue of values in Metric object for each timestamp - private Map loadCloudWatchMetricsData(MetricQuery query, List dq) { - final long startTimeMillis = System.currentTimeMillis(); - String nextToken = null; - Map metricMap = new LinkedHashMap<>(); - //get start date from Range if provided - final Instant[] times = MetricHelper.getTimeRangeForQuery( - query.getTimeRangeName(), - query.getTzOffset(), - query.getStartDate(), - query.getEndDate() - ); - LOGGER.info("loadCWMetricData: Start and Finish times for CW data query are {} and {}", times[0], times[1]); - //LOGGER.info(Utils.toJson(dq)); - do { - GetMetricDataRequest getMetReq = GetMetricDataRequest.builder() - .maxDatapoints(10000) - .startTime(times[0]) - .endTime(times[1]) - .metricDataQueries(dq) - .nextToken(nextToken) - .build(); - - final GetMetricDataResponse response = cloudWatch.getMetricData(getMetReq); - nextToken = response.nextToken(); - - final List data = response.metricDataResults(); - LOGGER.info("loadCWMetricData: fetch time in ms: " + (System.currentTimeMillis() - startTimeMillis)); - //LOGGER.info(Utils.toJson(data)); - - //process metrics data from CloudWatch into our own POJOs for aggregation - for (MetricDataResult item : data) { - LOGGER.info("loadCWMetricData: " + String.format("Id: %s, label: %s", item.id(), item.label())); - LOGGER.info("loadCWMetricData: The status code is " + item.statusCode().toString()); - LOGGER.info("loadCWMetricData: Returned items count " + item.values().size()); - - final MetricDimension metricDimension = dataQueryDimMap.get(item.id()); - Metric metric = metricMap.get(metricDimension); - if (null == metric) { - metric = new Metric(); - metric.setNameSpace(metricDimension.getNameSpace()); - metric.setMetricName(metricDimension.getMetricName()); - metric.setStat(query.getStat()); - metric.setPeriod(query.getPeriod()); - metricMap.put(metricDimension, metric); - } - - for (int x = 0; x < item.values().size(); x++) { - BigDecimal bd = BigDecimal.valueOf(item.values().get(x)).setScale(3, RoundingMode.HALF_UP); - double value = bd.doubleValue(); - //LOGGER.info("CloudWatch Metric Value " + item.values().get(x)); - //LOGGER.info("Metric Value as double {}", value); - //LOGGER.info("CloudWatch Metric Timestamp " + item.timestamps().get(x)); - if (query.isSingleTenant()) { - //store so it is not sorted by value - metric.addMetricValue(value); - //store time into sorted map - metric.addSortTime(item.timestamps().get(x)); - } else { - // If we're querying for all tenants, save the metrics keyed by tenant id - final MetricValue mv = new MetricValue(value, dataQueryDimMap.get(item.id()).getTenantId()); - metric.addQueueValue(item.timestamps().get(x), mv); - } - } - } - } while (Utils.isNotEmpty(nextToken)); - return metricMap; - } - - protected String getTenantLoadBalancerId(String tenantId) { - LOGGER.info("Getting ALB for tenant {}", tenantId); - Map tenant = MetricService.tenantCache.get(tenantId); - String alb; - try { - Map resources = (Map) tenant.get("resources"); - alb = ((Map) resources.get("LOAD_BALANCER")).get("name"); - } catch (NullPointerException npe) { - LOGGER.error("Can't find LOAD_BALANCER resource for tenant {}", tenantId); - alb = ""; - } - return alb; - } - - protected String getTenantEcsCluster(String tenantId) { - LOGGER.info("Getting ECS cluster for tenant {}", tenantId); - Map tenant = MetricService.tenantCache.get(tenantId); - String cluster; - try { - Map resources = (Map) tenant.get("resources"); - cluster = ((Map) resources.get("ECS_CLUSTER")).get("name"); - } catch (NullPointerException npe) { - LOGGER.error("Can't find ECS_CLUSTER resource for tenant {}", tenantId); - cluster = ""; - } - return cluster; - } - - protected Map getTaskMaxCapacity(List tenants) { - String nextToken = null; - final ServiceNamespace ns = ServiceNamespace.ECS; - final ScalableDimension ecsTaskCount = ScalableDimension.ECS_SERVICE_DESIRED_COUNT; - List resourceIds = new ArrayList<>(); - Map capacityMap = new LinkedHashMap<>(); - Map tenantMap = new LinkedHashMap<>(); - for (String tenantId : tenants) { - String segment1 = tenantId.split("-")[0]; - if (!tenantId.startsWith("tenant-")) { - segment1 = "tenant-" + segment1; - } - capacityMap.put(tenantId, 1); //initialize to value of 1 in case the Service does not have autoscaling setup - tenantMap.put(segment1, tenantId); //store full id so we can return back. - resourceIds.add("service/" + segment1 + "/" + segment1); - } - - // query for target - do { - DescribeScalableTargetsRequest dscRequest = DescribeScalableTargetsRequest.builder() - .serviceNamespace(ns) - .scalableDimension(ecsTaskCount) - .resourceIds(resourceIds) - .nextToken(nextToken) - .build(); - try { - long start = System.currentTimeMillis(); - DescribeScalableTargetsResponse resp = autoScaling.describeScalableTargets(dscRequest); - nextToken = resp.nextToken(); - LOGGER.info("getTaskMaxCapacity: DescribeScalableTargets result in " + (System.currentTimeMillis() - start) + " ms, : "); - // LOGGER.info(String.valueOf(resp)); - List targets = resp.scalableTargets(); - for (ScalableTarget target : targets) { - ScalableDimension dim = target.scalableDimension(); - String[] id = target.resourceId().split("/"); - String tenantFullId = tenantMap.get(id[2]); - capacityMap.put(tenantFullId, target.maxCapacity()); - LOGGER.info("getTaskMaxCapacity: Dim: " + dim + ", Resource: " + id[2] + ", MaxCapacity: " + target.maxCapacity()); - } - } catch (Exception e) { - LOGGER.error("getTaskMaxCapacity: Unable to describe scalable target: "); - LOGGER.error(e.getMessage()); - throw new RuntimeException(("getTaskMaxCapacity: Error with task capacity metrics")); - } - } while (Utils.isNotEmpty(nextToken)); - return capacityMap; - } - - private List getAthenaData(String query) throws Exception { - final long startTimeMillis = System.currentTimeMillis(); - String queryExecutionId = MetricHelper.submitAthenaQuery(athenaClient, query, S3_ATHENA_OUTPUT_PATH, ATHENA_DATABASE); - MetricHelper.waitForQueryToComplete(athenaClient, queryExecutionId); - List metricValueList = MetricHelper.processResultRows(athenaClient, queryExecutionId); - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("MetricsService::getAthenaData exec {}", totalTimeMillis); - return metricValueList; - } - - //create partition for Athena table - public void addAthenaPartition() throws Exception { - final long start = System.currentTimeMillis(); - LOGGER.info("addAthenaPartition: Start"); - if (Utils.isBlank(ACCESS_LOGS_PATH)) { - throw new IllegalStateException("Missing required environment variable ACCESS_LOGS_PATH"); - } - - //System.out.println(DATE_TIME_FORMATTER.format(new Date().toInstant())); - Instant today = Instant.now(); - String formatPartitionDate = DateTimeFormatter.ofPattern("yyyy/MM/dd").withZone(ZoneId.systemDefault()) - .format(today); //"2019/08/01"; - String dateTimeFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.systemDefault()) - .format(today); //"2019-08-01"; - String queryString = "ALTER TABLE \"" + ACCESS_LOGS_TABLE + "\" ADD IF NOT EXISTS PARTITION " - + "(time='" + dateTimeFormat + "') LOCATION '" + ACCESS_LOGS_PATH + "/" + formatPartitionDate + "/';"; - LOGGER.info("addAthenaPartition: Query for partition: {}", queryString); - String queryExecutionId = MetricHelper.submitAthenaQuery( - athenaClient, - queryString, - S3_ATHENA_OUTPUT_PATH, - ATHENA_DATABASE - ); - MetricHelper.waitForQueryToComplete(athenaClient, queryExecutionId); - - //get return data - //List metricValueList= MetricHelper.processResultRows(athenaClient, queryExecutionId); - LOGGER.info("addAthenaPartition: Executed in: " + (System.currentTimeMillis() - start) + " ms"); - //return metricValueList; - } - - public void publishAccessLogMetrics(String s3FileName, Enum timeRangeName, String metric) { - final long startTimeMillis = System.currentTimeMillis(); - try { - final List result = queryAccessLogs(timeRangeName.toString(), metric, null); - this.s3.putObject(PutObjectRequest.builder() - .bucket(S3_ATHENA_BUCKET) - .key(s3FileName) - .cacheControl("no-store") - .build(), RequestBody.fromString(Utils.toJson(result)) - ); - } catch (Exception e) { - LOGGER.error("writeAccessLogMetrics: Error " + e.getMessage()); - LOGGER.error(Utils.getFullStackTrace(e)); - } - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("writeAccessLogMetrics: exec " + totalTimeMillis); - } - - public URL getPreSignedUrl(String key) { - // Create a GetObjectRequest to be pre-signed - GetObjectRequest getObjectRequest = GetObjectRequest.builder() - .bucket(S3_ATHENA_BUCKET) - .key(key) - .build(); - - GetObjectPresignRequest getObjectPresignRequest = - GetObjectPresignRequest.builder() - .signatureDuration(Duration.ofMinutes(60)) - .getObjectRequest(getObjectRequest) - .build(); - - // Generate the presigned request - LOGGER.info("Generating presigned S3 URL for {}{}", S3_ATHENA_BUCKET, key); - PresignedGetObjectRequest presignedGetObjectRequest = presigner.presignGetObject(getObjectPresignRequest); - URL url; - if (presignedGetObjectRequest.isBrowserExecutable()) { - url = presignedGetObjectRequest.url(); - } else { - LOGGER.error("S3 URL can't be executed by web browser"); - url = null; - } - return url; - } - -} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsDataAccessLayer.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsDataAccessLayer.java new file mode 100644 index 00000000..3eba4044 --- /dev/null +++ b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsDataAccessLayer.java @@ -0,0 +1,8 @@ +package com.amazon.aws.partners.saasfactory.saasboost; + +public class MetricsDataAccessLayer { + + public MetricsProviderConfig getProviderConfig() { + return new MetricsProviderConfig(MetricsProvider.ProviderType.CLOUDWATCH); + } +} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsProvider.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsProvider.java new file mode 100644 index 00000000..000ecb90 --- /dev/null +++ b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import java.util.Properties; + +public interface MetricsProvider { + + enum ProviderType { + CLOUDWATCH + } + + ProviderType type(); + + Properties getProperties(); + + MetricsProviderApi api(); +} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsProviderApi.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsProviderApi.java new file mode 100644 index 00000000..9b539f76 --- /dev/null +++ b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsProviderApi.java @@ -0,0 +1,24 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import java.util.Collection; + +public interface MetricsProviderApi { + + void putMetrics(Collection metrics); +} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsProviderConfig.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsProviderConfig.java new file mode 100644 index 00000000..954eb1a4 --- /dev/null +++ b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsProviderConfig.java @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; +import java.util.Properties; + +public class MetricsProviderConfig { + + private final MetricsProvider.ProviderType type; + private final Properties properties; + + public MetricsProviderConfig(@JsonProperty("type") MetricsProvider.ProviderType type) { + this.type = type; + this.properties = new Properties(); + switch (this.type) { + case CLOUDWATCH: + this.properties.putAll(CloudWatchMetricsProvider.DEFAULTS); + break; + default: + break; + } + } + + public Properties getProperties() { + return (Properties) properties.clone(); + } + + public void setProperties(Map properties) { + if (properties != null) { + this.properties.putAll(properties); + } + } + + public MetricsProvider.ProviderType getType() { + return type; + } +} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsProviderFactory.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsProviderFactory.java new file mode 100644 index 00000000..0d357aba --- /dev/null +++ b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsProviderFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MetricsProviderFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(MetricsProviderFactory.class); + + private MetricsProviderFactory() { + } + + public static MetricsProviderFactory getInstance() { + return MetricsProviderFactoryInstance.instance; + } + + public MetricsProvider getProvider(MetricsProviderConfig providerConfig) { + switch (providerConfig.getType()) { + case CLOUDWATCH: + return new CloudWatchMetricsProvider(providerConfig.getProperties()); + default: + return null; + } + } + + private static class MetricsProviderFactoryInstance { + public static final MetricsProviderFactory instance = new MetricsProviderFactory(); + } +} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsService.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsService.java new file mode 100644 index 00000000..4d179fec --- /dev/null +++ b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/MetricsService.java @@ -0,0 +1,214 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.exception.SdkServiceException; +import software.amazon.awssdk.services.sqs.SqsClient; +import software.amazon.awssdk.services.sqs.model.BatchResultErrorEntry; +import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequestEntry; +import software.amazon.awssdk.services.sqs.model.SendMessageBatchResponse; + +import java.net.HttpURLConnection; +import java.util.*; +import java.util.stream.Collectors; + +public class MetricsService { + + private static final Logger LOGGER = LoggerFactory.getLogger(MetricsService.class); + private static final Map CORS = Map.of("Access-Control-Allow-Origin", "*"); + private static final String AWS_REGION = System.getenv("AWS_REGION"); + private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); + private static final String METRICS_QUEUE = System.getenv("METRICS_QUEUE"); + private static final String METRICS_DLQ = System.getenv("METRICS_DLQ"); + private final MetricsDataAccessLayer dal; + private final SqsClient sqs; + + public MetricsService() { + this(new DefaultDependencyFactory()); + } + + public MetricsService(MetricServiceDependencyFactory init) { + if (Utils.isBlank(AWS_REGION)) { + throw new IllegalStateException("Missing required environment variable AWS_REGION"); + } + if (Utils.isBlank(SAAS_BOOST_ENV)) { + throw new IllegalStateException("Missing required environment variable SAAS_BOOST_ENV"); + } + LOGGER.info("Version Info: {}", Utils.version(this.getClass())); + this.dal = init.dal(); + this.sqs = init.sqs(); + } + + /** + * Create new new metric using the configured Metric Provider. Integration for POST /metrics endpoint + * @param event API Gateway proxy request event + * @param context Lambda function context + * @return HTTP Created on success + */ + public APIGatewayProxyResponseEvent putMetrics(APIGatewayProxyRequestEvent event, Context context) { + if (Utils.warmup(event)) { + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); + } + + Utils.MAPPER.enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + Utils.MAPPER.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false); + Utils.MAPPER.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false); + //Utils.logRequestEvent(event); + Metric[] metricsArray = Utils.fromJson(event.getBody(), Metric[].class); + if (metricsArray == null) { + return new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) + .withBody(Utils.toJson(Map.of("message", "Invalid request body"))); + } + List metrics = new ArrayList<>(Arrays.asList(metricsArray)); + if (metrics.isEmpty()) { + return new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) + .withBody(Utils.toJson(Map.of("message", "Invalid request body"))); + } else { + LOGGER.info("Enqueuing {} metrics for processing", metrics.size()); + try { + // Queue metrics for batch processing by our metrics provider + List batch = new ArrayList<>(); + try { + for (Metric metric : metrics) { + if (batch.size() < 10) { + batch.add(SendMessageBatchRequestEntry.builder() + .id(String.valueOf(metric.hashCode())) + .messageBody(Utils.toJson(metric)) + .build() + ); + } else { + // Batch has reached max size of 10, make the request + SendMessageBatchResponse response = sqs.sendMessageBatch(request -> request + .entries(batch) + .queueUrl(METRICS_QUEUE) + ); + if (response.hasFailed()) { + for (BatchResultErrorEntry error : response.failed()) { + LOGGER.error("Failed to enqueue metric {} {}", error.code(), error.message()); + } + } + // Clear the batch so we can fill it up for the next request + batch.clear(); + } + } + if (!batch.isEmpty()) { + // get the last batch + SendMessageBatchResponse response = sqs.sendMessageBatch(request -> request + .entries(batch) + .queueUrl(METRICS_QUEUE) + ); + if (response.hasFailed()) { + for (BatchResultErrorEntry error : response.failed()) { + LOGGER.error("Failed to enqueue metric {} {}", error.code(), error.message()); + } + } + } + } catch (SdkServiceException sqsError) { + LOGGER.error("sqs:SendMessageBatch error", sqsError); + LOGGER.error(Utils.getFullStackTrace(sqsError)); + throw sqsError; + } + + return new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_CREATED); + } catch (SdkServiceException sqsError) { + LOGGER.error("sqs:SendMessage error", sqsError); + LOGGER.error(Utils.getFullStackTrace(sqsError)); + throw sqsError; + } + } + } + + public SQSBatchResponse processMetricsQueue(SQSEvent event, Context context) { + + final List retry = new ArrayList<>(); + final List fatal = new ArrayList<>(); + + Utils.MAPPER.enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + Utils.MAPPER.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false); + Utils.MAPPER.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false); + List metrics = new ArrayList<>(); + for (SQSEvent.SQSMessage message : event.getRecords()) { + String messageId = message.getMessageId(); + String messageBody = message.getBody(); + Metric metric = Utils.fromJson(messageBody, Metric.class); + if (metric != null) { + metrics.add(metric); + } else { + LOGGER.error("Can't parse metric from message {}", messageId); + fatal.add(message); + } + } + MetricsProviderConfig providerConfig = dal.getProviderConfig(); + MetricsProvider provider = MetricsProviderFactory.getInstance().getProvider(providerConfig); + provider.api().putMetrics(metrics); + + if (!fatal.isEmpty()) { + LOGGER.info("Moving non-recoverable failures to DLQ"); + SendMessageBatchResponse dlq = sqs.sendMessageBatch(request -> request + .queueUrl(METRICS_DLQ) + .entries(fatal.stream() + .map(msg -> SendMessageBatchRequestEntry.builder() + .id(msg.getMessageId()) + .messageBody(msg.getBody()) + .build() + ) + .collect(Collectors.toList()) + ) + ); + LOGGER.info(dlq.toString()); + } + + return SQSBatchResponse.builder().withBatchItemFailures(retry).build(); + } + + interface MetricServiceDependencyFactory { + + SqsClient sqs(); + + MetricsDataAccessLayer dal(); + } + + private static final class DefaultDependencyFactory implements MetricServiceDependencyFactory { + + @Override + public SqsClient sqs() { + return Utils.sdkClient(SqsClient.builder(), SqsClient.SERVICE_NAME); + } + + @Override + public MetricsDataAccessLayer dal() { + return new MetricsDataAccessLayer(); + } + } + +} diff --git a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/QueryResult.java b/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/QueryResult.java deleted file mode 100644 index 0a9282ca..00000000 --- a/services/metrics-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/QueryResult.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import java.util.ArrayList; -import java.util.List; - -public class QueryResult { - - private String id; - private List metrics = new ArrayList<>(); - private List periods = new ArrayList<>(); - private List tenantTaskMaxCapacity = new ArrayList<>(); - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public List getPeriods() { - return List.copyOf(periods); - } - - public void setPeriods(List periods) { - this.periods = periods != null ? periods : new ArrayList<>(); - } - - public List getMetrics() { - return List.copyOf(metrics); - } - - public void setMetrics(List metrics) { - this.metrics = metrics != null ? metrics : new ArrayList<>(); - } - - public List getTenantTaskMaxCapacity() { - return List.copyOf(tenantTaskMaxCapacity); - } - - public void setTenantTaskMaxCapacity(List tenantTaskMaxCapacity) { - this.tenantTaskMaxCapacity = tenantTaskMaxCapacity != null ? tenantTaskMaxCapacity : new ArrayList<>(); - } - -} diff --git a/services/metrics-service/src/main/resources/spotbugs-exclude.xml b/services/metrics-service/src/main/resources/spotbugs-exclude.xml new file mode 100644 index 00000000..17059f38 --- /dev/null +++ b/services/metrics-service/src/main/resources/spotbugs-exclude.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AthenaTest.java b/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AthenaTest.java deleted file mode 100644 index ba76e01b..00000000 --- a/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AthenaTest.java +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.amazon.aws.partners.saasfactory.saasboost; -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; -import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; -import software.amazon.awssdk.services.athena.AthenaClient; -import software.amazon.awssdk.services.athena.model.QueryExecutionContext; -import software.amazon.awssdk.services.athena.model.ResultConfiguration; -import software.amazon.awssdk.services.athena.model.StartQueryExecutionRequest; -import software.amazon.awssdk.services.athena.model.StartQueryExecutionResponse; -import software.amazon.awssdk.services.athena.model.AthenaException; -import software.amazon.awssdk.services.athena.model.GetQueryExecutionRequest; -import software.amazon.awssdk.services.athena.model.GetQueryExecutionResponse; -import software.amazon.awssdk.services.athena.model.QueryExecutionState; -import software.amazon.awssdk.services.athena.model.GetQueryResultsRequest; -import software.amazon.awssdk.services.athena.model.GetQueryResultsResponse; -import software.amazon.awssdk.services.athena.model.ColumnInfo; -import software.amazon.awssdk.services.athena.model.Row; -import software.amazon.awssdk.services.athena.model.Datum; -import software.amazon.awssdk.services.athena.paginators.GetQueryResultsIterable; - -import java.util.List; - -public class AthenaTest { - -/** - * StartQueryExample - * ------------------------------------- - * This code shows how to submit a query to Athena for execution, wait till results - * are available, and then process the results. - */ - public static void main(String[] args) throws InterruptedException { - - // Build an Athena client - AthenaClient athenaClient = AthenaClient.builder() - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .build(); - /* .region(Region.US_WEST_2) - .build();*/ - long start = System.currentTimeMillis(); - - String queryExecutionId = submitAthenaQuery(athenaClient); - - waitForQueryToComplete(athenaClient, queryExecutionId); - - processResultRows(athenaClient, queryExecutionId); - System.out.println(("Executed in: " + (System.currentTimeMillis() - start))); - } - - /** - * Submits a query to Athena and returns the execution ID of the query. - */ - public static String submitAthenaQuery(AthenaClient athenaClient) { - - try { - // The QueryExecutionContext allows us to set the Database. - QueryExecutionContext queryExecutionContext = QueryExecutionContext.builder() - .database(ExampleConstants.ATHENA_DEFAULT_DATABASE).build(); - - // The result configuration specifies where the results of the query should go in S3 and encryption options - ResultConfiguration resultConfiguration = ResultConfiguration.builder() - // You can provide encryption options for the output that is written. - // .withEncryptionConfiguration(encryptionConfiguration) - .outputLocation(ExampleConstants.ATHENA_OUTPUT_BUCKET).build(); - - // Create the StartQueryExecutionRequest to send to Athena which will start the query. - StartQueryExecutionRequest startQueryExecutionRequest = StartQueryExecutionRequest.builder() - .queryString(ExampleConstants.QUERY1) - .queryExecutionContext(queryExecutionContext) - .resultConfiguration(resultConfiguration).build(); - - StartQueryExecutionResponse startQueryExecutionResponse = athenaClient.startQueryExecution(startQueryExecutionRequest); - return startQueryExecutionResponse.queryExecutionId(); - } catch (AthenaException e) { - Utils.getFullStackTrace(e); - throw e; - } - - } - - /** - * Wait for an Athena query to complete, fail or to be cancelled. This is done by polling Athena over an - * interval of time. If a query fails or is cancelled, then it will throw an exception. - */ - - public static void waitForQueryToComplete(AthenaClient athenaClient, String queryExecutionId) throws InterruptedException { - GetQueryExecutionRequest getQueryExecutionRequest = GetQueryExecutionRequest.builder() - .queryExecutionId(queryExecutionId).build(); - - GetQueryExecutionResponse getQueryExecutionResponse; - boolean isQueryStillRunning = true; - while (isQueryStillRunning) { - getQueryExecutionResponse = athenaClient.getQueryExecution(getQueryExecutionRequest); - String queryState = getQueryExecutionResponse.queryExecution().status().state().toString(); - if (queryState.equals(QueryExecutionState.FAILED.toString())) { - throw new RuntimeException("Query Failed to run with Error Message: " + getQueryExecutionResponse - .queryExecution().status().stateChangeReason()); - } else if (queryState.equals(QueryExecutionState.CANCELLED.toString())) { - throw new RuntimeException("Query was cancelled."); - } else if (queryState.equals(QueryExecutionState.SUCCEEDED.toString())) { - isQueryStillRunning = false; - } else { - // Sleep an amount of time before retrying again. - Thread.sleep(ExampleConstants.SLEEP_AMOUNT_IN_MS); - } - //System.out.println("Current Status is: " + queryState); - } - } - - /** - * This code calls Athena and retrieves the results of a query. - * The query must be in a completed state before the results can be retrieved and - * paginated. The first row of results are the column headers. - */ - public static void processResultRows(AthenaClient athenaClient, String queryExecutionId) { - - try { - - /* - 1. Counts by PATH with status 200. Need to bucket by - 2. Latency by Paths - */ - GetQueryResultsRequest getQueryResultsRequest = GetQueryResultsRequest.builder() - // Max Results can be set but if its not set, - // it will choose the maximum page size - // As of the writing of this code, the maximum value is 1000 - // .withMaxResults(1000) - .queryExecutionId(queryExecutionId).build(); - - GetQueryResultsIterable getQueryResultsResults = athenaClient.getQueryResultsPaginator(getQueryResultsRequest); - - for (GetQueryResultsResponse result : getQueryResultsResults) { - List columnInfoList = result.resultSet().resultSetMetadata().columnInfo(); - List results = result.resultSet().rows(); - processRow(results, columnInfoList); - } - - } catch (AthenaException e) { - e.printStackTrace(); - System.exit(1); - } - } - - private static void processRow(List row, List columnInfoList) { - - //Write out the data - boolean first = true; - for (Row myRow : row) { - if (first) { - first = false; - continue; - } - List allData = myRow.data(); - int i = 0; - for (Datum data : allData) { - ColumnInfo colInfo = columnInfoList.get(i); - colInfo.type(); - System.out.println("The value of the column " + colInfo.name() + " of type: " + colInfo.type() + " is " + data.varCharValue()); - i++; - } - } - } -} - diff --git a/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CloudWatchApiTest.java b/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CloudWatchApiTest.java new file mode 100644 index 00000000..0cc1d4e2 --- /dev/null +++ b/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CloudWatchApiTest.java @@ -0,0 +1,33 @@ +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.junit.jupiter.api.Test; +import software.amazon.cloudwatchlogs.emf.exception.InvalidTimestampException; +import software.amazon.cloudwatchlogs.emf.util.Validator; + +import static org.junit.jupiter.api.Assertions.*; + +public class CloudWatchApiTest { + + @Test + public void testEmfTimestamp() { + + Utils.MAPPER.enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + Utils.MAPPER.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false); + Utils.MAPPER.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false); + + + Metric[] metricsArray = Utils.fromJson(getClass().getResourceAsStream("/metrics2.json"), Metric[].class); + for (int i = 0; i < metricsArray.length; i++) { + Metric metric = metricsArray[i]; + try { + Validator.validateTimestamp(metric.getTimestamp()); + } catch (InvalidTimestampException ite) { + System.out.println("Epoch " + metric.getTimestamp()); + System.out.println(Utils.toJson(metricsArray[i])); + break; + } + } + } +} diff --git a/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ExampleConstants.java b/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ExampleConstants.java deleted file mode 100644 index ecbe4025..00000000 --- a/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ExampleConstants.java +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.amazon.aws.partners.saasfactory.saasboost; - -public class ExampleConstants { - public static final int CLIENT_EXECUTION_TIMEOUT = 100000; - public static final String ATHENA_OUTPUT_BUCKET = "s3://some-test-bucket/query-results/"; //change the bucket name to match your environment - // This example demonstrates how to query a table with a CSV For information, see - //https://docs.aws.amazon.com/athena/latest/ug/work-with-data.html - public static final String ATHENA_SAMPLE_QUERY = "SELECT request_url,\n" + - " target_status_code,\n" + - " date_trunc('hour', (date_parse(time, '%Y-%m-%dT%H:%i:%s.%fZ'))) as time_hour,\n" + - " count(1) AS count,\n" + - " avg(target_processing_time) AS avg_time,\n" + - " max(target_processing_time) AS max_time\n" + - "FROM alb_logs\n" + - "where time > '2020-07-07T14'\n" + - " and target_status_code = '200'\n" + - "GROUP BY request_url,\n " + - " target_status_code,\n" + - " date_trunc('hour', (date_parse(time, '%Y-%m-%dT%H:%i:%s.%fZ')))\n" + - ";"; //change the Query statement to match your environment - - public static final String ATHENA_PATH_COUNT_QUERY = "SELECT date_trunc('hour', (date_parse(time, '%Y-%m-%dT%H:%i:%s.%fZ'))) AS time_hour,\n" + - "concat(url_extract_path(request_url), '+',request_verb) as url,\n" + - "count(1) as count\n" + - "FROM alb_logs\n" + - "WHERE target_status_code = '200'\n" + - "GROUP BY concat(url_extract_path(request_url),'+',request_verb),\n" + - "date_trunc('hour', (date_parse(time, '%Y-%m-%dT%H:%i:%s.%fZ'))) \n" + - "order by 1;"; - public static final long SLEEP_AMOUNT_IN_MS = 500; - public static final String ATHENA_DEFAULT_DATABASE = "saas-boost-alb-log"; //Change the database to match your database - public static final String QUERY1 = "SELECT\n" + - "concat(url_extract_path(request_url), '+',request_verb) as url,\n" + - "count(1) as request_count\n" + - "FROM alb_logs\n" + - " where target_status_code = '200'\n" + - "and time >= '2020-07-09T23:11:33.827Z' and time <= '2020-07-10T23:11:33.827Z' \n" + - "GROUP BY concat(url_extract_path(request_url),'+',request_verb)\n" + - "order by 2 desc\n" + - "limit 10;"; - -} - diff --git a/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/MetricContextTest.java b/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/MetricContextTest.java new file mode 100644 index 00000000..0f25367c --- /dev/null +++ b/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/MetricContextTest.java @@ -0,0 +1,43 @@ +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class MetricContextTest { + + @Test + public void testDefaultContextKeys() { + MetricContext context = new MetricContext(); + assertTrue(context.containsKey("TenantId")); + assertTrue(context.containsKey("UserId")); + assertTrue(context.containsKey("Action")); + assertTrue(context.containsKey("Application")); + + String json = " {\n" + + " \"name\": \"Jobs Scheduled\",\n" + + " \"timestamp\": 1695980548324,\n" + + " \"measure\": {\n" + + " \"type\": \"count\",\n" + + " \"value\": 1\n" + + " },\n" + + " \"context\": {\n" + + " \"TenantId\": \"deb0451a-5e20-4415-a9f2-9d9b941bb1f1\",\n" + + " \"UserId\": \"48517b4c-7169-421f-8187-138b6e109ea5\",\n" + + " \"Application\": \"Job Scheduler Service\",\n" + + " \"Action\": \"scheduleJob\"\n" + + " }\n" + + " }"; + Utils.MAPPER.enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + Utils.MAPPER.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false); + Utils.MAPPER.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false); + Metric m = Utils.fromJson(json, Metric.class); + System.out.println(m.getTimestamp()); + String json2 = Utils.toJson(m); + System.out.println(json2); + Metric m2 = Utils.fromJson(json2, Metric.class); + System.out.println(Utils.toJson(m2)); + } +} diff --git a/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/Test.java b/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/Test.java deleted file mode 100644 index e3d01f1f..00000000 --- a/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/Test.java +++ /dev/null @@ -1,436 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.amazon.aws.partners.saasfactory.saasboost; - - - -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.text.SimpleDateFormat; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.*; - - -public class Test { - public String getPropValues() throws IOException { - String result = ""; - InputStream inputStream = null; - try { - Properties prop = new Properties(); - String propFileName = "git.properties"; - - inputStream = getClass().getClassLoader().getResourceAsStream(propFileName); - - if (inputStream != null) { - prop.load(inputStream); - } else { - throw new FileNotFoundException("property file '" + propFileName + "' not found in the classpath"); - } - - Date time = new Date(System.currentTimeMillis()); - - // get the property value and print it out - String tag = prop.getProperty("git.commit.id.describe"); - String commitTime = prop.getProperty("git.commit.time"); - result = "Version: " + tag + ", Commit time: " + commitTime; - -// System.out.println(result + "\nProgram Ran on " + time ); - } catch (Exception e) { - System.out.println("Exception: " + e); - } finally { - if (null != inputStream) { - inputStream.close(); - } - } - return result; - } - - public static void main(String[] args) throws Exception { - - final Instant curDateTime = Instant.ofEpochMilli(new Date().getTime()); - LocalDateTime localStartDateTime = LocalDateTime.ofInstant(curDateTime.now(), ZoneId.systemDefault()); - System.out.println("local time: " + localStartDateTime); - System.out.println("minus 60 minutes: " + localStartDateTime.minusMinutes(60)); - System.out.println("minus -60 minutes: " + localStartDateTime.minusMinutes(-60)); - -//TEST THE METRIC SERVICE LAMBDA -/* String body = "{\"tenants\":[\"5fbd498c\",\"73ecc895\"],\"startDate\":\"2020-06-05T23:00:00Z\",\"endDate\"" + - ":\"2020-06-06T01:00:00Z\",\"metricName\":\"CPUUtilization\"," + - "\"nameSpace\":\"AWS/ECS\",\"stat\":\"Average\",\"period\":300,\"topTenantList\":false," + - "\"statsMap\":true,\"" + - "\"tenantDimensionMap\":{\"73ecc895\":[{\"name\":\"ClusterName\",\"value\":\"tenant-73ecc895\"}," + - "{\"name\":\"ServiceName\",\"value\":\"tenant-73ecc895\"}],\"5fbd498c\":[{\"name\":\"ClusterName\"," + - "\"value\":\"tenant-5fbd498c\"},{\"name\":\"ServiceName\",\"value\":\"tenant-5fbd498c\"}]}}";*/ - - - // EnvironmentVariableCredentialsProvider evp = EnvironmentVariableCredentialsProvider.create(); - // S3Client s3Client = S3Client.builder().credentialsProvider(evp).build(); -/* S3Client s3 = S3Client.builder() - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .build();*/ - MetricService ms = new MetricService(); - System.out.println("write files for request count"); - //ms.publishRequestCountMetrics(null, null, null); - - // Test test = new Test(); - //test.getPropValues(); - int x= 1; - if (x == 2) { - System.exit(0); - } - - - -// String body1 = "{\"tenants\":[\"5fbd498c\",\"73ecc895\"],\"startDate\":\"2020-06-05T23:00:00Z\",\"endDate\":\"2020-06-06T01:00:00Z\",\"metricName\":\"CPUUtilization\",\"nameSpace\":\"AWS/ECS\",\"stat\":\"Average\",\"period\":300,\"top10\":false,\"tenantDimensionMap\":{\"73ecc895\":[{\"name\":\"ClusterName\",\"value\":\"tenant-73ecc895\"},{\"name\":\"ServiceName\",\"value\":\"tenant-73ecc895\"}],\"5fbd498c\":[{\"name\":\"ClusterName\",\"value\":\"tenant-5fbd498c\"},{\"name\":\"ServiceName\",\"value\":\"tenant-5fbd498c\"}]}}"; - String body1 = "{\"id\":\"query1\",\"startDate\":\"2020-09-01T02:00:00Z\",\"endDate\":\"2020-09-1T03:00:00Z\"," + - "\"tenants\":[\"481bf44b-56a1-4a1e-8b39-838c03c1113f\"]," + - "\"singleTenant\":true," + - "\"timeRangeName\":\"HOUR_24\"," + - "\"dimensions\":[{\"metricName\":\"RequestCount\",\"nameSpace\":\"AWS/ApplicationELB\"}," + - "{\"metricName\":\"HTTPCode_Target_4XX_Count\",\"nameSpace\":\"AWS/ApplicationELB\"}]" + - ",\"stat\":\"Sum\",\"period\":300,\"topTenants\":true}}"; - - body1 = "{\n" + - " \"id\": \"albstats\",\n" + - " \"timeRangeName\": \"HOUR_2\",\n" + - " \"stat\": \"Average\",\n" + - " \"dimensions\": [\n" + - " {\n" + - " \"metricName\": \"HTTPCode_Target_4XX_Count\",\n" + - " \"nameSpace\": \"AWS/ApplicationELB\"\n" + - " }\n" + - " ],\n" + - " \"topTenants\": false,\n" + - " \"statsMap\": false,\n" + - " \"tenants\":[\"481bf44b-56a1-4a1e-8b39-838c03c1113f\"],\n" + - " \"singleTenant\": true\n" + - "}"; - - - - - Map testMap = new HashMap(); - testMap.put("body", body1); - -/* - APIGatewayProxyResponseEvent responseEvent = ms.queryMetrics(testMap, null); - System.out.println("Lambda response for body1 query: " + responseEvent.toString()); - - if (null != responseEvent) { - System.exit(0); - } -*/ - - String path = "{\n" + - " \"metric\": \"PATH_REQUEST_COUNT\",\n" + - " \"timerange\": \"HOUR_24\"\n" + - " }"; - - Map valMap = new HashMap<>(); - valMap.put("metric", "PATH_REQUEST_COUNT"); - valMap.put("timerange", "HOUR_24"); - valMap.put("tenantId", "c18e5a42-c54d-42ef-b108-73f3e9c8f917"); - - ms = new MetricService(); - testMap = new HashMap(); - testMap.put("pathParameters", valMap); - - APIGatewayProxyResponseEvent responseEvent = ms.queryAccessLogs(testMap, null); - System.out.println("Lambda response for pathParam query: " + responseEvent.toString()); - //System.exit(1); - - - String body = "{\"id\":\"query1\",\"startDate\":\"2020-08-10T23:00:00Z\",\"endDate\":\"2020-08-11T01:00:00Z\"," + - //"\"tenants\":[\"tenant-5fbd498c\"]," + - "\"timeRangeName\":\"TODAY\"," + - "\"tzOffset\":\"-420\"," + - "\"dimensions\":[{\"metricName\":\"CPUUtilization\",\"nameSpace\":\"AWS/ECS\"},{\"metricName\":\"MemoryUtilization\",\"nameSpace\":\"AWS/ECS\"}]," + - "\"stat\":\"Average\"," + "" + - "\"period\":300," + - "\"topTenants\":true," + - "\"statsMap\":false," + - "\"tenantTaskMaxCapacity\":true}}"; - MetricQuery query1 = Utils.fromJson(body, MetricQuery.class); - - //test metric service - MetricServiceDAL dal = new MetricServiceDAL(); - List result = dal.queryMetrics(query1); - System.out.println("CPUUtilization: " + Utils.toJson(result)); - - //Test out a single tenant -/* body = "{\"id\":\"query1\",\"startDate\":\"2020-06-05T23:00:00Z\",\"endDate\":\"2020-06-06T01:00:00Z\"," + - "\"singleTenant\":true," + - "\"tenants\":[\"ae928191-1555-408c-be76-1d0fceabd679\"]," + - "\"dimensions\":[{\"metricName\":\"CPUUtilization\",\"nameSpace\":\"AWS/ECS\"},{\"metricName\":\"MemoryUtilization\",\"nameSpace\":\"AWS/ECS\"}]," + - "\"stat\":\"Average\",\"period\":300,\"statsMap\":true}}";*/ - - body = "{\n" + - " \"id\": \"albstats\",\n" + - " \"timeRangeName\": \"DAY_7\",\n" + - " \"stat\": \"Average\",\n" + - " \"dimensions\": [\n" + - " {\n" + - " \"metricName\": \"RequestCount\",\n" + - " \"nameSpace\": \"AWS/ApplicationELB\"\n" + - " }\n" + - " ],\n" + - " \"topTenants\": false,\n" + - " \"statsMap\": false,\n" + - " \"tenants\":[\"ae928191-1555-408c-be76-1d0fceabd679\"],\n" + - " \"singleTenant\": true\n" + - "}"; - query1 = Utils.fromJson(body, MetricQuery.class); - - //test metric service - dal = new MetricServiceDAL(); - result = dal.queryTenantMetrics(query1); - //List result = dal.queryMetrics(query1); - System.out.println("CPUUtilization for Single Tenant: " + Utils.toJson(result)); - -/* if (null != start1) { - System.exit(0); - }*/ - - - - MetricQuery query = new MetricQuery(); - - //date range - Instant start = Instant.ofEpochMilli(new Date().getTime()); - start = Instant.parse("2020-06-05T23:00:00Z"); - query.setStartDate(start); - Instant endDate = Instant.now(); - endDate = Instant.parse("2020-06-06T01:00:00Z"); - query.setEndDate(endDate); - - //metric - MetricQuery.Dimension md = new MetricQuery.Dimension("AWS/ECS", "MemoryUtilization"); - List mdList = new ArrayList<>(); - mdList.add(md); -/* - query.setNameSpace("AWS/ECS"); - query.getDimensions().add("CPUUtilization"); - query.getDimensions().add("MemoryUtilization"); -*/ - query.setDimensions(mdList); - //query.setStat("SampleCount"); - query.setStat("Average"); -/* query.addTenant("5fbd498c"); - query.addTenant("73ecc895");*/ - query.setPeriod(43200); - query.setTimeRangeName("HOUR_2"); - System.out.println("Query for AWS/ECS JSON: " + Utils.toJson(query)); - - // result = dal.queryMetrics(query); - // System.out.println("CPUUtilization: " + Utils.toJson(result)); - - - //metric - md = new MetricQuery.Dimension("AWS/Usage", "ResourceCount"); - mdList = new ArrayList<>(); - mdList.add(md); -/* - query.setNameSpace("AWS/ECS"); - query.getDimensions().add("CPUUtilization"); - query.getDimensions().add("MemoryUtilization"); -*/ - query.setDimensions(mdList); - //query.setStat("SampleCount"); - query.setStat("Maximum"); -/* query.addTenant("5fbd498c"); - query.addTenant("73ecc895");*/ - query.setPeriod(43200); - System.out.println("Query for AWS/Usage JSON: " + Utils.toJson(query)); - - result = dal.queryMetrics(query); - System.out.println("EC2 ResourceCount: " + Utils.toJson(result)); - - //For P90 or aggregrate, need to go through results and calculate values by time slot - // Build a list for each time entry - // find the Pxx for each list - - - //For Top 10, build a list for each time slot - // Sort list for top 10. Return Tenant Id and Value pairs -// for (Map.Entry> entry : toSortSet.entrySet()) { -// List sbValues = entry.getValue(); -// -// PriorityQueue heap = new PriorityQueue(sbValues.size()); -// heap.addAll(sbValues); -// List topElements = (1..k).collect{heap.poll()}; -// } - - // System.out.println("AWS/ECS JSON: " + Utils.toJson(results)); - - query = new MetricQuery(); - //metric - md = new MetricQuery.Dimension("AWS/ApplicationELB", "HTTPCode_Target_4XX_Count"); - mdList = new ArrayList<>(); - mdList.add(md); - query.setDimensions(mdList); - - /* - query.setNameSpace("AWS/ApplicationELB"); - query.getDimensions().add("RequestCount"); - query.getDimensions().add("HTTPCode_Target_4XX_Count"); - - dset = new LinkedHashSet(); - dim = new MetricQuery.Dimension("LoadBalancer", "app/tenant-5fbd498c/63f1eedfca597fcc"); - dset.add(dim); - query.getTenantDimensionMap().put("5fbd498c", dset); - - dset = new LinkedHashSet(); - dim = new MetricQuery.Dimension("LoadBalancer", "app/tenant-73ecc895/94c0cac0a3c0da46"); - dset.add(dim); - query.getTenantDimensionMap().put("73ecc895", dset);*/ - - -/* - Instant start = Instant.ofEpochMilli(new Date().getTime()); - start = Instant.parse("2020-06-04T12:00:00Z"); - Instant endDate = Instant.now(); - endDate = Instant.parse("2020-06-04T23:55:35Z"); -*/ - - query.setStartDate(start); - query.setEndDate(endDate); - //query.setStat("SampleCount"); - query.setStat("Sum"); - query.addTenant("5fbd498c"); - query.addTenant("73ecc895"); - query.setPeriod(60); - query.setStatsMap(true); - query.setTopTenants(true); - - System.out.println("Query for ALB JSON: " + Utils.toJson(query)); - result = dal.queryMetrics(query); -/* for (Metric metric : results) { - System.out.println(metric); - //System.out.println(("Tenant: # of values:" + metric.getTimeValMap().size())); - for (Map.Entry> entry : metric.getTimeValMap().entrySet()) { - System.out.println("time: " + entry.getKey() + ", num of values: " + entry.getValue().size()); - Iterator itr2=entry.getValue().iterator(); - while (itr2.hasNext()) { - System.out.println(itr2.next()); - } - } - } */ -/* - System.out.println("ALB Requests: JSON: " + Utils.toJson(result)); - - query = new MetricQuery(); - query.setNameSpace("AWS/ApplicationELB"); - query.getDimensions().add("HTTPCode_Target_4XX_Count"); - - dset = new LinkedHashSet(); - dim = new MetricQuery.Dimension("LoadBalancer", "app/tenant-5fbd498c/63f1eedfca597fcc"); - dset.add(dim); - query.getTenantDimensionMap().put("5fbd498c", dset); - - dset = new LinkedHashSet(); - dim = new MetricQuery.Dimension("LoadBalancer", "app/tenant-73ecc895/94c0cac0a3c0da46"); - dset.add(dim); - query.getTenantDimensionMap().put("73ecc895", dset);*/ - - -/* -// Instant start = Instant.ofEpochMilli(new Date().getTime()); -// start = Instant.parse("2020-06-04T12:00:00Z"); -// Instant endDate = Instant.now(); -// endDate = Instant.parse("2020-06-04T23:55:35Z"); - - - query.setStartDate(start); - query.setEndDate(endDate); - //query.setStat("SampleCount"); - query.setStat("Sum"); - query.addTenant("5fbd498c"); - query.addTenant("73ecc895"); - query.setPeriod(60); - query.setTopTenants(false); - query.setStatsMap(true); - - System.out.println("Query for ALB JSON: " + Utils.toJson(query)); - result = dal.queryMetrics(query); -/* for (Metric metric : results) { - System.out.println(metric); - //System.out.println(("Tenant: # of values:" + metric.getTimeValMap().size())); - for (Map.Entry> entry : metric.getTimeValMap().entrySet()) { - System.out.println("time: " + entry.getKey() + ", num of values: " + entry.getValue().size()); - Iterator itr2=entry.getValue().iterator(); - while (itr2.hasNext()) { - System.out.println(itr2.next()); - } - } - }*/ - - System.out.println("4XX JSON: " + Utils.toJson(result)); - // double myPercentile90 = Quantiles.percentiles().index(90).compute(values); - // double quartiles = Quantiles.quartiles().index(90).compute(values); - - // System.out.println("Google p90 " + myPercentile90); - // System.out.println("Google quartiles " + quartiles); - // System.out.println("p90 " + StatUtils.percentile(values, 90)); - // assertEquals(StatUtils.percentile(values, 95), percentile95.summarize(c), 0.0001); - // assertEquals(StatUtils.percentile(values, 99), percentile99.summarize(c), 0.0001); -// - // assertEquals(10, countUnique.summarize(c), 0.0001); - - //Test priority queue - /* PriorityQueue p1 = new PriorityQueue(10); - for (int i = 1; i < 10 ; i++) { - SBValue val = new SBValue(); - val.value = i * 10; - val.tenantId = String.format("Tenant%s",i); - p1.add(val); - } - - Iterator itr2 = p1.iterator(); - while (itr2.hasNext()) { - SBValue val1 = itr2.next(); - System.out.println(val1); - } - - System.out.println("queue: p1 " + p1.toString()); - SBValue val = new SBValue(); - val.value = 15; - val.tenantId = String.format("Tenant15"); - SBValue sb = (SBValue) p1.peek(); - if (val.value > sb.value) { - //this needs to go into the Priority queue - //remove the least item and add this one - p1.remove(); - p1.add(val); - } - - itr2 = p1.iterator(); - while (itr2.hasNext()) { - SBValue val1 = itr2.next(); - System.out.println(val1); - } - -*/ - - } - - -} \ No newline at end of file diff --git a/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TestAutoScale.java b/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TestAutoScale.java deleted file mode 100644 index 27131a7c..00000000 --- a/services/metrics-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TestAutoScale.java +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.amazon.aws.partners.saasfactory.saasboost; - -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; -import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; -import software.amazon.awssdk.services.applicationautoscaling.ApplicationAutoScalingClient; -import software.amazon.awssdk.services.applicationautoscaling.ApplicationAutoScalingClientBuilder; -import software.amazon.awssdk.services.applicationautoscaling.model.*; -import software.amazon.awssdk.services.cloudwatch.CloudWatchClient; - -import java.util.ArrayList; -import java.util.List; - -public class TestAutoScale { - - // static ApplicationAutoScalingClient aaClient = (ApplicationAutoScalingClient) ApplicationAutoScalingClient.builder() ; - static ApplicationAutoScalingClient aaClient1 = ApplicationAutoScalingClient.builder() - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .build(); - - - - public static void main(String args[]) { - - String nextToken = null; - final ServiceNamespace ns = ServiceNamespace.ECS; - final ScalableDimension ecsTaskCount = ScalableDimension.ECS_SERVICE_DESIRED_COUNT; - List resourceIds = new ArrayList<>(); - resourceIds.add("service/tenant-5fbd498c/tenant-5fbd498c"); - resourceIds.add("service/tenant-73ecc895/tenant-73ecc895"); - - // Verify that the target was created - do { - DescribeScalableTargetsRequest dscRequest = DescribeScalableTargetsRequest.builder() - .serviceNamespace(ns) - .scalableDimension(ecsTaskCount) - .resourceIds(resourceIds) - .nextToken(nextToken) - .build(); - try { - long start = System.currentTimeMillis(); - DescribeScalableTargetsResponse resp = aaClient1.describeScalableTargets(dscRequest); - nextToken = resp.nextToken(); - System.out.println("DescribeScalableTargets result in " + (System.currentTimeMillis() - start) + " ms, : "); - System.out.println(resp); - List targets = resp.scalableTargets(); - for (ScalableTarget target : targets) { - ScalableDimension dim = target.scalableDimension(); - String[] id = target.resourceId().split("/"); - System.out.println("Dim: " + dim + ", MaxCapacity: " + target.maxCapacity() + ", resource: " + id[2]); - } - //System.out.println(resp.scalableTargets()); - } catch (Exception e) { - System.err.println("Unable to describe scalable target: "); - System.err.println(e.getMessage()); - } - } while (nextToken != null && !nextToken.isEmpty()); - - System.out.println(); - } - -} - - diff --git a/services/metrics-service/src/test/resources/metrics.json b/services/metrics-service/src/test/resources/metrics.json new file mode 100644 index 00000000..75cc7860 --- /dev/null +++ b/services/metrics-service/src/test/resources/metrics.json @@ -0,0 +1 @@ +[{"name": "Jobs", "timestamp": 1698987633032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "f580fc99-fb5e-404f-8ba4-10363e0a9a7e", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698987633032, "measure": {"type": "total", "value": 102, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "f580fc99-fb5e-404f-8ba4-10363e0a9a7e", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698961575032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "121ae5bb-1417-4764-8ce3-4a3d435e860e", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698961575032, "measure": {"type": "total", "value": 110, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "121ae5bb-1417-4764-8ce3-4a3d435e860e", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698959394032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "c158b982-f7eb-47fa-834a-0501cbb4aa7f", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698959394032, "measure": {"type": "total", "value": 49, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "c158b982-f7eb-47fa-834a-0501cbb4aa7f", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698961555032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "c926d47e-9aa9-429e-9c0e-9a0d77aed1db", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698961555032, "measure": {"type": "total", "value": 351, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "c926d47e-9aa9-429e-9c0e-9a0d77aed1db", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698962231032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "448f61fe-4f60-4180-918e-fec8699629d7", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698962231032, "measure": {"type": "total", "value": 228, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "448f61fe-4f60-4180-918e-fec8699629d7", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698960661032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "2f5879be-1784-496b-9954-a9961ca542cf", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698960661032, "measure": {"type": "total", "value": 568, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "2f5879be-1784-496b-9954-a9961ca542cf", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698960629032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "ef6eb59d-d944-47c4-9802-ccd86fd86bbb", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698960629032, "measure": {"type": "total", "value": 74, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "ef6eb59d-d944-47c4-9802-ccd86fd86bbb", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698960295032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "ba65da84-28c7-401a-8904-1eda79d21445", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698960295032, "measure": {"type": "total", "value": 203, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "ba65da84-28c7-401a-8904-1eda79d21445", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698959178032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "2bed1fe8-3920-4bd6-8f88-fa23b94251f5", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698959178032, "measure": {"type": "total", "value": 466, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "2bed1fe8-3920-4bd6-8f88-fa23b94251f5", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698961823032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "01e10ceb-7dc1-40b7-9b25-f7b8ff31a518", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698961823032, "measure": {"type": "total", "value": 480, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "01e10ceb-7dc1-40b7-9b25-f7b8ff31a518", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698962134032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "7dc65d72-842a-4115-a396-2aed0166adb1", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698962134032, "measure": {"type": "total", "value": 408, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "7dc65d72-842a-4115-a396-2aed0166adb1", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698941325032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "2a97678e-a32a-4971-8522-ae962839fb78", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698941325032, "measure": {"type": "total", "value": 56, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "2a97678e-a32a-4971-8522-ae962839fb78", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698970981032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "5f864c07-9292-4d61-8d00-b73745f33c63", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698970981032, "measure": {"type": "total", "value": 535, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "5f864c07-9292-4d61-8d00-b73745f33c63", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698970807032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "5ca831f5-2242-4e93-b709-95b58c720ffd", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698970807032, "measure": {"type": "total", "value": 223, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "5ca831f5-2242-4e93-b709-95b58c720ffd", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698969900032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "af3ccd3c-642c-4ba0-a9be-8e98f2414d9c", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698969900032, "measure": {"type": "total", "value": 257, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "af3ccd3c-642c-4ba0-a9be-8e98f2414d9c", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698970002032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "440aaedf-fbe1-441f-b0cb-3921a149bbd2", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698970002032, "measure": {"type": "total", "value": 96, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "440aaedf-fbe1-441f-b0cb-3921a149bbd2", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698972410032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "a46f9002-8f72-46d5-bade-badd90c46bea", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698972410032, "measure": {"type": "total", "value": 155, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "a46f9002-8f72-46d5-bade-badd90c46bea", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698969634032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "5d7dd77d-1c4d-4023-98e3-ceaac110ff86", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698969634032, "measure": {"type": "total", "value": 405, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "5d7dd77d-1c4d-4023-98e3-ceaac110ff86", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698969873032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "9f88f554-cc9a-4131-8c9f-d543465acdf5", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698969873032, "measure": {"type": "total", "value": 476, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "9f88f554-cc9a-4131-8c9f-d543465acdf5", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698972152032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "646eaac6-436e-4e0b-96f1-e2413a81f372", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698972152032, "measure": {"type": "total", "value": 80, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "646eaac6-436e-4e0b-96f1-e2413a81f372", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698971058032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "e2df4983-c01b-4367-b039-c8c15dcf35df", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698971058032, "measure": {"type": "total", "value": 176, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "e2df4983-c01b-4367-b039-c8c15dcf35df", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698970536032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "db39cc96-d81d-4dd9-ab6e-ceee3b1918ad", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698970536032, "measure": {"type": "total", "value": 426, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "db39cc96-d81d-4dd9-ab6e-ceee3b1918ad", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698928812032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "a25d7cc3-8627-4fd3-b7f9-8eca06f62578", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698928812032, "measure": {"type": "total", "value": 499, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "a25d7cc3-8627-4fd3-b7f9-8eca06f62578", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698929224032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "5fea6f32-b781-4f59-9321-9fa2477e2df4", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698929224032, "measure": {"type": "total", "value": 255, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "5fea6f32-b781-4f59-9321-9fa2477e2df4", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698928977032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "8e9b28fe-2481-48d2-9f32-dbf0325fb126", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698928977032, "measure": {"type": "total", "value": 29, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "8e9b28fe-2481-48d2-9f32-dbf0325fb126", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698929735032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "84374017-6d76-4abb-a451-8cc7d3db92d5", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698929735032, "measure": {"type": "total", "value": 365, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "84374017-6d76-4abb-a451-8cc7d3db92d5", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698929214032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "6ff28732-b2f2-42b7-b87f-4808fc821873", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698929214032, "measure": {"type": "total", "value": 6, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "6ff28732-b2f2-42b7-b87f-4808fc821873", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698927549032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "e4d5c61d-ca6f-456a-a442-dea52a39ce23", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698927549032, "measure": {"type": "total", "value": 540, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "e4d5c61d-ca6f-456a-a442-dea52a39ce23", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698926716032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "33ac8812-2eb6-4b76-8c43-a1385a6e9370", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698926716032, "measure": {"type": "total", "value": 277, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "33ac8812-2eb6-4b76-8c43-a1385a6e9370", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698928870032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "a863fb67-2966-438b-a475-b2641ae96a66", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698928870032, "measure": {"type": "total", "value": 80, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "a863fb67-2966-438b-a475-b2641ae96a66", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698929684032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "be0f598d-4f32-4ef1-aaec-be9b81a85b65", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698929684032, "measure": {"type": "total", "value": 240, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "be0f598d-4f32-4ef1-aaec-be9b81a85b65", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698927988032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "bd45b226-65c4-4c2c-acb6-2a0d4bf23af1", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698927988032, "measure": {"type": "total", "value": 423, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "bd45b226-65c4-4c2c-acb6-2a0d4bf23af1", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698946493032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "5cf26cdf-2208-4262-bc7c-4e9c4ced6d6a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698946493032, "measure": {"type": "total", "value": 181, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "5cf26cdf-2208-4262-bc7c-4e9c4ced6d6a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698945670032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "39a3932f-53a9-4e61-9088-a32f0720aed3", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698945670032, "measure": {"type": "total", "value": 292, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "39a3932f-53a9-4e61-9088-a32f0720aed3", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698944581032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "a85ad4c4-3c30-47bf-bfef-58a99e07d65d", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698944581032, "measure": {"type": "total", "value": 36, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "a85ad4c4-3c30-47bf-bfef-58a99e07d65d", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698947921032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "315a4993-45b5-4a62-bc90-4a36057e8899", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698947921032, "measure": {"type": "total", "value": 309, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "315a4993-45b5-4a62-bc90-4a36057e8899", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698945217032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "52b01dac-e932-456e-8831-a7d3cffa6fbf", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698945217032, "measure": {"type": "total", "value": 75, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "52b01dac-e932-456e-8831-a7d3cffa6fbf", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698947970032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "30bb1ffc-a0e9-42ea-bcbe-691ed552ee71", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698947970032, "measure": {"type": "total", "value": 351, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "30bb1ffc-a0e9-42ea-bcbe-691ed552ee71", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698977943032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "fe5500ed-c81f-4bbe-b917-2d40225ac7d8", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698977943032, "measure": {"type": "total", "value": 325, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "fe5500ed-c81f-4bbe-b917-2d40225ac7d8", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698979182032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "ec3173a7-43f4-4b90-b1b3-c1d1525432e4", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698979182032, "measure": {"type": "total", "value": 453, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "ec3173a7-43f4-4b90-b1b3-c1d1525432e4", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698986679032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "2454fe13-2732-4a7e-b29b-813dccc4c67a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698986679032, "measure": {"type": "total", "value": 82, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "b3c2fd5b-861a-4bdc-9747-3b0f42b128e4", "RequestId": "2454fe13-2732-4a7e-b29b-813dccc4c67a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698986544032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "1084abd1-f63f-45cb-9f57-d752c480140b", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698986544032, "measure": {"type": "total", "value": 284, "unit": "seconds"}, "context": {"TenantId": "89466af2-901b-4afa-ae83-9c5a6e80bca1", "UserId": "376ffb95-c6c4-47e3-b447-19f67ef27b45", "RequestId": "1084abd1-f63f-45cb-9f57-d752c480140b", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698988441032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "5e40d182-1aab-40da-9fac-95dd54810438", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698988441032, "measure": {"type": "total", "value": 544, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "5e40d182-1aab-40da-9fac-95dd54810438", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698989074032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "6e9f129b-1ee2-4098-ba5b-e04dcac75b90", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698989074032, "measure": {"type": "total", "value": 248, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "6e9f129b-1ee2-4098-ba5b-e04dcac75b90", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698989670032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "9d1dd8be-eb90-4030-8e0a-690ca2703333", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698989670032, "measure": {"type": "total", "value": 340, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "9d1dd8be-eb90-4030-8e0a-690ca2703333", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698989467032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "1dc1fb71-bdd4-4618-9ddd-22840b1a8447", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698989467032, "measure": {"type": "total", "value": 302, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "1dc1fb71-bdd4-4618-9ddd-22840b1a8447", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698989210032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "4bbe954b-1282-40ca-a348-33efa7942e63", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698989210032, "measure": {"type": "total", "value": 108, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "4bbe954b-1282-40ca-a348-33efa7942e63", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698987777032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "38443e3b-1cb2-4fc4-ad85-ce393679fff0", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698987777032, "measure": {"type": "total", "value": 109, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "38443e3b-1cb2-4fc4-ad85-ce393679fff0", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698989016032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "6464e734-1c02-4ec3-82af-3d946965a56e", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698989016032, "measure": {"type": "total", "value": 239, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "6464e734-1c02-4ec3-82af-3d946965a56e", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698989786032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "698e3fe6-f520-4e5f-b6bc-d20191812d97", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698989786032, "measure": {"type": "total", "value": 215, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "698e3fe6-f520-4e5f-b6bc-d20191812d97", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698989410032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "1d3dacc6-fc86-45cb-9016-8ddec649241a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698989410032, "measure": {"type": "total", "value": 585, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "1d3dacc6-fc86-45cb-9016-8ddec649241a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698926102032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "d3400998-2afd-4521-80fc-cb858d8c3744", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698926102032, "measure": {"type": "total", "value": 404, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "d3400998-2afd-4521-80fc-cb858d8c3744", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698923972032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "b47bda9b-3b10-4aa6-b992-40a70936004f", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698923972032, "measure": {"type": "total", "value": 437, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "b47bda9b-3b10-4aa6-b992-40a70936004f", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698924320032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "4354c2e8-53c7-460a-81af-1e849f3d4bc5", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698924320032, "measure": {"type": "total", "value": 261, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "4354c2e8-53c7-460a-81af-1e849f3d4bc5", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698963146032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "1ebfa1bc-f27f-41db-b2bd-9a53349c547b", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698963146032, "measure": {"type": "total", "value": 435, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "1ebfa1bc-f27f-41db-b2bd-9a53349c547b", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698965456032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "2ef606f1-6188-499f-9fbc-7ff11f28ae8e", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698965456032, "measure": {"type": "total", "value": 9, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "2ef606f1-6188-499f-9fbc-7ff11f28ae8e", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698972380032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "b0df3bca-dc4e-4d9b-a68d-52b8a2e35fcc", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698972380032, "measure": {"type": "total", "value": 56, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "b0df3bca-dc4e-4d9b-a68d-52b8a2e35fcc", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698970395032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "823c7421-3910-4634-bf12-57c3605bfa96", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698970395032, "measure": {"type": "total", "value": 201, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "823c7421-3910-4634-bf12-57c3605bfa96", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698970358032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "ebe69540-610c-4495-a59e-f91cbb29a072", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698970358032, "measure": {"type": "total", "value": 495, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "ebe69540-610c-4495-a59e-f91cbb29a072", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698972206032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "edccbbcf-eed5-4f91-a0a8-d446756bca6a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698972206032, "measure": {"type": "total", "value": 476, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "edccbbcf-eed5-4f91-a0a8-d446756bca6a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698970209032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "f325ff26-25ec-4c8f-bf91-258a56bc2e19", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698970209032, "measure": {"type": "total", "value": 181, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "f325ff26-25ec-4c8f-bf91-258a56bc2e19", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698975962032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "32a6d9fe-63c0-4c54-b1ba-b17295f715b7", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698975962032, "measure": {"type": "total", "value": 236, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "32a6d9fe-63c0-4c54-b1ba-b17295f715b7", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698976409032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "752a87b8-da80-4694-9d85-e36daf434dd0", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698976409032, "measure": {"type": "total", "value": 426, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "752a87b8-da80-4694-9d85-e36daf434dd0", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698973808032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "fbeef054-0fd1-4a37-bc61-acc1ff6baac5", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698973808032, "measure": {"type": "total", "value": 217, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "fbeef054-0fd1-4a37-bc61-acc1ff6baac5", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698976180032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "73fc7f47-e3d6-4bab-b89c-763eb75901fd", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698976180032, "measure": {"type": "total", "value": 410, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "73fc7f47-e3d6-4bab-b89c-763eb75901fd", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698974187032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "5e027203-2f52-45ac-b969-4b3e6d70c2b3", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698974187032, "measure": {"type": "total", "value": 172, "unit": "seconds"}, "context": {"TenantId": "e470159d-a43b-4401-9965-e2581a261d0d", "UserId": "78dac311-da48-431f-b104-2865441ba6fc", "RequestId": "5e027203-2f52-45ac-b969-4b3e6d70c2b3", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698931129032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "c1c71b03-3911-4e70-baaf-9d3b3afec4de", "RequestId": "b9543080-97f6-488e-b151-a2d20deac43f", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698931129032, "measure": {"type": "total", "value": 243, "unit": "seconds"}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "c1c71b03-3911-4e70-baaf-9d3b3afec4de", "RequestId": "b9543080-97f6-488e-b151-a2d20deac43f", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698933512032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "407408fb-01b5-41a6-9b76-5fc16453ad21", "RequestId": "1cef42a1-fd86-46aa-aaf6-fa54be524c6d", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698933512032, "measure": {"type": "total", "value": 363, "unit": "seconds"}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "407408fb-01b5-41a6-9b76-5fc16453ad21", "RequestId": "1cef42a1-fd86-46aa-aaf6-fa54be524c6d", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698931691032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "407408fb-01b5-41a6-9b76-5fc16453ad21", "RequestId": "2ae4e624-be89-4faa-af67-00c44babc4ed", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698931691032, "measure": {"type": "total", "value": 421, "unit": "seconds"}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "407408fb-01b5-41a6-9b76-5fc16453ad21", "RequestId": "2ae4e624-be89-4faa-af67-00c44babc4ed", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698931194032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "407408fb-01b5-41a6-9b76-5fc16453ad21", "RequestId": "04be2c6c-4e63-47b1-a822-35873bc1dddf", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698931194032, "measure": {"type": "total", "value": 212, "unit": "seconds"}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "407408fb-01b5-41a6-9b76-5fc16453ad21", "RequestId": "04be2c6c-4e63-47b1-a822-35873bc1dddf", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698949536032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "1144a55a-f644-454e-95b0-00f8cc981dba", "RequestId": "eb3874ea-f98e-423f-a61e-16d31114b534", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698949536032, "measure": {"type": "total", "value": 318, "unit": "seconds"}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "1144a55a-f644-454e-95b0-00f8cc981dba", "RequestId": "eb3874ea-f98e-423f-a61e-16d31114b534", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698949442032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "bfc785cc-fea5-4f9e-9114-089adedf4598", "RequestId": "756e8b10-8189-4959-a574-8f5c09683c84", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698949442032, "measure": {"type": "total", "value": 323, "unit": "seconds"}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "bfc785cc-fea5-4f9e-9114-089adedf4598", "RequestId": "756e8b10-8189-4959-a574-8f5c09683c84", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698950681032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "c1c71b03-3911-4e70-baaf-9d3b3afec4de", "RequestId": "dc0a2c67-2de7-4055-bd23-2c3904865594", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698950681032, "measure": {"type": "total", "value": 550, "unit": "seconds"}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "c1c71b03-3911-4e70-baaf-9d3b3afec4de", "RequestId": "dc0a2c67-2de7-4055-bd23-2c3904865594", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698950425032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "c1c71b03-3911-4e70-baaf-9d3b3afec4de", "RequestId": "8e4b4ead-4e9d-45dc-b018-dcb9cbb158ee", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698950425032, "measure": {"type": "total", "value": 309, "unit": "seconds"}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "c1c71b03-3911-4e70-baaf-9d3b3afec4de", "RequestId": "8e4b4ead-4e9d-45dc-b018-dcb9cbb158ee", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698948073032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "407408fb-01b5-41a6-9b76-5fc16453ad21", "RequestId": "ca726daa-193f-4642-96a2-e57215836fa5", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698948073032, "measure": {"type": "total", "value": 313, "unit": "seconds"}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "407408fb-01b5-41a6-9b76-5fc16453ad21", "RequestId": "ca726daa-193f-4642-96a2-e57215836fa5", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698949034032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "bfc785cc-fea5-4f9e-9114-089adedf4598", "RequestId": "6dc2a1ee-ac57-4fa9-a632-c9f8076522dc", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698949034032, "measure": {"type": "total", "value": 531, "unit": "seconds"}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "bfc785cc-fea5-4f9e-9114-089adedf4598", "RequestId": "6dc2a1ee-ac57-4fa9-a632-c9f8076522dc", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698949687032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "c1c71b03-3911-4e70-baaf-9d3b3afec4de", "RequestId": "45e0ca5f-69ee-49b2-9b9e-4dececcdb6c0", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698949687032, "measure": {"type": "total", "value": 433, "unit": "seconds"}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "c1c71b03-3911-4e70-baaf-9d3b3afec4de", "RequestId": "45e0ca5f-69ee-49b2-9b9e-4dececcdb6c0", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698951060032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "c1c71b03-3911-4e70-baaf-9d3b3afec4de", "RequestId": "3165f343-4560-431f-8472-5721f0716931", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698951060032, "measure": {"type": "total", "value": 79, "unit": "seconds"}, "context": {"TenantId": "4834cd7a-a90f-42f9-9f1b-b905cc0c7aa6", "UserId": "c1c71b03-3911-4e70-baaf-9d3b3afec4de", "RequestId": "3165f343-4560-431f-8472-5721f0716931", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698987844032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "8003f075-d514-4ddc-a7be-8777e0914b58", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698987844032, "measure": {"type": "total", "value": 126, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "8003f075-d514-4ddc-a7be-8777e0914b58", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698990256032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "44453d77-6156-40a8-aa46-89f070c58376", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698990256032, "measure": {"type": "total", "value": 104, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "44453d77-6156-40a8-aa46-89f070c58376", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698989052032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "2c19d794-714f-452f-93e0-9b79c02333db", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698989052032, "measure": {"type": "total", "value": 453, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "2c19d794-714f-452f-93e0-9b79c02333db", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698991190032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "35edd835-25c6-4756-a34e-474561f2cfb2", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698991190032, "measure": {"type": "total", "value": 271, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "35edd835-25c6-4756-a34e-474561f2cfb2", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698989578032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "ff5ef656-0348-4b7e-841c-c1063d005b28", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698989578032, "measure": {"type": "total", "value": 120, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "ff5ef656-0348-4b7e-841c-c1063d005b28", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698989005032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "ae8ab067-6327-41f8-bf01-83032409eafc", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698989005032, "measure": {"type": "total", "value": 2, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "ae8ab067-6327-41f8-bf01-83032409eafc", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698988634032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "c829e6cd-fe7f-4b76-8a11-e97c69cec4a6", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698988634032, "measure": {"type": "total", "value": 542, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "c829e6cd-fe7f-4b76-8a11-e97c69cec4a6", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698978179032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "561a64a2-79e8-4633-9f71-ab61c697f530", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698978179032, "measure": {"type": "total", "value": 296, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "561a64a2-79e8-4633-9f71-ab61c697f530", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698978773032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "d92a3d5c-8c3c-4f47-9974-98eed0bd70b3", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698978773032, "measure": {"type": "total", "value": 282, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "d92a3d5c-8c3c-4f47-9974-98eed0bd70b3", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698980065032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "19573d61-081b-4a8a-91ae-e6f5edc6521b", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698980065032, "measure": {"type": "total", "value": 214, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "19573d61-081b-4a8a-91ae-e6f5edc6521b", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698942646032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "2e79fa23-183f-4da8-8f96-47d395b6591a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698942646032, "measure": {"type": "total", "value": 97, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "2e79fa23-183f-4da8-8f96-47d395b6591a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698941129032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "5fa8431f-d2f5-4593-a9c8-a32d52226603", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698941129032, "measure": {"type": "total", "value": 583, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "5fa8431f-d2f5-4593-a9c8-a32d52226603", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698943198032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "2d644788-27fe-4607-9e97-9e7f7970ecf7", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698943198032, "measure": {"type": "total", "value": 232, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "2d644788-27fe-4607-9e97-9e7f7970ecf7", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698941447032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "9f456fac-46eb-4086-af94-bdf3a7518c98", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698941447032, "measure": {"type": "total", "value": 187, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "9f456fac-46eb-4086-af94-bdf3a7518c98", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698941539032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "fcbcdebb-e443-48b6-9da4-d0ce7122e244", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698941539032, "measure": {"type": "total", "value": 349, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "fcbcdebb-e443-48b6-9da4-d0ce7122e244", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698943073032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "81b07ef3-2fdd-42b7-b880-5fdeafa666bf", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698943073032, "measure": {"type": "total", "value": 576, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "81b07ef3-2fdd-42b7-b880-5fdeafa666bf", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698940852032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "8ee9e387-7e76-4414-a774-ce576cc62884", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698940852032, "measure": {"type": "total", "value": 390, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "8ee9e387-7e76-4414-a774-ce576cc62884", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698942535032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "628429b7-7e70-4043-a7a1-4ad55f902670", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698942535032, "measure": {"type": "total", "value": 262, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "628429b7-7e70-4043-a7a1-4ad55f902670", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698957800032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "cf11ff3e-59cf-4eb5-81b7-1380d927918f", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698957800032, "measure": {"type": "total", "value": 158, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "cf11ff3e-59cf-4eb5-81b7-1380d927918f", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698955320032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "7a68264d-8ad6-4f4d-9387-6508b8e19584", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698955320032, "measure": {"type": "total", "value": 320, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "7a68264d-8ad6-4f4d-9387-6508b8e19584", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698957235032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "9661ba52-21ca-4531-928a-12ae56620b51", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698957235032, "measure": {"type": "total", "value": 566, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "9661ba52-21ca-4531-928a-12ae56620b51", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698957159032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "401df2cc-5387-4b9d-89cf-969f8ad4c267", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698957159032, "measure": {"type": "total", "value": 462, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "401df2cc-5387-4b9d-89cf-969f8ad4c267", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698957052032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "f206d971-d270-43cc-bb2a-f4d30b8ed1e1", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698957052032, "measure": {"type": "total", "value": 154, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "f206d971-d270-43cc-bb2a-f4d30b8ed1e1", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698957062032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "25b4eb2a-1280-4d02-8f5f-f9ca91fad1b3", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698957062032, "measure": {"type": "total", "value": 433, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "25b4eb2a-1280-4d02-8f5f-f9ca91fad1b3", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698991528032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "881a56af-1919-4b5f-8b7a-9218c4b1186f", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698991528032, "measure": {"type": "total", "value": 465, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "881a56af-1919-4b5f-8b7a-9218c4b1186f", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698992479032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "a0cb38e5-344b-4dd2-88c5-be1092f24dca", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698992479032, "measure": {"type": "total", "value": 321, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "a0cb38e5-344b-4dd2-88c5-be1092f24dca", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698994618032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "c2cb4942-dfbe-4875-a781-2a52ea94d757", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698994618032, "measure": {"type": "total", "value": 192, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "c2cb4942-dfbe-4875-a781-2a52ea94d757", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698994433032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "92e1a5dc-ae9c-418d-ab7a-64c241750a8c", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698994433032, "measure": {"type": "total", "value": 462, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "92e1a5dc-ae9c-418d-ab7a-64c241750a8c", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698991269032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "da557384-f42a-4769-a46f-bc62ba3464a3", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698991269032, "measure": {"type": "total", "value": 333, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "da557384-f42a-4769-a46f-bc62ba3464a3", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698991964032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "b5248389-d2a6-45ad-9132-dbd7c33ee22c", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698991964032, "measure": {"type": "total", "value": 310, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "b5248389-d2a6-45ad-9132-dbd7c33ee22c", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698991448032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "8dd87aa1-6070-4bc2-bcd2-d60ed6c6cc08", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698991448032, "measure": {"type": "total", "value": 186, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "95c85d22-860a-46de-832e-ac13dc7dffba", "RequestId": "8dd87aa1-6070-4bc2-bcd2-d60ed6c6cc08", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698992806032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "e49dd494-a463-4e5f-a253-578a812c5db7", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698992806032, "measure": {"type": "total", "value": 227, "unit": "seconds"}, "context": {"TenantId": "87e15a2f-5554-4c9f-8c29-6f4999172d29", "UserId": "706d60e4-b17b-4605-acb1-f26cf1edace1", "RequestId": "e49dd494-a463-4e5f-a253-578a812c5db7", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698973088032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "a5ce4f46-8a17-4a75-bf00-87ccf439e3bf", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698973088032, "measure": {"type": "total", "value": 332, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "a5ce4f46-8a17-4a75-bf00-87ccf439e3bf", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698970140032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "38317653-2b9b-4c5f-875d-7e9c63ed0a9c", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698970140032, "measure": {"type": "total", "value": 337, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "38317653-2b9b-4c5f-875d-7e9c63ed0a9c", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698970980032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "ec01133c-36a9-43c5-a807-2ff4e9d7678f", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698970980032, "measure": {"type": "total", "value": 476, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "ec01133c-36a9-43c5-a807-2ff4e9d7678f", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698971714032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "e6c190d3-1916-49cc-96a7-6700def03c49", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698971714032, "measure": {"type": "total", "value": 390, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "e6c190d3-1916-49cc-96a7-6700def03c49", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698973198032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "4f734e3a-d55f-4850-bd0e-26e1a51201ea", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698973198032, "measure": {"type": "total", "value": 514, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "4f734e3a-d55f-4850-bd0e-26e1a51201ea", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698970427032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "df817249-f381-4541-8999-358a6a6751ee", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698970427032, "measure": {"type": "total", "value": 454, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "df817249-f381-4541-8999-358a6a6751ee", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698946785032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "441bcd4c-8be0-495a-b879-49e87743032a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698946785032, "measure": {"type": "total", "value": 271, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "441bcd4c-8be0-495a-b879-49e87743032a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698947534032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "9004108f-e8b7-46ae-a33f-de8e869dbffb", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698947534032, "measure": {"type": "total", "value": 332, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "9004108f-e8b7-46ae-a33f-de8e869dbffb", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698947509032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "393919de-f855-4bd3-a262-fab058357528", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698947509032, "measure": {"type": "total", "value": 449, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "393919de-f855-4bd3-a262-fab058357528", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698944830032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "b0d8655d-1b98-4057-8797-9cf6e5bfae63", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698944830032, "measure": {"type": "total", "value": 579, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "b0d8655d-1b98-4057-8797-9cf6e5bfae63", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698947774032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "ca04cb3e-bd54-47f2-95f9-ce1271686699", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698947774032, "measure": {"type": "total", "value": 229, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "ca04cb3e-bd54-47f2-95f9-ce1271686699", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698987952032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "b691d72f-5db5-4c2c-ab55-9e50c2b79094", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698987952032, "measure": {"type": "total", "value": 209, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "b691d72f-5db5-4c2c-ab55-9e50c2b79094", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698989427032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "bf7c7842-6189-49bb-b9c9-aa4b8a29edad", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698989427032, "measure": {"type": "total", "value": 371, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "bf7c7842-6189-49bb-b9c9-aa4b8a29edad", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698989373032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "f09e2bd5-a4ad-49fd-91ea-9110cca6ae8a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698989373032, "measure": {"type": "total", "value": 384, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "f09e2bd5-a4ad-49fd-91ea-9110cca6ae8a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698990555032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "defcab61-3407-4b82-854b-17d567373c4e", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698990555032, "measure": {"type": "total", "value": 312, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "defcab61-3407-4b82-854b-17d567373c4e", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698984444032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "7b86690a-71fe-40f4-95f0-10a95481597d", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698984444032, "measure": {"type": "total", "value": 535, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "7b86690a-71fe-40f4-95f0-10a95481597d", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698986024032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "11bb2514-5167-4742-bd9f-8c382780b8aa", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698986024032, "measure": {"type": "total", "value": 238, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "11bb2514-5167-4742-bd9f-8c382780b8aa", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698986819032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "f80487dd-1616-421c-b624-175fcbd963a4", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698986819032, "measure": {"type": "total", "value": 451, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "f80487dd-1616-421c-b624-175fcbd963a4", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698984533032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "f4723023-5fcb-462f-a413-e67a64243579", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698984533032, "measure": {"type": "total", "value": 23, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "f4723023-5fcb-462f-a413-e67a64243579", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698984354032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "32441191-c2be-4a3e-b56d-924e4a0da7ba", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698984354032, "measure": {"type": "total", "value": 116, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "32441191-c2be-4a3e-b56d-924e4a0da7ba", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698987226032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "61b46153-6485-4784-a806-6f4a73e93864", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698987226032, "measure": {"type": "total", "value": 284, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "61b46153-6485-4784-a806-6f4a73e93864", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698917715032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "d13b2a9b-1dd2-4207-bbb9-0c597b12093c", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698917715032, "measure": {"type": "total", "value": 574, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "d13b2a9b-1dd2-4207-bbb9-0c597b12093c", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698916385032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "ce6cef95-9c1c-4e56-8612-ab8b5a01349a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698916385032, "measure": {"type": "total", "value": 160, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "ce6cef95-9c1c-4e56-8612-ab8b5a01349a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698918875032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "d79b5320-0145-49fe-baea-84b6bbe0e207", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698918875032, "measure": {"type": "total", "value": 479, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "d79b5320-0145-49fe-baea-84b6bbe0e207", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698916607032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "e3fcaed7-ec84-4134-a9ad-bc3a6ba1ac4f", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698916607032, "measure": {"type": "total", "value": 76, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "e3fcaed7-ec84-4134-a9ad-bc3a6ba1ac4f", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698918445032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "00b88a75-06c8-4c89-8a1d-b4ad12d13141", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698918445032, "measure": {"type": "total", "value": 13, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "00b88a75-06c8-4c89-8a1d-b4ad12d13141", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698916088032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "cf330984-f085-4926-aa5a-4e1f11671b9e", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698916088032, "measure": {"type": "total", "value": 288, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "cf330984-f085-4926-aa5a-4e1f11671b9e", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698916256032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "e6bb3d0e-1736-40bb-8d46-589b996e6e02", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698916256032, "measure": {"type": "total", "value": 260, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "e6bb3d0e-1736-40bb-8d46-589b996e6e02", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698916777032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "531b4924-e78e-434a-8455-770144fde675", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698916777032, "measure": {"type": "total", "value": 335, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "531b4924-e78e-434a-8455-770144fde675", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698917586032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "956aebe4-64f4-433a-b330-587263870821", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698917586032, "measure": {"type": "total", "value": 339, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "956aebe4-64f4-433a-b330-587263870821", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698918327032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "2298704b-30b9-4d8c-9e40-1db818ff964f", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698918327032, "measure": {"type": "total", "value": 127, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "76dbd6bf-76d9-42ad-ace2-25b04ce4903f", "RequestId": "2298704b-30b9-4d8c-9e40-1db818ff964f", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698939710032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "f1885aa9-2d66-458a-bbf1-3b769e4a4e04", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698939710032, "measure": {"type": "total", "value": 101, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "f1885aa9-2d66-458a-bbf1-3b769e4a4e04", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698938459032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "44ec6bd0-ff47-4041-8a69-ec319ff18fa2", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698938459032, "measure": {"type": "total", "value": 433, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "44ec6bd0-ff47-4041-8a69-ec319ff18fa2", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698950573032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "6916b66a-eb46-4eb7-8db9-aea977ad3e0f", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698950573032, "measure": {"type": "total", "value": 581, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "6916b66a-eb46-4eb7-8db9-aea977ad3e0f", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698948316032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "acd81089-8eb4-4c93-9f4a-6c369508e7b6", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698948316032, "measure": {"type": "total", "value": 432, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "acd81089-8eb4-4c93-9f4a-6c369508e7b6", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698949228032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "abde6d4d-4055-41a6-972c-c94601d03be6", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698949228032, "measure": {"type": "total", "value": 415, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "6e2a28db-a088-4914-b3ab-eeb8a786fe48", "RequestId": "abde6d4d-4055-41a6-972c-c94601d03be6", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698949704032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "33ca228c-e2f7-4809-b49e-838f4df49915", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698949704032, "measure": {"type": "total", "value": 536, "unit": "seconds"}, "context": {"TenantId": "29bb8965-726b-4aa8-af03-5d67f4c9a713", "UserId": "a1a14151-33cd-489b-b7f9-70b9f752bd96", "RequestId": "33ca228c-e2f7-4809-b49e-838f4df49915", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698994158032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "031f3f4d-1889-4996-aba7-4c05ae6a82c6", "RequestId": "10278344-b7c5-4dec-84b8-a738a71d0a7d", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698994158032, "measure": {"type": "total", "value": 316, "unit": "seconds"}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "031f3f4d-1889-4996-aba7-4c05ae6a82c6", "RequestId": "10278344-b7c5-4dec-84b8-a738a71d0a7d", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698991523032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "d313a845-28c3-4e91-8db8-5051135023f2", "RequestId": "ee862d6e-9ac7-4348-9c16-b19d530a81b5", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698991523032, "measure": {"type": "total", "value": 431, "unit": "seconds"}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "d313a845-28c3-4e91-8db8-5051135023f2", "RequestId": "ee862d6e-9ac7-4348-9c16-b19d530a81b5", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698994624032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "031f3f4d-1889-4996-aba7-4c05ae6a82c6", "RequestId": "e201587d-f90f-4581-a824-7b96fa033793", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698994624032, "measure": {"type": "total", "value": 148, "unit": "seconds"}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "031f3f4d-1889-4996-aba7-4c05ae6a82c6", "RequestId": "e201587d-f90f-4581-a824-7b96fa033793", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698994738032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "c5311780-e5de-40a8-92f9-c91395c6660f", "RequestId": "14b2d8bc-e1e6-41f1-9d2c-2a3a17e1d00f", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698994738032, "measure": {"type": "total", "value": 70, "unit": "seconds"}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "c5311780-e5de-40a8-92f9-c91395c6660f", "RequestId": "14b2d8bc-e1e6-41f1-9d2c-2a3a17e1d00f", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698992154032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "031f3f4d-1889-4996-aba7-4c05ae6a82c6", "RequestId": "8283014f-791b-41b0-a2d9-e92713b7de04", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698992154032, "measure": {"type": "total", "value": 357, "unit": "seconds"}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "031f3f4d-1889-4996-aba7-4c05ae6a82c6", "RequestId": "8283014f-791b-41b0-a2d9-e92713b7de04", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698992457032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "d313a845-28c3-4e91-8db8-5051135023f2", "RequestId": "72c3333d-94b1-4d02-aa54-52b617ba36a2", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698992457032, "measure": {"type": "total", "value": 10, "unit": "seconds"}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "d313a845-28c3-4e91-8db8-5051135023f2", "RequestId": "72c3333d-94b1-4d02-aa54-52b617ba36a2", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698991690032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "d313a845-28c3-4e91-8db8-5051135023f2", "RequestId": "939e1ce3-7215-41fc-9aae-0a06b49e73aa", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698991690032, "measure": {"type": "total", "value": 468, "unit": "seconds"}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "d313a845-28c3-4e91-8db8-5051135023f2", "RequestId": "939e1ce3-7215-41fc-9aae-0a06b49e73aa", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698994073032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "79cd8f55-dbb6-44de-bc4a-0e239c02dfaf", "RequestId": "248b46ab-4e3c-4326-8c50-dbc8bbcc343e", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698994073032, "measure": {"type": "total", "value": 507, "unit": "seconds"}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "79cd8f55-dbb6-44de-bc4a-0e239c02dfaf", "RequestId": "248b46ab-4e3c-4326-8c50-dbc8bbcc343e", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698992897032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "79cd8f55-dbb6-44de-bc4a-0e239c02dfaf", "RequestId": "1761dbda-82c5-4b0e-adee-4c1269c4bbbe", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698992897032, "measure": {"type": "total", "value": 17, "unit": "seconds"}, "context": {"TenantId": "dc955639-8205-41ae-9b90-ec073e453fd3", "UserId": "79cd8f55-dbb6-44de-bc4a-0e239c02dfaf", "RequestId": "1761dbda-82c5-4b0e-adee-4c1269c4bbbe", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698991770032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "acfd33f6-cfc1-405a-a874-fc4e0fe9449a", "UserId": "86424b6a-e7c7-436b-b7a8-4cfb5b45e7a1", "RequestId": "6c6f0245-f1bd-430f-b150-eefa2a71ef28", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698991770032, "measure": {"type": "total", "value": 230, "unit": "seconds"}, "context": {"TenantId": "acfd33f6-cfc1-405a-a874-fc4e0fe9449a", "UserId": "86424b6a-e7c7-436b-b7a8-4cfb5b45e7a1", "RequestId": "6c6f0245-f1bd-430f-b150-eefa2a71ef28", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698992046032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "acfd33f6-cfc1-405a-a874-fc4e0fe9449a", "UserId": "5c975fec-dfef-4be9-8526-fa05a2c0d832", "RequestId": "4334360a-11b3-4b06-8ed8-d80f2fa025ea", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698992046032, "measure": {"type": "total", "value": 571, "unit": "seconds"}, "context": {"TenantId": "acfd33f6-cfc1-405a-a874-fc4e0fe9449a", "UserId": "5c975fec-dfef-4be9-8526-fa05a2c0d832", "RequestId": "4334360a-11b3-4b06-8ed8-d80f2fa025ea", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698993737032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "acfd33f6-cfc1-405a-a874-fc4e0fe9449a", "UserId": "a524eee4-fac0-41e8-b915-24ea9391a985", "RequestId": "b8e8d1bd-b33b-42f4-9e80-e852cbc25f18", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698993737032, "measure": {"type": "total", "value": 437, "unit": "seconds"}, "context": {"TenantId": "acfd33f6-cfc1-405a-a874-fc4e0fe9449a", "UserId": "a524eee4-fac0-41e8-b915-24ea9391a985", "RequestId": "b8e8d1bd-b33b-42f4-9e80-e852cbc25f18", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698992803032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "acfd33f6-cfc1-405a-a874-fc4e0fe9449a", "UserId": "86424b6a-e7c7-436b-b7a8-4cfb5b45e7a1", "RequestId": "1b3b9a01-7d2d-433c-bd5b-bca23625f4e0", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698992803032, "measure": {"type": "total", "value": 4, "unit": "seconds"}, "context": {"TenantId": "acfd33f6-cfc1-405a-a874-fc4e0fe9449a", "UserId": "86424b6a-e7c7-436b-b7a8-4cfb5b45e7a1", "RequestId": "1b3b9a01-7d2d-433c-bd5b-bca23625f4e0", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698927223032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "8289befd-dbc0-4a62-8900-f763aba35c09", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698927223032, "measure": {"type": "total", "value": 107, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "8289befd-dbc0-4a62-8900-f763aba35c09", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698945274032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "f28b5507-80eb-4fb8-9658-e6d68dc78973", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698945274032, "measure": {"type": "total", "value": 45, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "f28b5507-80eb-4fb8-9658-e6d68dc78973", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698947748032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "d2ab01a8-9656-49cc-b85a-2fc2cbcb1ec3", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698947748032, "measure": {"type": "total", "value": 593, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "d2ab01a8-9656-49cc-b85a-2fc2cbcb1ec3", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698946673032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "ea47949e-3fd3-400f-a520-0c3feba10e9e", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698946673032, "measure": {"type": "total", "value": 513, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "ea47949e-3fd3-400f-a520-0c3feba10e9e", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698945227032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "a4fa2d49-5e4f-4fd1-88b4-2fed7be3ac19", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698945227032, "measure": {"type": "total", "value": 466, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "a4fa2d49-5e4f-4fd1-88b4-2fed7be3ac19", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698947996032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "596e8226-621e-49ed-89ee-4fcbebe389c2", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698947996032, "measure": {"type": "total", "value": 229, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "596e8226-621e-49ed-89ee-4fcbebe389c2", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698945026032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "1f9519c5-e0fc-4e99-ae67-1f4a159429f8", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698945026032, "measure": {"type": "total", "value": 104, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "1f9519c5-e0fc-4e99-ae67-1f4a159429f8", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698917760032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "b95e0626-c48d-420a-b935-cd30c2f4a51a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698917760032, "measure": {"type": "total", "value": 163, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "b95e0626-c48d-420a-b935-cd30c2f4a51a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698915781032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "f0948c0d-06d4-4609-9c94-7902086a5e7d", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698915781032, "measure": {"type": "total", "value": 327, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "f0948c0d-06d4-4609-9c94-7902086a5e7d", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698919093032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "062a5021-a5d0-4013-8447-5312e220f73f", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698919093032, "measure": {"type": "total", "value": 392, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "062a5021-a5d0-4013-8447-5312e220f73f", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698917173032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "ef7ae910-ec55-43f4-bc24-dafb02566b5b", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698917173032, "measure": {"type": "total", "value": 203, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "ef7ae910-ec55-43f4-bc24-dafb02566b5b", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698919039032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "bc4a00da-5536-4548-a59d-ea359270b1d3", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698919039032, "measure": {"type": "total", "value": 346, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "bc4a00da-5536-4548-a59d-ea359270b1d3", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698916383032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "52c883df-0d8c-4bdf-9e5f-69bce1c56026", "RequestId": "a4696fc7-5cb0-4c7f-9cf2-29629b9da67e", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698916383032, "measure": {"type": "total", "value": 595, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "52c883df-0d8c-4bdf-9e5f-69bce1c56026", "RequestId": "a4696fc7-5cb0-4c7f-9cf2-29629b9da67e", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698919140032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "8ca70905-a2c8-4efe-96a0-c64d46dd87e8", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698919140032, "measure": {"type": "total", "value": 555, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "8ca70905-a2c8-4efe-96a0-c64d46dd87e8", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698915735032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "eafb0569-fdb7-4c07-9e3b-18fbe1133b29", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698915735032, "measure": {"type": "total", "value": 102, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "eafb0569-fdb7-4c07-9e3b-18fbe1133b29", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698917372032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "53aef466-23d4-41e0-b07a-8660835837a6", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698917372032, "measure": {"type": "total", "value": 493, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "53aef466-23d4-41e0-b07a-8660835837a6", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698989709032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "c45af2a3-2b9e-4f93-8554-f52c36410a75", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698989709032, "measure": {"type": "total", "value": 254, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "c45af2a3-2b9e-4f93-8554-f52c36410a75", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698988233032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "476cadad-fac4-4c2d-a033-806787322ef9", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698988233032, "measure": {"type": "total", "value": 120, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "476cadad-fac4-4c2d-a033-806787322ef9", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698988745032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "2d2f1a25-dafd-4e71-9337-9d55e1fa1e64", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698988745032, "measure": {"type": "total", "value": 581, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "2d2f1a25-dafd-4e71-9337-9d55e1fa1e64", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698990480032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "b17e5e4e-df2f-4c1e-8c70-991ae541d8ff", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698990480032, "measure": {"type": "total", "value": 71, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "b17e5e4e-df2f-4c1e-8c70-991ae541d8ff", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698988066032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "4441f5ca-cfcc-45fc-bc2d-53893906d20d", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698988066032, "measure": {"type": "total", "value": 74, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "4441f5ca-cfcc-45fc-bc2d-53893906d20d", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698988131032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "91158701-b7c9-4ed4-988a-0d11486e56a9", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698988131032, "measure": {"type": "total", "value": 231, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "91158701-b7c9-4ed4-988a-0d11486e56a9", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698990708032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "65add940-def2-4cc9-a8c3-4bf894274d65", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698990708032, "measure": {"type": "total", "value": 345, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "65add940-def2-4cc9-a8c3-4bf894274d65", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698987694032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "c76371ee-f97e-41fd-b6a8-19494e33a8ab", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698987694032, "measure": {"type": "total", "value": 317, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "c76371ee-f97e-41fd-b6a8-19494e33a8ab", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698988530032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "6996a026-4295-46d6-9258-c12757898e61", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698988530032, "measure": {"type": "total", "value": 58, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "6996a026-4295-46d6-9258-c12757898e61", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698988655032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "0e17a96f-52db-4b8a-a2ae-736d5449824d", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698988655032, "measure": {"type": "total", "value": 545, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "0e17a96f-52db-4b8a-a2ae-736d5449824d", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698921419032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "76705355-53da-4c64-bd74-b0072f61f41b", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698921419032, "measure": {"type": "total", "value": 392, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "76705355-53da-4c64-bd74-b0072f61f41b", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698919812032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "8598cd45-8cb2-475d-8ff5-0ec838df3177", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698919812032, "measure": {"type": "total", "value": 430, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "8598cd45-8cb2-475d-8ff5-0ec838df3177", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698921145032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "e7343190-9c23-4fa5-a20d-d5046e2a1a34", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698921145032, "measure": {"type": "total", "value": 259, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "e7343190-9c23-4fa5-a20d-d5046e2a1a34", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698920213032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "bfccaa9d-6739-4475-811d-2075c60857b5", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698920213032, "measure": {"type": "total", "value": 576, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "bfccaa9d-6739-4475-811d-2075c60857b5", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698920390032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "1f186931-2054-4bf7-8e9a-0873845cc6b1", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698920390032, "measure": {"type": "total", "value": 212, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "1f186931-2054-4bf7-8e9a-0873845cc6b1", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698921199032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "5ef6869c-4158-4dcb-af19-b96a82243834", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698921199032, "measure": {"type": "total", "value": 192, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "5ef6869c-4158-4dcb-af19-b96a82243834", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698921793032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "586898d6-3b47-4cfb-b67d-3290bcdaeeab", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698921793032, "measure": {"type": "total", "value": 556, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "586898d6-3b47-4cfb-b67d-3290bcdaeeab", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698919367032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "727913e5-7ed6-480d-8163-e3ca33f70dda", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698919367032, "measure": {"type": "total", "value": 469, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "727913e5-7ed6-480d-8163-e3ca33f70dda", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698948060032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "52c883df-0d8c-4bdf-9e5f-69bce1c56026", "RequestId": "018eb456-60a3-4771-b64e-0701fa50db06", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698948060032, "measure": {"type": "total", "value": 181, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "52c883df-0d8c-4bdf-9e5f-69bce1c56026", "RequestId": "018eb456-60a3-4771-b64e-0701fa50db06", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698948190032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "a5e25c5f-6cb0-4004-aa15-d09a10dab5fd", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698948190032, "measure": {"type": "total", "value": 442, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "a5e25c5f-6cb0-4004-aa15-d09a10dab5fd", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698948249032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "52c883df-0d8c-4bdf-9e5f-69bce1c56026", "RequestId": "516a42a9-ca75-492e-8203-87d936200461", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698948249032, "measure": {"type": "total", "value": 587, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "52c883df-0d8c-4bdf-9e5f-69bce1c56026", "RequestId": "516a42a9-ca75-492e-8203-87d936200461", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698949379032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "52c883df-0d8c-4bdf-9e5f-69bce1c56026", "RequestId": "0311ec97-bda0-43f1-9ec3-a514faec5385", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698949379032, "measure": {"type": "total", "value": 456, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "52c883df-0d8c-4bdf-9e5f-69bce1c56026", "RequestId": "0311ec97-bda0-43f1-9ec3-a514faec5385", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698949470032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "91d38c6c-0c32-42b8-8a91-4c7cf8bf3b45", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698949470032, "measure": {"type": "total", "value": 373, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "91d38c6c-0c32-42b8-8a91-4c7cf8bf3b45", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698951446032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "817cce62-9f3e-4098-bd7b-4d26002f23cb", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698951446032, "measure": {"type": "total", "value": 21, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "817cce62-9f3e-4098-bd7b-4d26002f23cb", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698948351032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "52c883df-0d8c-4bdf-9e5f-69bce1c56026", "RequestId": "8d3dd5bc-50e1-4341-9c03-955584e02a3d", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698948351032, "measure": {"type": "total", "value": 433, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "52c883df-0d8c-4bdf-9e5f-69bce1c56026", "RequestId": "8d3dd5bc-50e1-4341-9c03-955584e02a3d", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698950280032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "3d492335-0dd5-4b4b-aed0-8264bc26014c", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698950280032, "measure": {"type": "total", "value": 287, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "3d492335-0dd5-4b4b-aed0-8264bc26014c", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698955361032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "52c883df-0d8c-4bdf-9e5f-69bce1c56026", "RequestId": "9fddeba5-259c-42b8-bd9c-641f76d98077", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698955361032, "measure": {"type": "total", "value": 6, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "52c883df-0d8c-4bdf-9e5f-69bce1c56026", "RequestId": "9fddeba5-259c-42b8-bd9c-641f76d98077", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698957000032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "a71a3ff1-bb10-4c1b-b9fa-23f9ed84cc5f", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698957000032, "measure": {"type": "total", "value": 482, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "a71a3ff1-bb10-4c1b-b9fa-23f9ed84cc5f", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698956891032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "7e606254-61cf-453e-87b9-5c94ba6b87ca", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698956891032, "measure": {"type": "total", "value": 440, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "7e606254-61cf-453e-87b9-5c94ba6b87ca", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698957395032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "52c883df-0d8c-4bdf-9e5f-69bce1c56026", "RequestId": "0894312f-9750-4af7-bd41-38e13c1bf571", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698957395032, "measure": {"type": "total", "value": 484, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "52c883df-0d8c-4bdf-9e5f-69bce1c56026", "RequestId": "0894312f-9750-4af7-bd41-38e13c1bf571", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698955661032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "f496524b-4976-4ed9-97c1-29bc4c14ccb0", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698955661032, "measure": {"type": "total", "value": 211, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "f496524b-4976-4ed9-97c1-29bc4c14ccb0", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698956034032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "7b15e4fe-9ab8-4164-9c71-9f34d10ef5d4", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698956034032, "measure": {"type": "total", "value": 380, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2791f214-3e1d-4d7c-897d-defbedc7515c", "RequestId": "7b15e4fe-9ab8-4164-9c71-9f34d10ef5d4", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698966153032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "368c7937-87d1-489e-b5f8-7774eafa93e7", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698966153032, "measure": {"type": "total", "value": 553, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "368c7937-87d1-489e-b5f8-7774eafa93e7", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698968413032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "936789d9-3f0c-4774-9acd-8dcd8223153c", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698968413032, "measure": {"type": "total", "value": 103, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "936789d9-3f0c-4774-9acd-8dcd8223153c", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698968483032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "9470211b-1493-47d4-afdf-3ed1fb0dbb56", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698968483032, "measure": {"type": "total", "value": 557, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "9470211b-1493-47d4-afdf-3ed1fb0dbb56", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698967725032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "3df8bc01-b635-42ca-b01a-a23207ff4004", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698967725032, "measure": {"type": "total", "value": 232, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "3df8bc01-b635-42ca-b01a-a23207ff4004", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698967962032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "512f9eca-0c5d-4e94-a459-f354aa588a43", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698967962032, "measure": {"type": "total", "value": 428, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "2808b2cd-f9ba-42e6-8c32-d5be1356071d", "RequestId": "512f9eca-0c5d-4e94-a459-f354aa588a43", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698968580032, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "1fff5c71-5522-46dc-8b88-4be8ed4c27ab", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698968580032, "measure": {"type": "total", "value": 312, "unit": "seconds"}, "context": {"TenantId": "3a661d6d-1209-4e5d-ae63-17c24c06a113", "UserId": "14cdd487-0879-4f48-adcf-0ce92ddcc41a", "RequestId": "1fff5c71-5522-46dc-8b88-4be8ed4c27ab", "Application": "Job Service", "Action": "executeJob"}}] \ No newline at end of file diff --git a/services/metrics-service/src/test/resources/metrics2.json b/services/metrics-service/src/test/resources/metrics2.json new file mode 100644 index 00000000..bf6d7221 --- /dev/null +++ b/services/metrics-service/src/test/resources/metrics2.json @@ -0,0 +1 @@ +[{"name": "Jobs", "timestamp": 1698821303505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "408ad5c1-1314-4722-8f88-bf1bf5714376", "RequestId": "d1c440e4-e964-4069-a62a-4961a8efa87d", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698821303505, "measure": {"type": "total", "value": 386, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "408ad5c1-1314-4722-8f88-bf1bf5714376", "RequestId": "d1c440e4-e964-4069-a62a-4961a8efa87d", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698819919505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "77dbaee7-99c0-4de4-926b-c2281ae34d1f", "RequestId": "ff03c75f-ae08-4255-923a-611cff802c57", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698819919505, "measure": {"type": "total", "value": 462, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "77dbaee7-99c0-4de4-926b-c2281ae34d1f", "RequestId": "ff03c75f-ae08-4255-923a-611cff802c57", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698820266505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "cee50dec-bd95-4e9b-a24b-3acbe9d61495", "RequestId": "ab731f13-2a9a-43a2-b1d8-04450b5206e9", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698820266505, "measure": {"type": "total", "value": 174, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "cee50dec-bd95-4e9b-a24b-3acbe9d61495", "RequestId": "ab731f13-2a9a-43a2-b1d8-04450b5206e9", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698789816505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "cee50dec-bd95-4e9b-a24b-3acbe9d61495", "RequestId": "4680de68-f071-466d-943e-9ae6602290ed", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698789816505, "measure": {"type": "total", "value": 362, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "cee50dec-bd95-4e9b-a24b-3acbe9d61495", "RequestId": "4680de68-f071-466d-943e-9ae6602290ed", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698791704505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "72e01e2d-b674-4014-bfeb-aa8579014f60", "RequestId": "f0bd1622-2bc4-4426-bb40-6c07092ad901", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698791704505, "measure": {"type": "total", "value": 96, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "72e01e2d-b674-4014-bfeb-aa8579014f60", "RequestId": "f0bd1622-2bc4-4426-bb40-6c07092ad901", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698790127505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "77dbaee7-99c0-4de4-926b-c2281ae34d1f", "RequestId": "0994268d-c15f-4f5a-b609-626033ee41c6", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698790127505, "measure": {"type": "total", "value": 198, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "77dbaee7-99c0-4de4-926b-c2281ae34d1f", "RequestId": "0994268d-c15f-4f5a-b609-626033ee41c6", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698792360505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "77dbaee7-99c0-4de4-926b-c2281ae34d1f", "RequestId": "c627ed60-fe49-4481-ab5a-6e2de3dc84aa", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698792360505, "measure": {"type": "total", "value": 126, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "77dbaee7-99c0-4de4-926b-c2281ae34d1f", "RequestId": "c627ed60-fe49-4481-ab5a-6e2de3dc84aa", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698791033505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "72e01e2d-b674-4014-bfeb-aa8579014f60", "RequestId": "fea9bf5e-785b-4ea8-8eb0-d6e8139e716d", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698791033505, "measure": {"type": "total", "value": 80, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "72e01e2d-b674-4014-bfeb-aa8579014f60", "RequestId": "fea9bf5e-785b-4ea8-8eb0-d6e8139e716d", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698792152505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "c612a473-130f-4524-9d00-147d59ec4351", "RequestId": "01d58d4b-680c-4cca-9555-66293bae7215", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698792152505, "measure": {"type": "total", "value": 109, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "c612a473-130f-4524-9d00-147d59ec4351", "RequestId": "01d58d4b-680c-4cca-9555-66293bae7215", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698790650505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "408ad5c1-1314-4722-8f88-bf1bf5714376", "RequestId": "e377f79a-69d9-4508-a24e-e38908702e94", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698790650505, "measure": {"type": "total", "value": 538, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "408ad5c1-1314-4722-8f88-bf1bf5714376", "RequestId": "e377f79a-69d9-4508-a24e-e38908702e94", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698792033505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "c612a473-130f-4524-9d00-147d59ec4351", "RequestId": "b81fece9-1109-4b13-89c3-f90a2169cfaa", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698792033505, "measure": {"type": "total", "value": 291, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "c612a473-130f-4524-9d00-147d59ec4351", "RequestId": "b81fece9-1109-4b13-89c3-f90a2169cfaa", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698745723505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "77dbaee7-99c0-4de4-926b-c2281ae34d1f", "RequestId": "adae6882-bba4-4c3e-9851-903f354d1daf", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698745723505, "measure": {"type": "total", "value": 595, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "77dbaee7-99c0-4de4-926b-c2281ae34d1f", "RequestId": "adae6882-bba4-4c3e-9851-903f354d1daf", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698737286505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "cee50dec-bd95-4e9b-a24b-3acbe9d61495", "RequestId": "d3b74107-3971-48a1-a3f5-aa382d77e382", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698737286505, "measure": {"type": "total", "value": 520, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "cee50dec-bd95-4e9b-a24b-3acbe9d61495", "RequestId": "d3b74107-3971-48a1-a3f5-aa382d77e382", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698735615505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "c612a473-130f-4524-9d00-147d59ec4351", "RequestId": "acff7cea-72b1-492b-af09-7395184cf712", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698735615505, "measure": {"type": "total", "value": 196, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "c612a473-130f-4524-9d00-147d59ec4351", "RequestId": "acff7cea-72b1-492b-af09-7395184cf712", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698736486505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "c612a473-130f-4524-9d00-147d59ec4351", "RequestId": "40434c01-065f-4c88-b32a-634821f37411", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698736486505, "measure": {"type": "total", "value": 156, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "c612a473-130f-4524-9d00-147d59ec4351", "RequestId": "40434c01-065f-4c88-b32a-634821f37411", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698736618505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "cee50dec-bd95-4e9b-a24b-3acbe9d61495", "RequestId": "211de5a1-6565-4fcd-acd1-f80139538e38", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698736618505, "measure": {"type": "total", "value": 537, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "cee50dec-bd95-4e9b-a24b-3acbe9d61495", "RequestId": "211de5a1-6565-4fcd-acd1-f80139538e38", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698736512505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "cee50dec-bd95-4e9b-a24b-3acbe9d61495", "RequestId": "9c84058a-11fd-4aac-916b-02a3797e9876", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698736512505, "measure": {"type": "total", "value": 184, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "cee50dec-bd95-4e9b-a24b-3acbe9d61495", "RequestId": "9c84058a-11fd-4aac-916b-02a3797e9876", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698736370505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "77dbaee7-99c0-4de4-926b-c2281ae34d1f", "RequestId": "a4279ba4-9f93-4498-9ef1-ec73995c2998", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698736370505, "measure": {"type": "total", "value": 4, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "77dbaee7-99c0-4de4-926b-c2281ae34d1f", "RequestId": "a4279ba4-9f93-4498-9ef1-ec73995c2998", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698738525505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "c612a473-130f-4524-9d00-147d59ec4351", "RequestId": "3df18ced-dfea-4c1c-abe5-2aa022022518", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698738525505, "measure": {"type": "total", "value": 54, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "c612a473-130f-4524-9d00-147d59ec4351", "RequestId": "3df18ced-dfea-4c1c-abe5-2aa022022518", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698737677505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "408ad5c1-1314-4722-8f88-bf1bf5714376", "RequestId": "cbb6b066-beb6-4397-ac62-990e136af86c", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698737677505, "measure": {"type": "total", "value": 113, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "408ad5c1-1314-4722-8f88-bf1bf5714376", "RequestId": "cbb6b066-beb6-4397-ac62-990e136af86c", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698736917505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "77dbaee7-99c0-4de4-926b-c2281ae34d1f", "RequestId": "87a019ed-7e6c-41db-a16b-fc658feacf1f", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698736917505, "measure": {"type": "total", "value": 389, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "77dbaee7-99c0-4de4-926b-c2281ae34d1f", "RequestId": "87a019ed-7e6c-41db-a16b-fc658feacf1f", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698736270505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "cee50dec-bd95-4e9b-a24b-3acbe9d61495", "RequestId": "5aff4857-fd33-4045-95dc-4b6697b837c5", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698736270505, "measure": {"type": "total", "value": 363, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "cee50dec-bd95-4e9b-a24b-3acbe9d61495", "RequestId": "5aff4857-fd33-4045-95dc-4b6697b837c5", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698760716505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "c612a473-130f-4524-9d00-147d59ec4351", "RequestId": "62da48bc-87e7-44f3-badb-dde111590270", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698760716505, "measure": {"type": "total", "value": 45, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "c612a473-130f-4524-9d00-147d59ec4351", "RequestId": "62da48bc-87e7-44f3-badb-dde111590270", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698758822505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "408ad5c1-1314-4722-8f88-bf1bf5714376", "RequestId": "f756e881-5012-4290-8a0c-c4140390f64d", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698758822505, "measure": {"type": "total", "value": 505, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "408ad5c1-1314-4722-8f88-bf1bf5714376", "RequestId": "f756e881-5012-4290-8a0c-c4140390f64d", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698757881505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "77dbaee7-99c0-4de4-926b-c2281ae34d1f", "RequestId": "8a7cdc14-d794-4b51-b9f4-5966d08e2f7c", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698757881505, "measure": {"type": "total", "value": 509, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "77dbaee7-99c0-4de4-926b-c2281ae34d1f", "RequestId": "8a7cdc14-d794-4b51-b9f4-5966d08e2f7c", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698759286505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "72e01e2d-b674-4014-bfeb-aa8579014f60", "RequestId": "b6ffb8fc-6b1a-402e-96d2-1d5c217a83f4", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698759286505, "measure": {"type": "total", "value": 556, "unit": "seconds"}, "context": {"TenantId": "2b83532b-bfa5-4024-86e3-8dd11d1a646e", "UserId": "72e01e2d-b674-4014-bfeb-aa8579014f60", "RequestId": "b6ffb8fc-6b1a-402e-96d2-1d5c217a83f4", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698764619505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "2e27e85a-1b11-4b25-b842-517cd4949ec8", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698764619505, "measure": {"type": "total", "value": 559, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "2e27e85a-1b11-4b25-b842-517cd4949ec8", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698760349505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "8ffb72f1-cd86-46e7-b251-906907ff5c31", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698760349505, "measure": {"type": "total", "value": 243, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "8ffb72f1-cd86-46e7-b251-906907ff5c31", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698759796505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "6d9f2be3-e8e0-43b3-8a9a-5bf09d23c68a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698759796505, "measure": {"type": "total", "value": 97, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "6d9f2be3-e8e0-43b3-8a9a-5bf09d23c68a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698758870505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "8cab50de-4467-4dd6-a11f-4e80576d6bf4", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698758870505, "measure": {"type": "total", "value": 218, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "8cab50de-4467-4dd6-a11f-4e80576d6bf4", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698759239505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "72f1ab48-01f8-4abe-bf83-cd30448455db", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698759239505, "measure": {"type": "total", "value": 357, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "72f1ab48-01f8-4abe-bf83-cd30448455db", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698759322505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "1f69d591-8667-41b5-8122-bb03be7d44d1", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698759322505, "measure": {"type": "total", "value": 445, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "1f69d591-8667-41b5-8122-bb03be7d44d1", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698758586505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "5792c4bb-c99b-4476-a27e-fb52f7a79b02", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698758586505, "measure": {"type": "total", "value": 168, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "5792c4bb-c99b-4476-a27e-fb52f7a79b02", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698757469505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "831801c6-8fa5-48d7-a4e8-a9f648da7419", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698757469505, "measure": {"type": "total", "value": 523, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "831801c6-8fa5-48d7-a4e8-a9f648da7419", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698798925505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "0daf163a-f899-4c88-8f20-09b27d68df74", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698798925505, "measure": {"type": "total", "value": 61, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "0daf163a-f899-4c88-8f20-09b27d68df74", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698797990505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "fa2d7dbb-1918-4919-acfe-cde9e924e812", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698797990505, "measure": {"type": "total", "value": 302, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "fa2d7dbb-1918-4919-acfe-cde9e924e812", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698799175505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "98ef8feb-d3b7-4139-89c1-e7560dca9f10", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698799175505, "measure": {"type": "total", "value": 399, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "98ef8feb-d3b7-4139-89c1-e7560dca9f10", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698800297505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "c2f308af-b794-4059-a41e-bc9781dd120e", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698800297505, "measure": {"type": "total", "value": 469, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "c2f308af-b794-4059-a41e-bc9781dd120e", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698798397505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "d0e061e0-5533-469c-a9f1-30e084f517a5", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698798397505, "measure": {"type": "total", "value": 374, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "d0e061e0-5533-469c-a9f1-30e084f517a5", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698798540505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "133fd254-5191-46f8-b196-b1425df59a15", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698798540505, "measure": {"type": "total", "value": 431, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "133fd254-5191-46f8-b196-b1425df59a15", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698820877505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "fc56aa01-a961-4b12-bbe3-38a3f129db22", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698820877505, "measure": {"type": "total", "value": 224, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "fc56aa01-a961-4b12-bbe3-38a3f129db22", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698821279505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "7c60e16f-34f8-4307-89eb-7e033ae1ec3c", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698821279505, "measure": {"type": "total", "value": 343, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "7c60e16f-34f8-4307-89eb-7e033ae1ec3c", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698821617505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "dc560a49-776d-4e88-8594-2276b0d1a311", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698821617505, "measure": {"type": "total", "value": 78, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "dc560a49-776d-4e88-8594-2276b0d1a311", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698819508505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "05c79a8a-eeb1-4275-b561-cf3334f76eff", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698819508505, "measure": {"type": "total", "value": 26, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "05c79a8a-eeb1-4275-b561-cf3334f76eff", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698821398505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "bb98bf75-92a9-4a41-b7e8-6c6975020c13", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698821398505, "measure": {"type": "total", "value": 211, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "bb98bf75-92a9-4a41-b7e8-6c6975020c13", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698820783505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "a18db050-a133-4ceb-a33a-ba29bf20aa5c", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698820783505, "measure": {"type": "total", "value": 105, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "a18db050-a133-4ceb-a33a-ba29bf20aa5c", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698819852505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "036afdb5-6917-45a0-8f8e-4b22f3db6b14", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698819852505, "measure": {"type": "total", "value": 506, "unit": "seconds"}, "context": {"TenantId": "8462e248-3b96-4977-949c-b093693662ff", "UserId": "4fd145b2-cb44-4db0-a164-04f7e3a920f7", "RequestId": "036afdb5-6917-45a0-8f8e-4b22f3db6b14", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698796924505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "a33e58c6-937f-47e1-a1b7-d899bec9e186", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698796924505, "measure": {"type": "total", "value": 304, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "a33e58c6-937f-47e1-a1b7-d899bec9e186", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698799307505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "1ca2fa3d-d304-4630-8404-d059d97be9e4", "RequestId": "1c8178a0-466f-4f42-a527-d1ce228add8e", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698799307505, "measure": {"type": "total", "value": 58, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "1ca2fa3d-d304-4630-8404-d059d97be9e4", "RequestId": "1c8178a0-466f-4f42-a527-d1ce228add8e", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698800310505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "c3d0ead0-eaf9-4ac7-9a55-f2077e9060db", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698800310505, "measure": {"type": "total", "value": 495, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "c3d0ead0-eaf9-4ac7-9a55-f2077e9060db", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698798419505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "1ca2fa3d-d304-4630-8404-d059d97be9e4", "RequestId": "bf1581a8-6b52-4e86-8491-d3fa866f17f1", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698798419505, "measure": {"type": "total", "value": 166, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "1ca2fa3d-d304-4630-8404-d059d97be9e4", "RequestId": "bf1581a8-6b52-4e86-8491-d3fa866f17f1", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698800257505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "696c5ce7-2a49-42ea-a125-85eb04dc6737", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698800257505, "measure": {"type": "total", "value": 257, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "696c5ce7-2a49-42ea-a125-85eb04dc6737", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698799459505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "a45a0086-4c09-48eb-86f0-8a5cc3a644ac", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698799459505, "measure": {"type": "total", "value": 215, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "a45a0086-4c09-48eb-86f0-8a5cc3a644ac", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698800358505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "c06ac9f0-f0c9-479d-80ae-fa26156384b2", "RequestId": "103fcd33-b0e4-4227-be96-c3cc4ea2bc66", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698800358505, "measure": {"type": "total", "value": 339, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "c06ac9f0-f0c9-479d-80ae-fa26156384b2", "RequestId": "103fcd33-b0e4-4227-be96-c3cc4ea2bc66", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698798811505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "1ca2fa3d-d304-4630-8404-d059d97be9e4", "RequestId": "9f77bb72-09cf-41f1-bfa3-bb048fa245a3", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698798811505, "measure": {"type": "total", "value": 383, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "1ca2fa3d-d304-4630-8404-d059d97be9e4", "RequestId": "9f77bb72-09cf-41f1-bfa3-bb048fa245a3", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698797829505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "1ca2fa3d-d304-4630-8404-d059d97be9e4", "RequestId": "c3d864c2-89f8-4435-a2f6-33126e85d2ce", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698797829505, "measure": {"type": "total", "value": 564, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "1ca2fa3d-d304-4630-8404-d059d97be9e4", "RequestId": "c3d864c2-89f8-4435-a2f6-33126e85d2ce", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698802158505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "99a4713d-be0d-48cc-af8b-2909878afd1a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698802158505, "measure": {"type": "total", "value": 520, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "99a4713d-be0d-48cc-af8b-2909878afd1a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698801864505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "1ca2fa3d-d304-4630-8404-d059d97be9e4", "RequestId": "24554864-d2ed-4305-bacc-a9c14c5c045f", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698801864505, "measure": {"type": "total", "value": 33, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "1ca2fa3d-d304-4630-8404-d059d97be9e4", "RequestId": "24554864-d2ed-4305-bacc-a9c14c5c045f", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698803127505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "13b25f1c-0a82-4251-9fdf-646a388e4de6", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698803127505, "measure": {"type": "total", "value": 407, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "13b25f1c-0a82-4251-9fdf-646a388e4de6", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698800900505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "c06ac9f0-f0c9-479d-80ae-fa26156384b2", "RequestId": "f364cea0-0a43-4a69-81bb-7e9471fc2fb5", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698800900505, "measure": {"type": "total", "value": 227, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "c06ac9f0-f0c9-479d-80ae-fa26156384b2", "RequestId": "f364cea0-0a43-4a69-81bb-7e9471fc2fb5", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698808694505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "1ca2fa3d-d304-4630-8404-d059d97be9e4", "RequestId": "4404896e-0c7f-484e-9b39-1004e026e773", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698808694505, "measure": {"type": "total", "value": 265, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "1ca2fa3d-d304-4630-8404-d059d97be9e4", "RequestId": "4404896e-0c7f-484e-9b39-1004e026e773", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698809438505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "1ca2fa3d-d304-4630-8404-d059d97be9e4", "RequestId": "ed6276a6-eba8-43ff-808e-46da408c0b1b", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698809438505, "measure": {"type": "total", "value": 22, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "1ca2fa3d-d304-4630-8404-d059d97be9e4", "RequestId": "ed6276a6-eba8-43ff-808e-46da408c0b1b", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698809906505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "7ff49fb2-9c1b-4bd2-acfe-623a10c602a4", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698809906505, "measure": {"type": "total", "value": 252, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "7ff49fb2-9c1b-4bd2-acfe-623a10c602a4", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698809304505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "be6b9336-cac1-477b-bd31-012486384cd0", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698809304505, "measure": {"type": "total", "value": 34, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "be6b9336-cac1-477b-bd31-012486384cd0", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698810988505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "fe69039c-951f-478b-866f-121e9f1297e3", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698810988505, "measure": {"type": "total", "value": 477, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "fe69039c-951f-478b-866f-121e9f1297e3", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698807749505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "c06ac9f0-f0c9-479d-80ae-fa26156384b2", "RequestId": "4d8f5523-5607-4ba3-a077-bed05bd64ede", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698807749505, "measure": {"type": "total", "value": 351, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "c06ac9f0-f0c9-479d-80ae-fa26156384b2", "RequestId": "4d8f5523-5607-4ba3-a077-bed05bd64ede", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698810443505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "4fd42b2e-e824-4ed1-af35-8ac6a85029c5", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698810443505, "measure": {"type": "total", "value": 176, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "fc2e1101-f1f4-40dd-842f-20dc9ebedf37", "RequestId": "4fd42b2e-e824-4ed1-af35-8ac6a85029c5", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698809207505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "c06ac9f0-f0c9-479d-80ae-fa26156384b2", "RequestId": "b44cad64-c8d6-4607-b2da-34a67d1743b0", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698809207505, "measure": {"type": "total", "value": 225, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "c06ac9f0-f0c9-479d-80ae-fa26156384b2", "RequestId": "b44cad64-c8d6-4607-b2da-34a67d1743b0", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698762669505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "1ca2fa3d-d304-4630-8404-d059d97be9e4", "RequestId": "30c620ce-241a-4e33-a668-2e6e60704bab", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698762669505, "measure": {"type": "total", "value": 386, "unit": "seconds"}, "context": {"TenantId": "49e27ef7-e3f7-4a04-a432-c7cd01300121", "UserId": "1ca2fa3d-d304-4630-8404-d059d97be9e4", "RequestId": "30c620ce-241a-4e33-a668-2e6e60704bab", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698738838505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "9fdc1e7f-eb7c-4998-b0b8-bcdf01e5bc90", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698738838505, "measure": {"type": "total", "value": 204, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "9fdc1e7f-eb7c-4998-b0b8-bcdf01e5bc90", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698738290505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "f2f1ff8c-df72-494a-8aa8-4aa7d97639c2", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698738290505, "measure": {"type": "total", "value": 357, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "f2f1ff8c-df72-494a-8aa8-4aa7d97639c2", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698736464505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "09b78643-8a7e-4ef0-b998-97ed8e83130e", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698736464505, "measure": {"type": "total", "value": 73, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "09b78643-8a7e-4ef0-b998-97ed8e83130e", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698739182505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "c07b7b62-5563-415b-a159-b26c16ec0fba", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698739182505, "measure": {"type": "total", "value": 569, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "c07b7b62-5563-415b-a159-b26c16ec0fba", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698736156505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "bc7a0aa6-c3bf-466f-a9bf-8f06a8adbf29", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698736156505, "measure": {"type": "total", "value": 591, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "bc7a0aa6-c3bf-466f-a9bf-8f06a8adbf29", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698738153505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "a80e24f3-c19d-493f-8e16-08fb83de678a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698738153505, "measure": {"type": "total", "value": 594, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "a80e24f3-c19d-493f-8e16-08fb83de678a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698738323505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "c2f2f1d2-9b7d-4df9-b235-50717b4d0e47", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698738323505, "measure": {"type": "total", "value": 496, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "c2f2f1d2-9b7d-4df9-b235-50717b4d0e47", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698736099505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "9b2a5583-3e8d-4296-adc3-fed5c0256905", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698736099505, "measure": {"type": "total", "value": 489, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "9b2a5583-3e8d-4296-adc3-fed5c0256905", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698789339505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "9a676de4-448a-43c4-99c1-0fbfd899d1b9", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698789339505, "measure": {"type": "total", "value": 436, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "9a676de4-448a-43c4-99c1-0fbfd899d1b9", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698788834505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "c29998f5-d22a-49f3-a5b0-031701e1c7b2", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698788834505, "measure": {"type": "total", "value": 114, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "c29998f5-d22a-49f3-a5b0-031701e1c7b2", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698786753505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "2a35473b-e7d5-44a0-aa00-5aa25580c549", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698786753505, "measure": {"type": "total", "value": 444, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "2a35473b-e7d5-44a0-aa00-5aa25580c549", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698789535505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "fac92042-5961-4c5f-8ba1-34d9e1c8a618", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698789535505, "measure": {"type": "total", "value": 294, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "fac92042-5961-4c5f-8ba1-34d9e1c8a618", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698787393505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "ffdcb97f-0758-4090-bdea-5681161e14c0", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698787393505, "measure": {"type": "total", "value": 320, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "ffdcb97f-0758-4090-bdea-5681161e14c0", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698812659505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "d3f32b5c-04d4-4ada-9b13-6b3f480f4fa3", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698812659505, "measure": {"type": "total", "value": 40, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "d3f32b5c-04d4-4ada-9b13-6b3f480f4fa3", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698813792505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "d6c879a5-36b2-4bff-83d3-a4430d3cb39d", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698813792505, "measure": {"type": "total", "value": 214, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "d6c879a5-36b2-4bff-83d3-a4430d3cb39d", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698811888505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "af10da0c-e07f-40da-ab30-a6ca1b441e13", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698811888505, "measure": {"type": "total", "value": 448, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "af10da0c-e07f-40da-ab30-a6ca1b441e13", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698812844505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "39fa5b42-e73f-4da7-bf10-e9822048f1ec", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698812844505, "measure": {"type": "total", "value": 411, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "39fa5b42-e73f-4da7-bf10-e9822048f1ec", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698814409505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "4ab972e0-4f14-4198-a92a-30195eee39c2", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698814409505, "measure": {"type": "total", "value": 519, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "4ab972e0-4f14-4198-a92a-30195eee39c2", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698811458505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "73ed6be0-6899-46b2-a87f-3866d194f020", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698811458505, "measure": {"type": "total", "value": 266, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "73ed6be0-6899-46b2-a87f-3866d194f020", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698812764505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "a90bc6ba-f6b1-48a8-95ba-43de4bf481b6", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698812764505, "measure": {"type": "total", "value": 16, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "a90bc6ba-f6b1-48a8-95ba-43de4bf481b6", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698813563505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "58488903-23b8-424e-9230-1ac3dec4b87a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698813563505, "measure": {"type": "total", "value": 179, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "dcb07c3f-7f1e-4651-8159-290e91c7029f", "RequestId": "58488903-23b8-424e-9230-1ac3dec4b87a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698811539505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "e309b662-6d74-4747-af05-86fcffbde1d8", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698811539505, "measure": {"type": "total", "value": 181, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "e309b662-6d74-4747-af05-86fcffbde1d8", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698812273505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "32368e43-298a-43d8-a6b7-b50c37afdc98", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698812273505, "measure": {"type": "total", "value": 98, "unit": "seconds"}, "context": {"TenantId": "a69628d7-45fb-4a2d-9e0f-0d9aea8407c6", "UserId": "aa365805-3b03-481b-89dd-d994c8e38959", "RequestId": "32368e43-298a-43d8-a6b7-b50c37afdc98", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698806756505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "64f6366d-e347-4c59-ab28-dc938984ce99", "UserId": "3523236c-cdf6-4bd7-9c02-65043d454634", "RequestId": "bc381754-a96f-43e2-8d58-578815b4416d", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698806756505, "measure": {"type": "total", "value": 222, "unit": "seconds"}, "context": {"TenantId": "64f6366d-e347-4c59-ab28-dc938984ce99", "UserId": "3523236c-cdf6-4bd7-9c02-65043d454634", "RequestId": "bc381754-a96f-43e2-8d58-578815b4416d", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698804687505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "64f6366d-e347-4c59-ab28-dc938984ce99", "UserId": "32fbde0a-3e3b-4702-8cd6-9ad4c262b382", "RequestId": "19ab1c6a-cfa8-4ca3-8361-03c7e9963334", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698804687505, "measure": {"type": "total", "value": 579, "unit": "seconds"}, "context": {"TenantId": "64f6366d-e347-4c59-ab28-dc938984ce99", "UserId": "32fbde0a-3e3b-4702-8cd6-9ad4c262b382", "RequestId": "19ab1c6a-cfa8-4ca3-8361-03c7e9963334", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698805493505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "64f6366d-e347-4c59-ab28-dc938984ce99", "UserId": "3523236c-cdf6-4bd7-9c02-65043d454634", "RequestId": "3938319e-8ffb-476f-bc92-22bda976943c", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698805493505, "measure": {"type": "total", "value": 468, "unit": "seconds"}, "context": {"TenantId": "64f6366d-e347-4c59-ab28-dc938984ce99", "UserId": "3523236c-cdf6-4bd7-9c02-65043d454634", "RequestId": "3938319e-8ffb-476f-bc92-22bda976943c", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698805586505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "64f6366d-e347-4c59-ab28-dc938984ce99", "UserId": "32fbde0a-3e3b-4702-8cd6-9ad4c262b382", "RequestId": "7d610e60-84ae-4956-9dc8-26e35eec59df", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698805586505, "measure": {"type": "total", "value": 189, "unit": "seconds"}, "context": {"TenantId": "64f6366d-e347-4c59-ab28-dc938984ce99", "UserId": "32fbde0a-3e3b-4702-8cd6-9ad4c262b382", "RequestId": "7d610e60-84ae-4956-9dc8-26e35eec59df", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698804316505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "64f6366d-e347-4c59-ab28-dc938984ce99", "UserId": "3523236c-cdf6-4bd7-9c02-65043d454634", "RequestId": "d1f49284-ebfa-450c-b0be-6ee7e72ded83", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698804316505, "measure": {"type": "total", "value": 394, "unit": "seconds"}, "context": {"TenantId": "64f6366d-e347-4c59-ab28-dc938984ce99", "UserId": "3523236c-cdf6-4bd7-9c02-65043d454634", "RequestId": "d1f49284-ebfa-450c-b0be-6ee7e72ded83", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698795126505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "e8b189ff-efa6-446b-8bff-074b0256f0a4", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698795126505, "measure": {"type": "total", "value": 524, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "e8b189ff-efa6-446b-8bff-074b0256f0a4", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698796402505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "8899cc72-9b64-4995-b27e-568c6c33f235", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698796402505, "measure": {"type": "total", "value": 560, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "8899cc72-9b64-4995-b27e-568c6c33f235", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698794265505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "9354be3e-ff49-427f-a347-9e18050a94da", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698794265505, "measure": {"type": "total", "value": 336, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "9354be3e-ff49-427f-a347-9e18050a94da", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698794674505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "d05f1a96-f6b5-4149-986c-0c963ca437fb", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698794674505, "measure": {"type": "total", "value": 118, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "d05f1a96-f6b5-4149-986c-0c963ca437fb", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698795990505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "641e94bc-bb70-4b15-bb7f-089c3ccfc75a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698795990505, "measure": {"type": "total", "value": 277, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "641e94bc-bb70-4b15-bb7f-089c3ccfc75a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698794183505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "2d3e916e-269c-44bf-8b55-0c9564c3bf1d", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698794183505, "measure": {"type": "total", "value": 470, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "2d3e916e-269c-44bf-8b55-0c9564c3bf1d", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698758140505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "a8698e6e-269a-4bdd-914d-690ac05d9acb", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698758140505, "measure": {"type": "total", "value": 399, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "a8698e6e-269a-4bdd-914d-690ac05d9acb", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698757737505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "46988af9-30ee-4ff0-9e32-25d8c2cdeb7a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698757737505, "measure": {"type": "total", "value": 45, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "46988af9-30ee-4ff0-9e32-25d8c2cdeb7a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698758532505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "95ce025a-73ec-4343-8a86-dc275113c00b", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698758532505, "measure": {"type": "total", "value": 166, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "95ce025a-73ec-4343-8a86-dc275113c00b", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698759957505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "55544bb2-5db6-4179-a494-f4606ef2ef2a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698759957505, "measure": {"type": "total", "value": 67, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "55544bb2-5db6-4179-a494-f4606ef2ef2a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698757867505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "ba1517a8-bcda-4379-977e-15c301fbf53f", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698757867505, "measure": {"type": "total", "value": 112, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "ba1517a8-bcda-4379-977e-15c301fbf53f", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698758649505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "7d186d8e-961a-46d2-8f4c-a8dadf8252f8", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698758649505, "measure": {"type": "total", "value": 12, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "7d186d8e-961a-46d2-8f4c-a8dadf8252f8", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698760067505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "293aa884-4f42-42c4-9fed-5b06736a5062", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698760067505, "measure": {"type": "total", "value": 16, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "293aa884-4f42-42c4-9fed-5b06736a5062", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698757962505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "ecba01dd-fd84-4066-af5f-a5bed76e89bf", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698757962505, "measure": {"type": "total", "value": 72, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "ecba01dd-fd84-4066-af5f-a5bed76e89bf", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698759008505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "ef214421-2d44-4f85-a4cf-90c65badccb2", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698759008505, "measure": {"type": "total", "value": 319, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "ef214421-2d44-4f85-a4cf-90c65badccb2", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698759984505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "4b62dbe5-0fc3-4df1-94ed-28d9a0735f33", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698759984505, "measure": {"type": "total", "value": 116, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "4b62dbe5-0fc3-4df1-94ed-28d9a0735f33", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698741179505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "fd0b33ca-1d67-43cf-b075-0fbe02196c0c", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698741179505, "measure": {"type": "total", "value": 392, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "fd0b33ca-1d67-43cf-b075-0fbe02196c0c", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698741995505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "ad7b5b64-cc9a-4b5e-ad72-e5b4ad87fa6a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698741995505, "measure": {"type": "total", "value": 65, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "ad7b5b64-cc9a-4b5e-ad72-e5b4ad87fa6a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698737416505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "4bf7792e-441e-4ada-944b-037a67f482ab", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698737416505, "measure": {"type": "total", "value": 119, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "4bf7792e-441e-4ada-944b-037a67f482ab", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698737852505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "0baaef69-0aa5-414c-b755-f30235a6e996", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698737852505, "measure": {"type": "total", "value": 525, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "0baaef69-0aa5-414c-b755-f30235a6e996", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698739037505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "74263ea5-e001-4033-924c-6747d22883cf", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698739037505, "measure": {"type": "total", "value": 78, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "74263ea5-e001-4033-924c-6747d22883cf", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698737697505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "98037d29-8f76-4a29-a4bc-d8373956a62a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698737697505, "measure": {"type": "total", "value": 418, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "98037d29-8f76-4a29-a4bc-d8373956a62a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698738182505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "88e2d740-fb48-419c-a081-a0ea404be9c6", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698738182505, "measure": {"type": "total", "value": 554, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "88e2d740-fb48-419c-a081-a0ea404be9c6", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698738498505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "f1c35310-98fd-433a-9f26-e04848803180", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698738498505, "measure": {"type": "total", "value": 154, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "f1c35310-98fd-433a-9f26-e04848803180", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698735968505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "dfefd73e-ac57-4796-b239-6ba798f9803e", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698735968505, "measure": {"type": "total", "value": 461, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "dfefd73e-ac57-4796-b239-6ba798f9803e", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698736305505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "a079281c-f331-4091-bfe2-e79b9c5daca7", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698736305505, "measure": {"type": "total", "value": 25, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "a079281c-f331-4091-bfe2-e79b9c5daca7", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698736650505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "b58a8fb3-2c29-4f99-aaf6-1d7260053130", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698736650505, "measure": {"type": "total", "value": 179, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "b58a8fb3-2c29-4f99-aaf6-1d7260053130", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698737925505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "2558fd27-544b-40f5-ae35-fe4df01373d2", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698737925505, "measure": {"type": "total", "value": 282, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "2558fd27-544b-40f5-ae35-fe4df01373d2", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698754104505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "54eb8166-e83e-465b-9238-c1b8daf80bdd", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698754104505, "measure": {"type": "total", "value": 98, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "54eb8166-e83e-465b-9238-c1b8daf80bdd", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698754024505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "89efa0d1-b995-4f66-8f7f-1d4e24e41c36", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698754024505, "measure": {"type": "total", "value": 137, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "89efa0d1-b995-4f66-8f7f-1d4e24e41c36", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698755230505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "9188cd4a-9322-4e73-a29a-97fdc2ad6763", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698755230505, "measure": {"type": "total", "value": 69, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "9188cd4a-9322-4e73-a29a-97fdc2ad6763", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698754199505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "b2e98f21-d690-478c-9770-93dec51c070d", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698754199505, "measure": {"type": "total", "value": 544, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "b2e98f21-d690-478c-9770-93dec51c070d", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698756561505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "8edf5794-6330-4ec8-9580-283cca1810f9", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698756561505, "measure": {"type": "total", "value": 311, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "8edf5794-6330-4ec8-9580-283cca1810f9", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698753638505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "28dbab69-e658-4a7d-a114-ef39ffc750ed", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698753638505, "measure": {"type": "total", "value": 72, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "28dbab69-e658-4a7d-a114-ef39ffc750ed", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698754917505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "dae7a7be-0f6b-43d6-9fcb-0d8481792f0d", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698754917505, "measure": {"type": "total", "value": 557, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "dae7a7be-0f6b-43d6-9fcb-0d8481792f0d", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698753833505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "97be3759-9ab2-4a98-816f-6ae6aff214a7", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698753833505, "measure": {"type": "total", "value": 204, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "97be3759-9ab2-4a98-816f-6ae6aff214a7", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698753687505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "961b8b0d-20ff-42b9-acd9-7dd77ca360b1", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698753687505, "measure": {"type": "total", "value": 424, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "179017bc-b5a6-4bf9-8dde-ba496d45c0b5", "RequestId": "961b8b0d-20ff-42b9-acd9-7dd77ca360b1", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698755832505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "5412b25b-c652-482a-bcfc-fb8539965569", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698755832505, "measure": {"type": "total", "value": 332, "unit": "seconds"}, "context": {"TenantId": "9d7e463e-f93b-4b75-a869-b09b3b82354d", "UserId": "2454f956-d725-4ae1-a6e1-b0091999854d", "RequestId": "5412b25b-c652-482a-bcfc-fb8539965569", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698794092505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "292ecfd6-9082-4071-b812-4d6e99c13c84", "UserId": "73e45655-0050-44a7-b0f6-e510d940bdc4", "RequestId": "26ec28d2-018b-432f-9aba-933816a8bcbc", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698794092505, "measure": {"type": "total", "value": 380, "unit": "seconds"}, "context": {"TenantId": "292ecfd6-9082-4071-b812-4d6e99c13c84", "UserId": "73e45655-0050-44a7-b0f6-e510d940bdc4", "RequestId": "26ec28d2-018b-432f-9aba-933816a8bcbc", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698796607505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "292ecfd6-9082-4071-b812-4d6e99c13c84", "UserId": "73e45655-0050-44a7-b0f6-e510d940bdc4", "RequestId": "8f68b447-002d-4dd3-ac5f-e0b9e1188af8", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698796607505, "measure": {"type": "total", "value": 202, "unit": "seconds"}, "context": {"TenantId": "292ecfd6-9082-4071-b812-4d6e99c13c84", "UserId": "73e45655-0050-44a7-b0f6-e510d940bdc4", "RequestId": "8f68b447-002d-4dd3-ac5f-e0b9e1188af8", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698808563505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "292ecfd6-9082-4071-b812-4d6e99c13c84", "UserId": "6bbb8ccc-aab7-4fa3-9e12-6ce24eaaafe9", "RequestId": "a5e41c98-e92c-4172-8d78-b0ade432a39a", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698808563505, "measure": {"type": "total", "value": 373, "unit": "seconds"}, "context": {"TenantId": "292ecfd6-9082-4071-b812-4d6e99c13c84", "UserId": "6bbb8ccc-aab7-4fa3-9e12-6ce24eaaafe9", "RequestId": "a5e41c98-e92c-4172-8d78-b0ade432a39a", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698809785505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "292ecfd6-9082-4071-b812-4d6e99c13c84", "UserId": "34ed31b1-0ab2-4502-a1ef-02732902c21e", "RequestId": "eb7a41a5-0447-47b4-863f-57410e5e64b4", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698809785505, "measure": {"type": "total", "value": 372, "unit": "seconds"}, "context": {"TenantId": "292ecfd6-9082-4071-b812-4d6e99c13c84", "UserId": "34ed31b1-0ab2-4502-a1ef-02732902c21e", "RequestId": "eb7a41a5-0447-47b4-863f-57410e5e64b4", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698749074505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "292ecfd6-9082-4071-b812-4d6e99c13c84", "UserId": "73e45655-0050-44a7-b0f6-e510d940bdc4", "RequestId": "885c09a9-98f9-4ead-8a75-55619f45098c", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698749074505, "measure": {"type": "total", "value": 158, "unit": "seconds"}, "context": {"TenantId": "292ecfd6-9082-4071-b812-4d6e99c13c84", "UserId": "73e45655-0050-44a7-b0f6-e510d940bdc4", "RequestId": "885c09a9-98f9-4ead-8a75-55619f45098c", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698749947505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "292ecfd6-9082-4071-b812-4d6e99c13c84", "UserId": "34ed31b1-0ab2-4502-a1ef-02732902c21e", "RequestId": "2dbf945c-b626-4724-9644-3ff17393174b", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698749947505, "measure": {"type": "total", "value": 494, "unit": "seconds"}, "context": {"TenantId": "292ecfd6-9082-4071-b812-4d6e99c13c84", "UserId": "34ed31b1-0ab2-4502-a1ef-02732902c21e", "RequestId": "2dbf945c-b626-4724-9644-3ff17393174b", "Application": "Job Service", "Action": "executeJob"}}, {"name": "Jobs", "timestamp": 1698747224505, "measure": {"type": "count", "value": 1}, "context": {"TenantId": "292ecfd6-9082-4071-b812-4d6e99c13c84", "UserId": "6bbb8ccc-aab7-4fa3-9e12-6ce24eaaafe9", "RequestId": "b0ed9783-0b91-4a84-b35e-05acbbabd8d1", "Application": "Job Service", "Action": "scheduleJob"}}, {"name": "Job Duration", "timestamp": 1698747224505, "measure": {"type": "total", "value": 545, "unit": "seconds"}, "context": {"TenantId": "292ecfd6-9082-4071-b812-4d6e99c13c84", "UserId": "6bbb8ccc-aab7-4fa3-9e12-6ce24eaaafe9", "RequestId": "b0ed9783-0b91-4a84-b35e-05acbbabd8d1", "Application": "Job Service", "Action": "executeJob"}}] \ No newline at end of file diff --git a/services/metrics-service/update.sh b/services/metrics-service/update.sh index ddad057d..98598a02 100755 --- a/services/metrics-service/update.sh +++ b/services/metrics-service/update.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). @@ -17,43 +17,45 @@ if [ -z $1 ]; then echo "Usage: $0 [Lambda Folder]" exit 2 fi - -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -echo "AWS Region = $MY_AWS_REGION" - ENVIRONMENT=$1 -LAMBDA_STAGE_FOLDER=$2 -if [ -z $LAMBDA_STAGE_FOLDER ]; then - LAMBDA_STAGE_FOLDER="lambdas" -fi +FUNCTIONS=($2) LAMBDA_CODE=MetricsService-lambda.zip -#set this for V2 AWS CLI to disable paging +# Disable AWS CLI paging export AWS_PAGER="" +MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') +echo "AWS Region = $MY_AWS_REGION" + SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" if [ -z $SAAS_BOOST_BUCKET ]; then echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" exit 1 fi +LAMBDAS_FOLDER=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/LAMBDAS_FOLDER" --query 'Parameter.Value' --output text 2>/dev/null) +if [ -z $LAMBDAS_FOLDER ]; then + LAMBDAS_FOLDER="lambdas/" +fi +echo "Lambdas folder = $LAMBDAS_FOLDER" +echo "Function code = $LAMBDA_CODE" # Do a fresh build of the project -mvn +mvn -Dspotbugs.skip -Djacoco.skip if [ $? -ne 0 ]; then echo "Error building project" exit 1 fi # And copy it up to S3 -aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ +aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDAS_FOLDER # Find all the functions for this microservice -eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-metrics-\`)] | [].FunctionName' --output text"\) -FUNCTIONS=($FUNCTIONS) +if [ -z $FUNCTIONS ]; then + eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-metrics-\`)] | [].FunctionName' --output text"\) + FUNCTIONS=($FUNCTIONS) +fi for FX in "${FUNCTIONS[@]}"; do - if [[ ! $FX == *"listener"* ]]; then - printf "Updating function code for %s\n" $FX - aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "$FX" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE - fi + printf "Updating function code for %s\n" $FX + aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "$FX" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key "${LAMBDAS_FOLDER}${LAMBDA_CODE}" done diff --git a/services/onboarding-service/pom.xml b/services/onboarding-service/pom.xml index 80b88a6a..9a449277 100644 --- a/services/onboarding-service/pom.xml +++ b/services/onboarding-service/pom.xml @@ -33,7 +33,8 @@ limitations under the License. - 24 + ${project.basedir}/../.. + 0 @@ -46,6 +47,17 @@ limitations under the License. org.apache.maven.plugins maven-surefire-plugin + + + us-east-1 + test + test + + + + + org.jacoco + jacoco-maven-plugin org.apache.maven.plugins @@ -68,19 +80,13 @@ limitations under the License. 0.1.0 + ${project.basedir}/src/main/resources/spotbugs-exclude.xml - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - com.amazon.aws.partners.saasfactory.saasboost ApiGatewayHelper @@ -103,21 +109,6 @@ limitations under the License. - - software.amazon.awssdk - cloudformation - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - software.amazon.awssdk eventbridge @@ -133,21 +124,6 @@ limitations under the License. - - software.amazon.awssdk - ecr - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - software.amazon.awssdk s3 @@ -163,21 +139,6 @@ limitations under the License. - - software.amazon.awssdk - route53 - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - software.amazon.awssdk sqs @@ -193,35 +154,5 @@ limitations under the License. - - software.amazon.awssdk - codepipeline - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - software.amazon.awssdk - directory - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - diff --git a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AbstractStackParameters.java b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AbstractStackParameters.java deleted file mode 100644 index f02e8dde..00000000 --- a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AbstractStackParameters.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.amazon.aws.partners.saasfactory.saasboost; - -import software.amazon.awssdk.services.cloudformation.model.Parameter; - -import java.util.*; - -public abstract class AbstractStackParameters extends Properties { - - private AbstractStackParameters() { - } - - public AbstractStackParameters(Properties defaults) { - super(defaults); - } - - @Override - public synchronized Object setProperty(String key, String value) { - if (value == null) { - value = getProperty(key); - } - return super.setProperty(key, value); - } - - public final List forCreate() { - List parameters = new ArrayList<>(); - for (String parameter : stringPropertyNames()) { - parameters.add( - Parameter.builder() - .parameterKey(parameter) - .parameterValue(getProperty(parameter)) - .build() - ); - } - validateForCreate(); - return parameters; - } - - public final List forUpdate(Map updateParameters) { - for (Map.Entry updateParam : updateParameters.entrySet()) { - setProperty(updateParam.getKey(), updateParam.getValue()); - } - List parameters = new ArrayList<>(); - for (String parameter : stringPropertyNames()) { - Parameter.Builder builder = Parameter.builder(); - builder.parameterKey(parameter); - if (updateParameters.containsKey(parameter)) { - builder.parameterValue(getProperty(parameter)); - } else { - builder.usePreviousValue(true); - } - parameters.add(builder.build()); - } - validateForUpdate(); - return parameters; - } - - protected abstract void validateForCreate(); - - protected void validateForUpdate() { - // CloudFormation SDK stack operations fail if any parameters are null - List invalidParameters = new ArrayList<>(); - for (String parameter : stringPropertyNames()) { - if (null == getProperty(parameter)) { - invalidParameters.add(parameter); - } - } - if (!invalidParameters.isEmpty()) { - throw new RuntimeException("NULL CloudFormation parameters " + String.join(",", invalidParameters)); - } - } -} diff --git a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CoreStackParameters.java b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CoreStackParameters.java deleted file mode 100644 index 80d14f26..00000000 --- a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CoreStackParameters.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.amazon.aws.partners.saasfactory.saasboost; - -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -public class CoreStackParameters extends AbstractStackParameters { - - static Properties DEFAULTS = new Properties(); - static final List REQUIRED_FOR_CREATE = List.of("SaaSBoostBucket", "Environment", "LambdaSourceFolder", - "Tier", "SystemIdentityProvider", "AdminUsername", "AdminEmailAddress", "PublicApiStage", "PrivateApiStage", - "Version", "CreateMacroResources"); - - static { - DEFAULTS.put("SaaSBoostBucket", ""); - DEFAULTS.put("LambdaSourceFolder", "lambdas"); - DEFAULTS.put("Environment", ""); - DEFAULTS.put("SystemIdentityProvider", "COGNITO"); - DEFAULTS.put("SystemIdentityProviderDomain", ""); - DEFAULTS.put("SystemIdentityProviderHostedZone", ""); - DEFAULTS.put("SystemIdentityProviderCertificate", ""); - DEFAULTS.put("AdminWebAppDomain", ""); - DEFAULTS.put("AdminWebAppHostedZone", ""); - DEFAULTS.put("AdminWebAppCertificate", ""); - DEFAULTS.put("AdminUsername", "admin"); - DEFAULTS.put("AdminEmailAddress", ""); - DEFAULTS.put("PublicApiStage", "v1"); - DEFAULTS.put("PrivateApiStage", "v1"); - DEFAULTS.put("Version", ""); - DEFAULTS.put("ApplicationServices", ""); - DEFAULTS.put("AppExtensions", ""); - DEFAULTS.put("CreateMacroResources", "false"); - } - - public CoreStackParameters() { - super(DEFAULTS); - } - - @Override - protected void validateForCreate() { - List invalidParameters = new ArrayList<>(); - for (String requiredParameter : REQUIRED_FOR_CREATE) { - if (Utils.isBlank(getProperty(requiredParameter))) { - invalidParameters.add(requiredParameter); - } - } - if (!invalidParameters.isEmpty()) { - throw new RuntimeException("Missing values for required parameters " - + String.join(",", invalidParameters)); - } - } -} diff --git a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Onboarding.java b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Onboarding.java index 266c92aa..2177517d 100644 --- a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Onboarding.java +++ b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Onboarding.java @@ -17,10 +17,7 @@ package com.amazon.aws.partners.saasfactory.saasboost; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; import java.util.UUID; -import java.util.stream.Collectors; public class Onboarding { @@ -30,9 +27,7 @@ public class Onboarding { private OnboardingStatus status; private UUID tenantId; private OnboardingRequest request; - private List stacks = new ArrayList<>(); private String zipFile; - private boolean ecsClusterLocked; public Onboarding() { } @@ -85,10 +80,6 @@ public void setRequest(OnboardingRequest request) { this.request = request; } - public List getStacks() { - return stacks; - } - public void setZipFile(String zipFile) { this.zipFile = zipFile; } @@ -97,101 +88,4 @@ public String getZipFile() { return zipFile; } - public void setStacks(List stacks) { - this.stacks = stacks != null ? new ArrayList<>(stacks) : new ArrayList<>(); - } - - public void addStack(OnboardingStack stack) { - if (stack != null) { - this.stacks.add(stack); - } - } - - public OnboardingStack baseStack() { - OnboardingStack baseStack = null; - for (OnboardingStack stack : getStacks()) { - if (stack.isBaseStack()) { - baseStack = stack; - break; - } - } - return baseStack; - } - - public boolean isEcsClusterLocked() { - return ecsClusterLocked; - } - - public void setEcsClusterLocked(boolean locked) { - this.ecsClusterLocked = locked; - } - - public boolean hasBaseStacks() { - return !getStacks() - .stream() - .filter(OnboardingStack::isBaseStack) - .collect(Collectors.toList()) - .isEmpty(); - } - - public boolean hasAppStacks() { - return !getStacks() - .stream() - .filter(s -> !s.isBaseStack()) - .collect(Collectors.toList()) - .isEmpty(); - } - - public boolean appStacksDeleted() { - return !hasAppStacks() || getStacks() - .stream() - .filter(s -> !s.isBaseStack()) - .filter(s -> !"DELETE_COMPLETE".equals(s.getStatus())) - .collect(Collectors.toList()) - .isEmpty(); - } - - public boolean baseStacksComplete() { - return stacksComplete(true); - } - - public boolean stacksDeployed() { - boolean deployed = true; - for (OnboardingStack stack : getStacks()) { - if (!stack.isDeployed()) { - deployed = false; - break; - } - } - return deployed; - } - - public boolean stacksComplete() { - return stacksComplete(false); - } - - protected boolean stacksComplete(boolean baseStacks) { - boolean complete = false; - if (!getStacks().isEmpty()) { - if (baseStacks && !hasBaseStacks()) { - // If there are no base stacks, then base stacks can't be complete - complete = false; - } else { - if (baseStacks) { - // All base stacks have to be complete - complete = getStacks().stream() - .filter(stack -> stack.isBaseStack() && !stack.isComplete()) - .collect(Collectors.toList()) - .isEmpty(); - } else { - // All stacks have to be complete - complete = getStacks().stream() - .filter(stack -> !stack.isComplete()) - .collect(Collectors.toList()) - .isEmpty(); - } - } - } - return complete; - } } diff --git a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingAppStackParameters.java b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingAppStackParameters.java deleted file mode 100644 index 987ccb73..00000000 --- a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingAppStackParameters.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.amazon.aws.partners.saasfactory.saasboost; - -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -public class OnboardingAppStackParameters extends AbstractStackParameters { - - static final Properties DEFAULTS = new Properties(); - static final List REQUIRED_FOR_CREATE = List.of("Environment", "TenantId", "Tier", "VPC", "SubnetPrivateA", - "SubnetPrivateB", "ECSCluster", "ECSSecurityGroup", "ContainerRepository", "ContainerRepositoryTag"); - - static { - DEFAULTS.put("Environment", ""); - DEFAULTS.put("TenantId", ""); - DEFAULTS.put("Tier", ""); - DEFAULTS.put("ServiceName", ""); - DEFAULTS.put("ServiceResourceName", ""); - DEFAULTS.put("ContainerRepository", ""); - DEFAULTS.put("ContainerRepositoryTag", "latest"); - DEFAULTS.put("ECSCluster", ""); - DEFAULTS.put("EnableECSExec", "false"); - DEFAULTS.put("PubliclyAddressable", "true"); - DEFAULTS.put("PublicPathRoute", "/*"); - DEFAULTS.put("PublicPathRulePriority", "1"); - DEFAULTS.put("VPC", ""); - DEFAULTS.put("SubnetPrivateA", ""); - DEFAULTS.put("SubnetPrivateB", ""); - DEFAULTS.put("PrivateRouteTable", ""); - DEFAULTS.put("ServiceDiscoveryNamespace", ""); - DEFAULTS.put("ECSLoadBalancerHttpListener", ""); - DEFAULTS.put("ECSLoadBalancerHttpsListener", ""); - DEFAULTS.put("ECSSecurityGroup", ""); - DEFAULTS.put("ContainerOS", ""); - DEFAULTS.put("ClusterInstanceType", ""); - DEFAULTS.put("TaskLaunchType", ""); - DEFAULTS.put("TaskMemory", "1024"); - DEFAULTS.put("TaskCPU", "512"); - DEFAULTS.put("MinTaskCount", "1"); - DEFAULTS.put("MaxTaskCount", "1"); - DEFAULTS.put("MinAutoScalingGroupSize", "1"); - DEFAULTS.put("MaxAutoScalingGroupSize", "1"); - DEFAULTS.put("ContainerPort", "0"); - DEFAULTS.put("ContainerHealthCheckPath", ""); - DEFAULTS.put("UseRDS", "false"); - DEFAULTS.put("RDSInstanceClass", ""); - DEFAULTS.put("RDSEngine", ""); - DEFAULTS.put("RDSEngineVersion", ""); - DEFAULTS.put("RDSParameterGroupFamily", ""); - DEFAULTS.put("RDSUsername", ""); - DEFAULTS.put("RDSPasswordParam", ""); - DEFAULTS.put("RDSPort", ""); - DEFAULTS.put("RDSDatabase", ""); - DEFAULTS.put("RDSBootstrap", ""); - DEFAULTS.put("MetricsStream", ""); - DEFAULTS.put("EventBus", ""); - DEFAULTS.put("FileSystemMountPoint", ""); - DEFAULTS.put("UseEFS", "false"); - DEFAULTS.put("EncryptEFS", "true"); - DEFAULTS.put("EFSLifecyclePolicy", "NEVER"); - DEFAULTS.put("UseFSx", "false"); - DEFAULTS.put("ActiveDirectoryId", ""); - DEFAULTS.put("ActiveDirectoryDnsIps", ""); - DEFAULTS.put("ActiveDirectoryDnsName", ""); - DEFAULTS.put("ActiveDirectoryCredentials", ""); - DEFAULTS.put("FSxFileSystemType", "FSX_WINDOWS"); - DEFAULTS.put("FSxWindowsMountDrive", "G:"); - DEFAULTS.put("FileSystemStorage", "0"); - DEFAULTS.put("FileSystemThroughput", "0"); - DEFAULTS.put("FSxBackupRetention", "0"); - DEFAULTS.put("FSxDailyBackupTime", ""); - DEFAULTS.put("FSxWeeklyMaintenanceTime", ""); - DEFAULTS.put("OntapVolumeSize", "20"); - DEFAULTS.put("Disable", "false"); - DEFAULTS.put("OnboardingDdbTable", ""); - DEFAULTS.put("TenantStorageBucket", ""); - DEFAULTS.put("WIN2022FULL", "/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-ECS_Optimized/image_id"); - DEFAULTS.put("WIN2022CORE", "/aws/service/ami-windows-latest/Windows_Server-2022-English-Core-ECS_Optimized/image_id"); - DEFAULTS.put("WIN2019FULL", "/aws/service/ami-windows-latest/Windows_Server-2019-English-Full-ECS_Optimized/image_id"); - DEFAULTS.put("WIN2019CORE", "/aws/service/ami-windows-latest/Windows_Server-2019-English-Core-ECS_Optimized/image_id"); - DEFAULTS.put("WIN2016FULL", "/aws/service/ami-windows-latest/Windows_Server-2016-English-Full-ECS_Optimized/image_id"); - DEFAULTS.put("AMZNLINUX2", "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id"); - } - - public OnboardingAppStackParameters() { - super(DEFAULTS); - } - - @Override - protected void validateForCreate() { - List invalidParameters = new ArrayList<>(); - if (Utils.isBlank(getProperty("ECSLoadBalancerHttpListener")) - && Utils.isBlank(getProperty("ECSLoadBalancerHttpsListener"))) { - invalidParameters.add("ECSLoadBalancerHttpListener"); - invalidParameters.add("ECSLoadBalancerHttpsListener"); - } - for (String requiredParameter : REQUIRED_FOR_CREATE) { - if ("ECSLoadBalancerHttpListener".equals(requiredParameter)) { - continue; - } - if ("ECSLoadBalancerHttpsListener".equals(requiredParameter)) { - continue; - } - if (Utils.isBlank(getProperty(requiredParameter))) { - invalidParameters.add(requiredParameter); - } - } - if (!invalidParameters.isEmpty()) { - throw new RuntimeException("Missing values for required parameters " - + String.join(",", invalidParameters)); - } - } -} diff --git a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingBaseStackParameters.java b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingBaseStackParameters.java deleted file mode 100644 index 3892101e..00000000 --- a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingBaseStackParameters.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.amazon.aws.partners.saasfactory.saasboost; - -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -public class OnboardingBaseStackParameters extends AbstractStackParameters { - - static final Properties DEFAULTS = new Properties(); - static final List REQUIRED_FOR_CREATE = List.of("Environment", "TenantId", "Tier", "CidrPrefix"); - - static { - DEFAULTS.put("Environment", ""); - DEFAULTS.put("DomainName", ""); - DEFAULTS.put("HostedZoneId", ""); - DEFAULTS.put("SSLCertificateArn", ""); - DEFAULTS.put("TenantId", ""); - DEFAULTS.put("TenantSubDomain", ""); - DEFAULTS.put("CidrPrefix", ""); - DEFAULTS.put("Tier", ""); - DEFAULTS.put("PrivateServices", "false"); - DEFAULTS.put("DeployActiveDirectory", "false"); - } - - public OnboardingBaseStackParameters() { - super(DEFAULTS); - } - - @Override - protected void validateForCreate() { - List invalidParameters = new ArrayList<>(); - for (String requiredParameter : REQUIRED_FOR_CREATE) { - if (Utils.isBlank(getProperty(requiredParameter))) { - invalidParameters.add(requiredParameter); - } - } - if (!invalidParameters.isEmpty()) { - throw new RuntimeException("Missing values for required parameters " - + String.join(",", invalidParameters)); - } - } -} diff --git a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingServiceDAL.java b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingDataAccessLayer.java similarity index 58% rename from services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingServiceDAL.java rename to services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingDataAccessLayer.java index 7d6fe0ef..4a5007f5 100644 --- a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingServiceDAL.java +++ b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingDataAccessLayer.java @@ -27,21 +27,18 @@ import java.util.*; import java.util.stream.Collectors; -public class OnboardingServiceDAL { +public class OnboardingDataAccessLayer { - private static final Logger LOGGER = LoggerFactory.getLogger(OnboardingServiceDAL.class); - private static final String ONBOARDING_TABLE = System.getenv("ONBOARDING_TABLE"); - private static final String CIDR_BLOCK_TABLE = System.getenv("CIDR_BLOCK_TABLE"); + private static final Logger LOGGER = LoggerFactory.getLogger(OnboardingDataAccessLayer.class); + private final String onboardingTable; private final DynamoDbClient ddb; - public OnboardingServiceDAL() { + public OnboardingDataAccessLayer(DynamoDbClient ddb, String onboardingTable) { final long startTimeMillis = System.currentTimeMillis(); - if (Utils.isBlank(ONBOARDING_TABLE)) { - throw new IllegalStateException("Missing required environment variable ONBOARDING_TABLE"); - } - this.ddb = Utils.sdkClient(DynamoDbClient.builder(), DynamoDbClient.SERVICE_NAME); + this.ddb = ddb; + this.onboardingTable = onboardingTable; // Cold start performance hack -- take the TLS hit for the client in the constructor - this.ddb.describeTable(r -> r.tableName(ONBOARDING_TABLE)); + this.ddb.describeTable(r -> r.tableName(this.onboardingTable)); LOGGER.info("Constructor init: {}", System.currentTimeMillis() - startTimeMillis); } @@ -50,7 +47,7 @@ public List getOnboardings() { LOGGER.info("OnboardingServiceDAL::getOnboardings"); List onboardings = new ArrayList<>(); try { - ScanResponse response = ddb.scan(request -> request.tableName(ONBOARDING_TABLE)); + ScanResponse response = ddb.scan(request -> request.tableName(onboardingTable)); response.items().forEach(item -> onboardings.add(fromAttributeValueMap(item)) ); @@ -74,7 +71,7 @@ public Onboarding getOnboarding(String onboardingId) { try { Map key = new HashMap<>(); key.put("id", AttributeValue.builder().s(onboardingId).build()); - GetItemResponse response = ddb.getItem(request -> request.tableName(ONBOARDING_TABLE).key(key)); + GetItemResponse response = ddb.getItem(request -> request.tableName(onboardingTable).key(key)); item = response.item(); } catch (DynamoDbException e) { LOGGER.error("OnboardingServiceDAL::getOnboarding " + Utils.getFullStackTrace(e)); @@ -99,10 +96,10 @@ public Onboarding getOnboardingByTenantId(String tenantId) { filter = "tenant_id = :tenantId"; } ScanResponse scan = ddb.scan(ScanRequest.builder() - .tableName(ONBOARDING_TABLE) + .tableName(onboardingTable) .filterExpression(filter) .expressionAttributeValues( - Collections.singletonMap(":tenantId", AttributeValue.builder().s(tenantId).build()) + Map.of(":tenantId", AttributeValue.builder().s(tenantId).build()) ) .build() ); @@ -110,7 +107,8 @@ public Onboarding getOnboardingByTenantId(String tenantId) { LOGGER.info("Scanning onboarding for tenant id " + tenantId); onboarding = fromAttributeValueMap(scan.items().get(0)); } else { - LOGGER.info("Onboarding scan for tenant id " + tenantId + " returned " + scan.items().size() + " results"); + LOGGER.info("Onboarding scan for tenant id " + tenantId + " returned " + + scan.items().size() + " results"); } } catch (DynamoDbException e) { LOGGER.error("OnboardingServiceDAL::getOnboardingByTenantId " + Utils.getFullStackTrace(e)); @@ -132,7 +130,7 @@ public Onboarding updateOnboarding(Onboarding onboarding) { // object was persisted onboarding.setModified(LocalDateTime.now()); Map item = toAttributeValueMap(onboarding); - ddb.putItem(request -> request.tableName(ONBOARDING_TABLE).item(item)); + ddb.putItem(request -> request.tableName(onboardingTable).item(item)); } catch (DynamoDbException e) { LOGGER.error("OnboardingServiceDAL::updateOnboarding " + Utils.getFullStackTrace(e)); throw e; @@ -158,7 +156,7 @@ public Onboarding updateStatus(UUID onboardingId, OnboardingStatus status) { Map key = new HashMap<>(); key.put("id", AttributeValue.builder().s(onboardingId.toString()).build()); UpdateItemResponse response = ddb.updateItem(request -> request - .tableName(ONBOARDING_TABLE) + .tableName(onboardingTable) .key(key) .updateExpression("SET #status = :status, modified = :modified") .expressionAttributeNames(Map.of("#status", "status")) @@ -199,7 +197,7 @@ public Onboarding insertOnboarding(Onboarding onboarding) { onboarding.setModified(now); Map item = toAttributeValueMap(onboarding); try { - ddb.putItem(request -> request.tableName(ONBOARDING_TABLE).item(item)); + ddb.putItem(request -> request.tableName(onboardingTable).item(item)); long putItemTimeMillis = System.currentTimeMillis() - startTimeMillis; LOGGER.info("OnboardingServiceDAL::insertOnboarding PutItem exec " + putItemTimeMillis); } catch (DynamoDbException e) { @@ -211,117 +209,36 @@ public Onboarding insertOnboarding(Onboarding onboarding) { return onboarding; } - public String getCidrBlock(UUID tenantId) { - return getCidrBlock(tenantId.toString()); - } - - public String getCidrBlock(String tenantId) { - if (Utils.isBlank(CIDR_BLOCK_TABLE)) { - throw new IllegalStateException("Missing required environment variable CIDR_BLOCK_TABLE"); - } - String cidrBlock = null; - try { - ScanResponse scan = ddb.scan(r -> r.tableName(CIDR_BLOCK_TABLE)); - if (!scan.items().isEmpty()) { - for (Map item : scan.items()) { - if (item.containsKey("tenant_id") && item.get("tenant_id").s().equals(tenantId)) { - cidrBlock = item.get("cidr_block").s(); - } - } - } - } catch (DynamoDbException ddbError) { - LOGGER.error("dynamodb:Scan error", ddbError); - LOGGER.error(Utils.getFullStackTrace(ddbError)); - throw ddbError; - } catch (Exception e) { - LOGGER.error("Unexpected error", e); - LOGGER.error(Utils.getFullStackTrace(e)); - throw new RuntimeException(e); - } - return cidrBlock; - } - - public boolean availableCidrBlock() { - if (Utils.isBlank(CIDR_BLOCK_TABLE)) { - throw new IllegalStateException("Missing required environment variable CIDR_BLOCK_TABLE"); - } - boolean available; - try { - ScanResponse scan = ddb.scan(r -> r - .tableName(CIDR_BLOCK_TABLE) - .filterExpression("attribute_not_exists(tenant_id)") - ); - available = scan.hasItems() && !scan.items().isEmpty(); - } catch (DynamoDbException ddbError) { - LOGGER.error("dynamodb:Scan error", ddbError); - LOGGER.error(Utils.getFullStackTrace(ddbError)); - throw ddbError; - } - return available; - } + public Onboarding deleteOnboarding(Onboarding onboarding) { + final long startTimeMillis = System.currentTimeMillis(); + LOGGER.info("OnboardingServiceDAL::deleteOnboarding"); - public String assignCidrBlock(String tenantId) { - if (Utils.isBlank(CIDR_BLOCK_TABLE)) { - throw new IllegalStateException("Missing required environment variable CIDR_BLOCK_TABLE"); - } - String cidrBlock; try { - long scanStartTimeMillis = System.currentTimeMillis(); - List availableCidrBlocks = new ArrayList<>(); - ScanResponse fullScan = ddb.scan(r -> r.tableName(CIDR_BLOCK_TABLE)); - if (!fullScan.items().isEmpty()) { - for (Map item : fullScan.items()) { - // Make sure we're not trying to assign a CIDR block to a tenant that already has one - if (item.containsKey("tenant_id") && tenantId.equals(item.get("tenant_id").s())) { - throw new RuntimeException("CIDR block already assigned for tenant " + tenantId); - } - if (!item.containsKey("tenant_id")) { - availableCidrBlocks.add(item.get("cidr_block").s()); - } - } - // Make sure we have an open CIDR block left to assign - if (availableCidrBlocks.isEmpty()) { - throw new RuntimeException("No remaining CIDR blocks"); - } - } - long scanTotalTimeMillis = System.currentTimeMillis() - scanStartTimeMillis; - LOGGER.info("OnboardingServiceDAL::assignCidrBlock scan " + scanTotalTimeMillis); - - long updateStartTimeMillis = System.currentTimeMillis(); - String cidr = availableCidrBlocks.get((int) (Math.random() * availableCidrBlocks.size())); - // Claim this one for this tenant - Map key = new HashMap<>(); - key.put("cidr_block", AttributeValue.builder().s(cidr).build()); - UpdateItemResponse update = ddb.updateItem(r -> r - .tableName(CIDR_BLOCK_TABLE) - .key(key) - .updateExpression("SET tenant_id = :tenantId") - .expressionAttributeValues( - Collections.singletonMap(":tenantId", AttributeValue.builder().s(tenantId).build()) - ) - .conditionExpression("attribute_not_exists(tenant_id)") - .returnValues(ReturnValue.ALL_NEW) + ddb.deleteItem(request -> request + .tableName(onboardingTable) + .key(Map.of("id", AttributeValue.builder().s(onboarding.getId().toString()).build())) ); - Map updated = update.attributes(); - cidrBlock = updated.get("cidr_block").s(); - long updateTotalTimeMillis = System.currentTimeMillis() - updateStartTimeMillis; - LOGGER.info("OnboardingServiceDAL::assignCidrBlock update " + updateTotalTimeMillis); + long deleteItemTimeMillis = System.currentTimeMillis() - startTimeMillis; + LOGGER.info("OnboardingServiceDAL::deleteOnboarding DeleteItem exec " + deleteItemTimeMillis); } catch (DynamoDbException e) { - LOGGER.error("OnboardingServiceDAL::assignCidrBlock " + Utils.getFullStackTrace(e)); + LOGGER.error("OnboardingServiceDAL::deleteOnboarding " + Utils.getFullStackTrace(e)); throw e; } - - return cidrBlock; + long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; + LOGGER.info("OnboardingServiceDAL::deleteOnboarding exec " + totalTimeMillis); + return onboarding; } public static Map toAttributeValueMap(Onboarding onboarding) { Map item = new HashMap<>(); item.put("id", AttributeValue.builder().s(onboarding.getId().toString()).build()); if (onboarding.getCreated() != null) { - item.put("created", AttributeValue.builder().s(onboarding.getCreated().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); + item.put("created", AttributeValue.builder().s( + onboarding.getCreated().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); } if (onboarding.getModified() != null) { - item.put("modified", AttributeValue.builder().s(onboarding.getModified().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); + item.put("modified", AttributeValue.builder().s( + onboarding.getModified().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); } if (onboarding.getStatus() != null) { item.put("status", AttributeValue.builder().s(onboarding.getStatus().toString()).build()); @@ -344,9 +261,6 @@ public static Map toAttributeValueMap(Onboarding onboard if (Utils.isNotBlank(request.getSubdomain())) { requestMap.put("subdomain", AttributeValue.builder().s(request.getSubdomain()).build()); } - if (Utils.isNotBlank(request.getBillingPlan())) { - requestMap.put("billing_plan", AttributeValue.builder().s(request.getBillingPlan()).build()); - } if (request.getAttributes() != null && !request.getAttributes().isEmpty()) { requestMap.put("attributes", AttributeValue.builder().m(request.getAttributes().entrySet() .stream() @@ -356,39 +270,24 @@ public static Map toAttributeValueMap(Onboarding onboard ) ).build()); } + if (request.getAdminUsers() != null && !request.getAdminUsers().isEmpty()) { + List userMaps = new ArrayList<>(); + request.getAdminUsers() + .stream() + .map(userMap -> AttributeValue.builder().m( + userMap.entrySet() + .stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> AttributeValue.builder().s( + String.valueOf(entry.getValue())).build() + )) + ).build()) + .forEach(userMaps::add); + requestMap.put("admin_users", AttributeValue.builder().l(userMaps).build()); + } item.put("request", AttributeValue.builder().m(requestMap).build()); } - if (!onboarding.getStacks().isEmpty()) { - item.put("stacks", AttributeValue.builder().l(onboarding.getStacks() - .stream() - .map(stack -> { - Map stackItem = new HashMap<>(); - if (stack.getService() != null) { - stackItem.put("service", AttributeValue.builder().s(stack.getService()).build()); - } - if (stack.getName() != null) { - stackItem.put("name", AttributeValue.builder().s(stack.getName()).build()); - } - if (stack.getArn() != null) { - stackItem.put("arn", AttributeValue.builder().s(stack.getArn()).build()); - } - stackItem.put("baseStack", AttributeValue.builder().bool(stack.isBaseStack()).build()); - if (stack.getStatus() != null) { - stackItem.put("status", AttributeValue.builder().s(stack.getStatus()).build()); - } - if (stack.getPipeline() != null) { - stackItem.put("pipeline", AttributeValue.builder().s(stack.getPipeline()).build()); - } - if (stack.getPipelineStatus() != null) { - stackItem.put("pipelineStatus", AttributeValue.builder().s(stack.getPipelineStatus()).build()); - } - return AttributeValue.builder().m(stackItem).build(); - }) - .collect(Collectors.toList()) - ).build() - ); - } - item.put("ecs_cluster_locked", AttributeValue.builder().bool(onboarding.isEcsClusterLocked()).build()); return item; } @@ -414,7 +313,8 @@ public static Onboarding fromAttributeValueMap(Map item) } if (item.containsKey("created")) { try { - LocalDateTime created = LocalDateTime.parse(item.get("created").s(), DateTimeFormatter.ISO_DATE_TIME); + LocalDateTime created = LocalDateTime.parse(item.get("created").s(), + DateTimeFormatter.ISO_DATE_TIME); onboarding.setCreated(created); } catch (DateTimeParseException e) { LOGGER.error("Failed to parse created date from database: " + item.get("created").s()); @@ -423,7 +323,8 @@ public static Onboarding fromAttributeValueMap(Map item) } if (item.containsKey("modified")) { try { - LocalDateTime created = LocalDateTime.parse(item.get("modified").s(), DateTimeFormatter.ISO_DATE_TIME); + LocalDateTime created = LocalDateTime.parse(item.get("modified").s(), + DateTimeFormatter.ISO_DATE_TIME); onboarding.setModified(created); } catch (DateTimeParseException e) { LOGGER.error("Failed to parse created date from database: " + item.get("modified").s()); @@ -443,49 +344,52 @@ public static Onboarding fromAttributeValueMap(Map item) } if (item.containsKey("request")) { Map requestMap = item.get("request").m(); - OnboardingRequest request = new OnboardingRequest(requestMap.get("name").s()); + String name = null; + if (requestMap.containsKey("name")) { + name = requestMap.get("name").s(); + } + String tier = null; if (requestMap.containsKey("tier")) { - request.setTier(requestMap.get("tier").s()); + tier = requestMap.get("tier").s(); } + String subdomain = null; if (requestMap.containsKey("subdomain")) { - request.setSubdomain(requestMap.get("subdomain").s()); - } - if (requestMap.containsKey("billing_plan")) { - request.setBillingPlan(requestMap.get("billing_plan").s()); + subdomain = requestMap.get("subdomain").s(); } + Map attributes = null; if (requestMap.containsKey("attributes")) { - request.setAttributes(requestMap.get("attributes").m().entrySet().stream() + attributes = requestMap.get("attributes").m().entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, entry -> entry.getValue().s(), (valForKey, valForDupKey) -> valForKey, LinkedHashMap::new - )) - ); + )); } + Set> adminUsers = new LinkedHashSet<>(); + if (requestMap.containsKey("admin_users")) { + List adminUsersList = requestMap.get("admin_users").l(); + for (AttributeValue adminUserMap : adminUsersList) { + Map adminUser = adminUserMap.m().entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().s(), + (valForKey, valForDupKey) -> valForKey, + LinkedHashMap::new + )); + adminUsers.add(adminUser); + } + } + OnboardingRequest request = OnboardingRequest.builder() + .name(name) + .tier(tier) + .subdomain(subdomain) + .attributes(attributes) + .adminUsers(adminUsers) + .build(); onboarding.setRequest(request); } - if (item.containsKey("stacks")) { - onboarding.setStacks(item.get("stacks").l() - .stream() - .map(stackItem -> { - Map stack = stackItem.m(); - return OnboardingStack.builder() - .service(stack.containsKey("service") ? stack.get("service").s() : null) - .name(stack.containsKey("name") ? stack.get("name").s() : null) - .arn(stack.containsKey("arn") ? stack.get("arn").s() : null) - .baseStack(stack.containsKey("baseStack") ? stack.get("baseStack").bool() : false) - .status(stack.containsKey("status") ? stack.get("status").s() : null) - .pipeline(stack.containsKey("pipeline") ? stack.get("pipeline").s() : null) - .pipelineStatus(stack.containsKey("pipelineStatus") ? stack.get("pipelineStatus").s() : null) - .build(); - }) - .collect(Collectors.toList()) - ); - } - if (item.containsKey("ecs_cluster_locked")) { - onboarding.setEcsClusterLocked(item.get("ecs_cluster_locked").bool()); - } + } return onboarding; } diff --git a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingEvent.java b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingEvent.java index 304d5c32..f4313419 100644 --- a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingEvent.java +++ b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingEvent.java @@ -16,25 +16,27 @@ package com.amazon.aws.partners.saasfactory.saasboost; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.Map; import java.util.UUID; // TODO Make a marker interface of SaaSBoostEvent? public enum OnboardingEvent { - ONBOARDING_INITIATED("Onboarding Initiated"), - ONBOARDING_VALID("Onboarding Validated"), - ONBOARDING_TENANT_ASSIGNED("Onboarding Tenant Assigned"), - ONBOARDING_STACK_STATUS_CHANGED("Onboarding Stack Status Changed"), - ONBOARDING_BASE_PROVISIONED("Onboarding Base Provisioned"), - ONBOARDING_BASE_UPDATED("Onboarding Base Updated"), - ONBOARDING_PROVISIONED("Onboarding Provisioned"), - ONBOARDING_DEPLOYMENT_PIPELINE_CREATED("Onboarding Deployment Pipeline Created"), - ONBOARDING_DEPLOYMENT_PIPELINE_CHANGED("Onboarding Deployment Pipeline Change"), - ONBOARDING_DEPLOYED("Onboarding Deployed"), - ONBOARDING_COMPLETED("Onboarding Completed"), - ONBOARDING_FAILED("Onboarding Failed") + ONBOARDING_INITIATED("Onboarding Initiated"), // Produce + ONBOARDING_VALIDATED("Onboarding Validated"), // Consume + ONBOARDING_TENANT_ASSIGNED("Onboarding Tenant Assigned"), // Produce + ONBOARDING_PROVISIONING("Onboarding Provisioning"), // Consume (optional) + ONBOARDING_PROVISIONED("Onboarding Provisioned"), // Consume (optional) + ONBOARDING_DEPLOYING("Onboarding Deploying"), // Consume (optional) + ONBOARDING_DEPLOYED("Onboarding Deployed"), // Consume + ONBOARDING_APPLICATION_READY("Onboarding Application Ready"), // Consume + ONBOARDING_COMPLETED("Onboarding Completed"), // Produce + ONBOARDING_FAILED("Onboarding Failed") // Produce/Consume ; + private static final Logger LOGGER = LoggerFactory.getLogger(OnboardingEvent.class); private final String detailType; OnboardingEvent(String detailType) { @@ -61,27 +63,38 @@ public static boolean validate(Map event) { } public static boolean validate(Map event, String... requiredKeys) { - if (event == null || !event.containsKey("detail")) { + if (event == null || !event.containsKey("detail") || !event.containsKey("source")) { + LOGGER.error("Event is null or is missing 'detail' or 'source' attributes"); + return false; + } + if (!"saas-boost".equals(event.get("source"))) { + LOGGER.error("Event 'source' != saas-boost"); return false; } try { Map detail = (Map) event.get("detail"); - if (detail == null || !detail.containsKey("onboardingId")) { + if (detail == null) { + LOGGER.error("Event is missing 'detail'"); return false; } - try { - UUID.fromString(String.valueOf(detail.get("onboardingId"))); - } catch (IllegalArgumentException iae) { - return false; + if (detail.containsKey("onboardingId")) { + try { + UUID.fromString(String.valueOf(detail.get("onboardingId"))); + } catch (IllegalArgumentException iae) { + LOGGER.error("Event onboardingId can't be parsed to UUID"); + return false; + } } if (requiredKeys != null) { for (String requiredKey : requiredKeys) { if (!detail.containsKey(requiredKey)) { + LOGGER.error("Event 'detail' is missing required key '" + requiredKey + "'"); return false; } } } } catch (ClassCastException cce) { + LOGGER.error("Event detail is not a Map " + cce.getMessage()); return false; } return true; diff --git a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingRequest.java b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingRequest.java index 7db58737..e10c7f7c 100644 --- a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingRequest.java +++ b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingRequest.java @@ -16,78 +16,94 @@ package com.amazon.aws.partners.saasfactory.saasboost; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.*; +@JsonDeserialize(builder = OnboardingRequest.Builder.class) public class OnboardingRequest { - private String name; - private String tier; - private String subdomain; - private String billingPlan; - private Map attributes = new LinkedHashMap<>(); - - public OnboardingRequest(String name) { - this(name, "default"); - } - - public OnboardingRequest(String name, String tier) { - this(name, tier, null, null); - } - - @JsonCreator - public OnboardingRequest(@JsonProperty("name") String name, @JsonProperty("tier") String tier, - @JsonProperty("subdomain") String subdomain, - @JsonProperty("billingPlan") String billingPlan) { - if (name == null) { - throw new IllegalArgumentException("name is required"); - } - this.name = name; - this.tier = tier != null ? tier : "default"; - this.subdomain = subdomain; - this.billingPlan = Utils.isBlank(billingPlan) ? null : billingPlan; + private final String name; + private final String tier; + private final String subdomain; + private final Map attributes; + private final Set> adminUsers; + + private OnboardingRequest(Builder builder) { + this.name = builder.name; + this.tier = builder.tier; + this.subdomain = builder.subdomain; + this.attributes = builder.attributes; + this.adminUsers = builder.adminUsers; } public String getName() { return name; } - public void setName(String name) { - this.name = name; - } - public String getTier() { return tier; } - public void setTier(String tier) { - this.tier = tier; - } - public String getSubdomain() { return subdomain; } - public void setSubdomain(String subdomain) { - this.subdomain = subdomain; + public Map getAttributes() { + return Map.copyOf(attributes); } - public String getBillingPlan() { - return billingPlan; + public Set> getAdminUsers() { + return Set.copyOf(adminUsers); } - public void setBillingPlan(String billingPlan) { - this.billingPlan = billingPlan; + public static Builder builder() { + return new Builder(); } - public Map getAttributes() { - return attributes; - } + @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property] + public static final class Builder { + private String name; + private String tier; + private String subdomain; + private Map attributes = new LinkedHashMap<>(); + private Set> adminUsers = new LinkedHashSet<>(); - public void setAttributes(Map attributes) { - this.attributes = attributes != null ? attributes : new LinkedHashMap<>(); + private Builder() { + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder tier(String tier) { + this.tier = tier; + return this; + } + + public Builder subdomain(String subdomain) { + this.subdomain = subdomain; + return this; + } + + public Builder attributes(Map attributes) { + if (attributes != null) { + this.attributes.putAll(attributes); + } + return this; + } + + public Builder adminUsers(Collection> adminUsers) { + if (adminUsers != null) { + this.adminUsers.addAll(adminUsers); + } + return this; + } + + public OnboardingRequest build() { + return new OnboardingRequest(this); + } } } diff --git a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingService.java b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingService.java index ac91bb4c..105688fd 100644 --- a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingService.java +++ b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingService.java @@ -17,31 +17,16 @@ package com.amazon.aws.partners.saasfactory.saasboost; import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; import com.amazonaws.services.lambda.runtime.events.SQSEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; -import software.amazon.awssdk.core.exception.SdkServiceException; -import software.amazon.awssdk.core.sync.RequestBody; -import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.cloudformation.CloudFormationClient; -import software.amazon.awssdk.services.cloudformation.model.*; -import software.amazon.awssdk.services.codepipeline.CodePipelineClient; -import software.amazon.awssdk.services.codepipeline.model.ListTagsForResourceResponse; -import software.amazon.awssdk.services.codepipeline.model.Tag; -import software.amazon.awssdk.services.directory.DirectoryClient; -import software.amazon.awssdk.services.directory.model.DescribeDirectoriesResponse; -import software.amazon.awssdk.services.directory.model.DirectoryDescription; -import software.amazon.awssdk.services.ecr.EcrClient; -import software.amazon.awssdk.services.ecr.model.EcrException; -import software.amazon.awssdk.services.ecr.model.ImageIdentifier; -import software.amazon.awssdk.services.ecr.model.ListImagesResponse; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.eventbridge.EventBridgeClient; -import software.amazon.awssdk.services.route53.Route53Client; -import software.amazon.awssdk.services.route53.model.*; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.*; import software.amazon.awssdk.services.s3.presigner.S3Presigner; @@ -50,10 +35,9 @@ import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequestEntry; import software.amazon.awssdk.services.sqs.model.SendMessageBatchResponse; -import java.io.*; +import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.*; import java.util.stream.Collectors; @@ -61,62 +45,40 @@ public class OnboardingService { private static final Logger LOGGER = LoggerFactory.getLogger(OnboardingService.class); - private static final Map CORS = Map.of("Access-Control-Allow-Origin", "*"); private static final String AWS_REGION = System.getenv("AWS_REGION"); - private static final String EVENT_SOURCE = "saas-boost"; - private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); private static final String SAAS_BOOST_EVENT_BUS = System.getenv("SAAS_BOOST_EVENT_BUS"); - private static final String SAAS_BOOST_METRICS_STREAM = System.getenv("SAAS_BOOST_METRICS_STREAM"); - private static final String API_GATEWAY_HOST = System.getenv("API_GATEWAY_HOST"); - private static final String API_GATEWAY_STAGE = System.getenv("API_GATEWAY_STAGE"); - private static final String API_TRUST_ROLE = System.getenv("API_TRUST_ROLE"); - private static final String SAAS_BOOST_BUCKET = System.getenv("SAAS_BOOST_BUCKET"); + private static final String API_APP_CLIENT = System.getenv("API_APP_CLIENT"); private static final String ONBOARDING_TABLE = System.getenv("ONBOARDING_TABLE"); - private static final String ONBOARDING_STACK_SNS = System.getenv("ONBOARDING_STACK_SNS"); - private static final String ONBOARDING_APP_STACK_SNS = System.getenv("ONBOARDING_APP_STACK_SNS"); - private static final String ONBOARDING_VALIDATION_QUEUE = System.getenv("ONBOARDING_VALIDATION_QUEUE"); - private static final String ONBOARDING_VALIDATION_DLQ = System.getenv("ONBOARDING_VALIDATION_DLQ"); private static final String RESOURCES_BUCKET = System.getenv("RESOURCES_BUCKET"); private static final String TENANT_CONFIG_DLQ = System.getenv("TENANT_CONFIG_DLQ"); + private static final Map CORS = Map.of("Access-Control-Allow-Origin", "*"); + private static final String EVENT_SOURCE = "saas-boost"; private static final String RESOURCES_BUCKET_TEMP_FOLDER = "00temp/"; - private final OnboardingServiceDAL dal; - private final CloudFormationClient cfn; + private static final String TENANT_ONBOARDING_STATUS_CHANGED = "Tenant Onboarding Status Changed"; + private final OnboardingDataAccessLayer dal; private final EventBridgeClient eventBridge; - private final EcrClient ecr; private final S3Client s3; private final S3Presigner presigner; - private final Route53Client route53; private final SqsClient sqs; - private final CodePipelineClient codePipeline; - private final DirectoryClient ds; public OnboardingService() { + this(new DefaultDependencyFactory()); + } + + // Facilitates testing by being able to mock out AWS SDK dependencies + public OnboardingService(OnboardingServiceDependencyFactory init) { if (Utils.isBlank(AWS_REGION)) { throw new IllegalStateException("Missing environment variable AWS_REGION"); } - LOGGER.info("Version Info: {}", Utils.version(this.getClass())); - this.dal = new OnboardingServiceDAL(); - this.cfn = Utils.sdkClient(CloudFormationClient.builder(), CloudFormationClient.SERVICE_NAME); - this.eventBridge = Utils.sdkClient(EventBridgeClient.builder(), EventBridgeClient.SERVICE_NAME); - this.ecr = Utils.sdkClient(EcrClient.builder(), EcrClient.SERVICE_NAME); - this.s3 = Utils.sdkClient(S3Client.builder(), S3Client.SERVICE_NAME); - try { - String presignerEndpoint = "https://" + s3.serviceName() + "." - + Region.of(AWS_REGION) - + "." - + Utils.endpointSuffix(AWS_REGION); - this.presigner = S3Presigner.builder() - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .region(Region.of(AWS_REGION)) - .endpointOverride(new URI(presignerEndpoint)) - .build(); - } catch (URISyntaxException e) { - throw new RuntimeException(e); + if (Utils.isBlank(ONBOARDING_TABLE)) { + throw new IllegalStateException("Missing environment variable ONBOARDING_TABLE"); } - this.route53 = Utils.sdkClient(Route53Client.builder(), Route53Client.SERVICE_NAME); - this.sqs = Utils.sdkClient(SqsClient.builder(), SqsClient.SERVICE_NAME); - this.codePipeline = Utils.sdkClient(CodePipelineClient.builder(), CodePipelineClient.SERVICE_NAME); - this.ds = Utils.sdkClient(DirectoryClient.builder(), DirectoryClient.SERVICE_NAME); + LOGGER.info("Version Info: {}", Utils.version(this.getClass())); + this.s3 = init.s3(); + this.eventBridge = init.eventBridge(); + this.sqs = init.sqs(); + this.presigner = init.s3Presigner(); + this.dal = init.dal(); } /** @@ -125,24 +87,26 @@ public OnboardingService() { * @param context * @return Onboarding object for id or HTTP 404 if not found */ - public APIGatewayProxyResponseEvent getOnboarding(Map event, Context context) { + public APIGatewayProxyResponseEvent getOnboarding(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } //Utils.logRequestEvent(event); APIGatewayProxyResponseEvent response; - Map params = (Map) event.get("pathParameters"); + Map params = event.getPathParameters(); String onboardingId = params.get("id"); Onboarding onboarding = dal.getOnboarding(onboardingId); if (onboarding != null) { response = new APIGatewayProxyResponseEvent() .withHeaders(CORS) - .withStatusCode(200) + .withStatusCode(HttpURLConnection.HTTP_OK) .withBody(Utils.toJson(onboarding)); } else { - response = new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(404); + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_NOT_FOUND); } return response; @@ -154,25 +118,25 @@ public APIGatewayProxyResponseEvent getOnboarding(Map event, Con * @param context * @return List of onboarding objects */ - public APIGatewayProxyResponseEvent getOnboardings(Map event, Context context) { + public APIGatewayProxyResponseEvent getOnboardings(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } //Utils.logRequestEvent(event); APIGatewayProxyResponseEvent response; List onboardings; - Map queryParams = (Map) event.get("queryStringParameters"); + Map queryParams = event.getQueryStringParameters(); if (queryParams != null && queryParams.containsKey("tenantId") && Utils.isNotBlank(queryParams.get("tenantId"))) { - onboardings = Collections.singletonList(dal.getOnboardingByTenantId(queryParams.get("tenantId"))); + onboardings = List.of(dal.getOnboardingByTenantId(queryParams.get("tenantId"))); } else { onboardings = dal.getOnboardings(); } response = new APIGatewayProxyResponseEvent() .withHeaders(CORS) - .withStatusCode(200) + .withStatusCode(HttpURLConnection.HTTP_OK) .withBody(Utils.toJson(onboardings)); return response; @@ -184,32 +148,33 @@ public APIGatewayProxyResponseEvent getOnboardings(Map event, Co * @param context * @return HTTP 200 if updated, HTTP 400 on failure */ - public APIGatewayProxyResponseEvent updateOnboarding(Map event, Context context) { + public APIGatewayProxyResponseEvent updateOnboarding(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } + //Utils.logRequestEvent(event); APIGatewayProxyResponseEvent response; - Map params = (Map) event.get("pathParameters"); + Map params = event.getPathParameters(); String onboardingId = params.get("id"); - Onboarding onboarding = Utils.fromJson((String) event.get("body"), Onboarding.class); + Onboarding onboarding = Utils.fromJson(event.getBody(), Onboarding.class); if (onboarding == null) { response = new APIGatewayProxyResponseEvent() - .withStatusCode(400) + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) .withHeaders(CORS) .withBody(Utils.toJson(Map.of("message", "Invalid request body"))); } else { if (onboarding.getId() == null || !onboarding.getId().toString().equals(onboardingId)) { LOGGER.error("Can't update onboarding {} at resource {}", onboarding.getId(), onboardingId); response = new APIGatewayProxyResponseEvent() - .withStatusCode(400) + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) .withHeaders(CORS) .withBody(Utils.toJson(Map.of("message", "Request body must include id"))); } else { onboarding = dal.updateOnboarding(onboarding); response = new APIGatewayProxyResponseEvent() - .withStatusCode(200) + .withStatusCode(HttpURLConnection.HTTP_OK) .withHeaders(CORS) .withBody(Utils.toJson(onboarding)); } @@ -224,26 +189,35 @@ public APIGatewayProxyResponseEvent updateOnboarding(Map event, * @param context * @return HTTP 204 if deleted, HTTP 400 on failure */ - public APIGatewayProxyResponseEvent deleteOnboarding(Map event, Context context) { + public APIGatewayProxyResponseEvent deleteOnboarding(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } + //Utils.logRequestEvent(event); APIGatewayProxyResponseEvent response; - Map params = (Map) event.get("pathParameters"); + Map params = event.getPathParameters(); String onboardingId = params.get("id"); - try { - //dal.deleteOnboarding(onboardingId); - response = new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(204); // No content - } catch (Exception e) { + Onboarding onboarding = dal.getOnboarding(onboardingId); + if (onboarding == null) { response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) .withHeaders(CORS) - .withStatusCode(400) - .withBody(Utils.toJson(Map.of("message", "Failed to delete onboarding record " - + onboardingId))); + .withBody(Utils.toJson(Map.of("message", "Invalid onboarding id"))); + } else { + try { + dal.deleteOnboarding(onboarding); + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_NO_CONTENT); // No content + } catch (Exception e) { + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_NOT_FOUND) + .withBody(Utils.toJson(Map.of("message", "Failed to delete onboarding record " + + onboardingId))); + } } return response; } @@ -255,47 +229,41 @@ public APIGatewayProxyResponseEvent deleteOnboarding(Map event, * @param context * @return Onboarding object in a created state or HTTP 400 if the request does not contain a name */ - public APIGatewayProxyResponseEvent insertOnboarding(Map event, Context context) { + public APIGatewayProxyResponseEvent insertOnboarding(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); - } - if (Utils.isBlank(SAAS_BOOST_BUCKET)) { - throw new IllegalStateException("Missing required environment variable SAAS_BOOST_BUCKET"); + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } + if (Utils.isBlank(SAAS_BOOST_EVENT_BUS)) { throw new IllegalArgumentException("Missing required environment variable SAAS_BOOST_EVENT_BUS"); } - final long startTimeMillis = System.currentTimeMillis(); - LOGGER.info("OnboardingService::startOnboarding"); - Utils.logRequestEvent(event); // Parse the onboarding request - OnboardingRequest onboardingRequest = Utils.fromJson((String) event.get("body"), OnboardingRequest.class); + OnboardingRequest onboardingRequest = Utils.fromJson(event.getBody(), OnboardingRequest.class); if (null == onboardingRequest) { LOGGER.error("Onboarding request is invalid"); return new APIGatewayProxyResponseEvent() - .withStatusCode(400) + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) .withHeaders(CORS) .withBody("{\"message\": \"Invalid onboarding request.\"}"); } if (Utils.isBlank(onboardingRequest.getName())) { LOGGER.error("Onboarding request is missing tenant name"); return new APIGatewayProxyResponseEvent() - .withStatusCode(400) + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) .withHeaders(CORS) .withBody("{\"message\": \"Tenant name is required.\"}"); } if (Utils.isBlank(onboardingRequest.getTier())) { LOGGER.error("Onboarding request is missing tier"); return new APIGatewayProxyResponseEvent() - .withStatusCode(400) + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) .withHeaders(CORS) .withBody("{\"message\": \"Tier is required.\"}"); } - // TODO check for duplicate tenant name? Do it by just looking at the local onboarding requests, or make - // a call out to the tenant service? // Create a new onboarding request record for a tenant Onboarding onboarding = new Onboarding(); @@ -306,150 +274,92 @@ public APIGatewayProxyResponseEvent insertOnboarding(Map event, onboarding = dal.insertOnboarding(onboarding); // Generate the presigned URL for this tenant's ZIP archive - final String key = RESOURCES_BUCKET_TEMP_FOLDER + onboarding.getId().toString() + ".zip"; - final Duration expires = Duration.ofMinutes(15); // UI times out in 10 min - PresignedPutObjectRequest presignedObject = presigner.presignPutObject(request -> request - .signatureDuration(expires) - .putObjectRequest(PutObjectRequest.builder() - .bucket(RESOURCES_BUCKET) - .key(key) - .build() - ) - .build() - ); - onboarding.setZipFile(presignedObject.url().toString()); - // Don't save the temporary presigned URL to the database. If the user actually uploads - // a tenant config file, we'll persist the information then. + if (Utils.isNotEmpty(RESOURCES_BUCKET)) { + final String key = RESOURCES_BUCKET_TEMP_FOLDER + onboarding.getId().toString() + ".zip"; + final Duration expires = Duration.ofMinutes(15); // UI times out in 10 min + PresignedPutObjectRequest presignedObject = presigner.presignPutObject(request -> request + .signatureDuration(expires) + .putObjectRequest(PutObjectRequest.builder() + .bucket(RESOURCES_BUCKET) + .key(key) + .build() + ) + .build() + ); + onboarding.setZipFile(presignedObject.url().toString()); + // Don't save the temporary presigned URL to the database. If the user actually uploads + // a tenant config file, we'll persist the information then. + } // Let everyone know we've created an onboarding request so it can be validated - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, "saas-boost", + Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, OnboardingEvent.ONBOARDING_INITIATED.detailType(), Map.of("onboardingId", onboarding.getId()) ); - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("OnboardingService::startOnboarding exec " + totalTimeMillis); - return new APIGatewayProxyResponseEvent() .withHeaders(CORS) - .withStatusCode(200) + .withStatusCode(HttpURLConnection.HTTP_CREATED) .withBody(Utils.toJson(onboarding)); } - public void handleOnboardingEvent(Map event, Context context) { - if ("saas-boost".equals(event.get("source"))) { + /** + * Event listener (EventBridge Rule target) for Onboarding Events. The single public + * entry point both reduces the number of EventBridge rules and simplifies debug logging. + * @param event the EventBridge event + */ + public void handleOnboardingEvent(Map event) { + Utils.logRequestEvent(event); + if (OnboardingEvent.validate(event)) { String detailType = (String) event.get("detail-type"); OnboardingEvent onboardingEvent = OnboardingEvent.fromDetailType(detailType); if (onboardingEvent != null) { switch (onboardingEvent) { - case ONBOARDING_INITIATED: - LOGGER.info("Handling Onboarding Initiated"); - handleOnboardingInitiated(event, context); - break; - case ONBOARDING_VALID: + case ONBOARDING_VALIDATED: LOGGER.info("Handling Onboarding Validated"); - handleOnboardingValidated(event, context); - break; - case ONBOARDING_TENANT_ASSIGNED: - LOGGER.info("Handling Onboarding Tenant Assigned"); - handleOnboardingTenantAssigned(event, context); - break; - case ONBOARDING_STACK_STATUS_CHANGED: - LOGGER.info("Handling Onboarding Stack Status Changed"); - handleOnboardingStackStatusChanged(event, context); - break; - case ONBOARDING_BASE_PROVISIONED: - LOGGER.info("Handling Onboarding Base Provisioned"); - handleOnboardingBaseProvisioned(event, context); + handleOnboardingValidated(event); break; - case ONBOARDING_BASE_UPDATED: - LOGGER.info("Handling Onboarding Base Updated"); - handleOnboardingBaseUpdated(event, context); + case ONBOARDING_PROVISIONING: + LOGGER.info("Handling Onboarding Resources Provisioning"); + handleOnboardingProvisioning(event); break; case ONBOARDING_PROVISIONED: - LOGGER.info("Handling Onboarding Provisioning Complete"); - handleOnboardingProvisioned(event, context); + LOGGER.info("Handling Onboarding Resources Provisioned"); + handleOnboardingProvisioned(event); break; - case ONBOARDING_DEPLOYMENT_PIPELINE_CREATED: - LOGGER.info("Handling Onboarding Deployment Pipeline Created"); - handleOnboardingDeploymentPipelineCreated(event, context); + case ONBOARDING_DEPLOYING: + LOGGER.info("Handling Onboarding Workloads Deploying"); + handleOnboardingDeploying(event); break; case ONBOARDING_DEPLOYED: LOGGER.info("Handling Onboarding Workloads Deployed"); - handleOnboardingDeployed(event, context); + handleOnboardingDeployed(event); + break; + case ONBOARDING_FAILED: + LOGGER.info("Handling Onboarding Failed"); + handleOnboardingFailed(event); break; default: LOGGER.error("Unknown Onboarding Event!"); } - } else if (detailType.startsWith("Application Configuration ")) { - LOGGER.info("Handling App Config Event"); - handleAppConfigEvent(event, context); - } else if (detailType.startsWith("Tenant ")) { - LOGGER.info("Handling Tenant Event"); - handleTenantEvent(event, context); + } else if (detailType.startsWith("Billing ")) { + // Billing events that effect the onboarding status + // Use this entry point for consolidated logging of the onboarding lifecycle + LOGGER.info("Handling Billing Event"); + handleBillingEvent(event); } else { LOGGER.error("Can't find onboarding event for detail-type {}", event.get("detail-type")); // TODO Throw here? Would end up in DLQ. } - } else if ("aws.codepipeline".equals(event.get("source"))) { - LOGGER.info("Handling Onboarding Deployment Pipeline Changed"); - Utils.logRequestEvent(event); - handleOnboardingDeploymentPipelineChanged(event, context); - } else { - LOGGER.error("Unknown event source " + event.get("source")); - // TODO Throw here? Would end up in DLQ. - } - } - - protected void handleOnboardingInitiated(Map event, Context context) { - if (Utils.isBlank(ONBOARDING_VALIDATION_QUEUE)) { - throw new IllegalStateException("Missing required environment variable ONBOARDING_VALIDATION_QUEUE"); - } - if (OnboardingEvent.validate(event)) { - Map detail = (Map) event.get("detail"); - Onboarding onboarding = dal.getOnboarding((String) detail.get("onboardingId")); - if (onboarding != null) { - if (OnboardingStatus.created == onboarding.getStatus()) { - try { - // Queue this newly created onboarding request for validation - LOGGER.info("Publishing message to onboarding validation queue {} {}", onboarding.getId(), - ONBOARDING_VALIDATION_QUEUE); - sqs.sendMessage(request -> request - .queueUrl(ONBOARDING_VALIDATION_QUEUE) - .messageBody(Utils.toJson(Map.of("onboardingId", onboarding.getId()))) - ); - dal.updateStatus(onboarding.getId(), OnboardingStatus.validating); - } catch (SdkServiceException sqsError) { - LOGGER.error("sqs:SendMessage error", sqsError); - LOGGER.error(Utils.getFullStackTrace(sqsError)); - throw sqsError; - } - } else { - // Onboarding is in the wrong state for validation - LOGGER.error("Can not queue onboarding {} for validation with status {}", onboarding.getId(), - onboarding.getStatus()); - // TODO Throw here? Would end up in DLQ. - } - } else { - // Can't find an onboarding record for this id - LOGGER.error("Can't find onboarding record for {}", detail.get("onboardingId")); - // TODO Throw here? Would end up in DLQ. - } } else { - LOGGER.error("Missing onboardingId in event detail {}", Utils.toJson(event.get("detail"))); + LOGGER.error("Invalid SaaS Boost Onboarding Event " + Utils.toJson(event)); // TODO Throw here? Would end up in DLQ. } } - protected void handleOnboardingValidated(Map event, Context context) { - if (Utils.isBlank(API_GATEWAY_HOST)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_HOST"); - } - if (Utils.isBlank(API_GATEWAY_STAGE)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_STAGE"); - } - if (Utils.isBlank(API_TRUST_ROLE)) { - throw new IllegalStateException("Missing required environment variable API_TRUST_ROLE"); + protected void handleOnboardingValidated(Map event) { + if (Utils.isBlank(API_APP_CLIENT)) { + throw new IllegalStateException("Missing required environment variable API_APP_CLIENT"); } if (OnboardingEvent.validate(event)) { Map detail = (Map) event.get("detail"); @@ -467,19 +377,9 @@ protected void handleOnboardingValidated(Map event, Context cont // Call the tenant service synchronously to insert the new tenant record LOGGER.info("Calling tenant service insert tenant API"); LOGGER.info(Utils.toJson(onboarding.getRequest())); - String insertTenantResponseBody = ApiGatewayHelper.signAndExecuteApiRequest( - ApiGatewayHelper.getApiRequest( - API_GATEWAY_HOST, - API_GATEWAY_STAGE, - ApiRequest.builder() - .resource("tenants") - .method("POST") - .body(Utils.toJson(onboarding.getRequest())) - .build() - ), - API_TRUST_ROLE, - (String) event.get("id") - ); + ApiGatewayHelper api = ApiGatewayHelper.clientCredentialsHelper(API_APP_CLIENT); + String insertTenantResponseBody = api + .authorizedRequest("POST", "tenants", Utils.toJson(onboarding.getRequest())); Map insertedTenant = Utils.fromJson(insertTenantResponseBody, LinkedHashMap.class); if (null == insertedTenant) { failOnboarding(onboarding.getId(), "Tenant insert API call failed"); @@ -489,27 +389,18 @@ protected void handleOnboardingValidated(Map event, Context cont String tenantId = (String) insertedTenant.get("id"); onboarding.setTenantId(UUID.fromString(tenantId)); onboarding = dal.updateOnboarding(onboarding); - - // Assign a CIDR block to this tenant to use for its VPC - try { - dal.assignCidrBlock(tenantId); - } catch (Exception e) { - // Unexpected error since we have already validated... but eventual consistency - failOnboarding(onboarding.getId(), "Could not assign CIDR for tenant VPC"); - return; - } // Let the tenant service know the onboarding status Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Tenant Onboarding Status Changed", + TENANT_ONBOARDING_STATUS_CHANGED, Map.of( "tenantId", tenantId, "onboardingStatus", onboarding.getStatus() ) ); - // Ready to provision the base infrastructure for this tenant - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, "saas-boost", + // Ready to provision the infrastructure for this tenant + Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, OnboardingEvent.ONBOARDING_TENANT_ASSIGNED.detailType(), Map.of("onboardingId", onboarding.getId(), "tenant", insertedTenant)); } else { @@ -523,816 +414,112 @@ protected void handleOnboardingValidated(Map event, Context cont } } - protected void handleOnboardingTenantAssigned(Map event, Context context) { - if (Utils.isBlank(SAAS_BOOST_ENV)) { - throw new IllegalStateException("Missing required environment variable SAAS_BOOST_ENV"); - } - if (Utils.isBlank(SAAS_BOOST_BUCKET)) { - throw new IllegalArgumentException("Missing required environment variable SAAS_BOOST_BUCKET"); - } - if (Utils.isBlank(API_GATEWAY_HOST)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_HOST"); - } - if (Utils.isBlank(API_GATEWAY_STAGE)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_STAGE"); - } - if (Utils.isBlank(API_TRUST_ROLE)) { - throw new IllegalStateException("Missing required environment variable API_TRUST_ROLE"); - } - if (Utils.isBlank(ONBOARDING_STACK_SNS)) { - throw new IllegalArgumentException("Missing required environment variable ONBOARDING_STACK_SNS"); - } - if (OnboardingEvent.validate(event, "tenant")) { - Map detail = (Map) event.get("detail"); - Onboarding onboarding = dal.getOnboarding((String) detail.get("onboardingId")); - if (onboarding != null) { - String tenantId = onboarding.getTenantId().toString(); - String cidrBlock = dal.getCidrBlock(onboarding.getTenantId()); - if (Utils.isBlank(cidrBlock)) { - // TODO rethrow to DLQ? - failOnboarding(onboarding.getId(), "Can't find assigned CIDR for tenant " + tenantId); - return; - } - - // Make a synchronous call to the settings service for the app config - Map appConfig = getAppConfig(context); - if (null == appConfig) { - // TODO rethrow to DLQ? - failOnboarding(onboarding.getId(), "Settings getAppConfig API call failed"); - return; - } - - // And parameters specific to this tenant - Map tenant = (Map) detail.get("tenant"); - - OnboardingBaseStackParameters parameters = new OnboardingBaseStackParameters(); - parameters.setProperty("Environment", SAAS_BOOST_ENV); - parameters.setProperty("DomainName", (String) appConfig.get("domainName")); - parameters.setProperty("HostedZoneId", (String) appConfig.get("hostedZone")); - parameters.setProperty("SSLCertificateArn", (String) appConfig.get("sslCertificate")); - parameters.setProperty("TenantId", tenantId); - parameters.setProperty("TenantSubDomain", (String) tenant.get("subdomain")); - parameters.setProperty("CidrPrefix", - cidrBlock.substring(0, cidrBlock.indexOf(".", cidrBlock.indexOf(".") + 1))); - parameters.setProperty("Tier", (String) tenant.get("tier")); - parameters.setProperty("PrivateServices", Boolean.toString(hasPrivateServices(appConfig))); - parameters.setProperty("DeployActiveDirectory", Boolean.toString(hasActiveDirectoryConfigured(appConfig))); - - String tenantShortId = tenantId.substring(0, 8); - String stackName = "sb-" + SAAS_BOOST_ENV + "-tenant-" + tenantShortId; - - // Now run the onboarding stack to provision the infrastructure for this tenant - LOGGER.info("OnboardingService::provisionTenant create stack " + stackName); - String templateUrl = "https://" + SAAS_BOOST_BUCKET + ".s3." + AWS_REGION - + "." + Utils.endpointSuffix(AWS_REGION) + "/tenant-onboarding.yaml"; - String stackId; - try { - CreateStackResponse cfnResponse = cfn.createStack(CreateStackRequest.builder() - .stackName(stackName) - .disableRollback(false) - .capabilitiesWithStrings("CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND") - .notificationARNs(ONBOARDING_STACK_SNS) - .templateURL(templateUrl) - .parameters(parameters.forCreate()) - .build() - ); - stackId = cfnResponse.stackId(); - onboarding.setStatus(OnboardingStatus.provisioning); - onboarding.addStack(OnboardingStack.builder() - .name(stackName) - .arn(stackId) - .baseStack(true) - .status("CREATE_IN_PROGRESS") - .build() - ); - dal.updateOnboarding(onboarding); - LOGGER.info("OnboardingService::provisionTenant stack id " + stackId); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Tenant Onboarding Status Changed", - Map.of( - "tenantId", tenantId, - "onboardingStatus", onboarding.getStatus() - ) - ); - } catch (SdkServiceException cfnError) { - LOGGER.error("cloudformation::createStack failed {}", cfnError.getMessage()); - LOGGER.error(Utils.getFullStackTrace(cfnError)); - failOnboarding(onboarding.getId(), cfnError.getMessage()); - throw cfnError; - } - } else { - // Can't find an onboarding record for this id - LOGGER.error("Can't find onboarding record for {}", detail.get("onboardingId")); - // TODO Throw here? Would end up in DLQ. - } - } else { - LOGGER.error("Missing onboardingId in event detail {}", Utils.toJson(event.get("detail"))); - // TODO Throw here? Would end up in DLQ. - } + protected void handleOnboardingProvisioning(Map event) { + Map detail = (Map) event.get("detail"); + Onboarding onboarding = dal.updateStatus((String) detail.get("onboardingId"), + OnboardingStatus.provisioning.name()); + // Let the tenant service know the onboarding status + Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, + TENANT_ONBOARDING_STATUS_CHANGED, + Map.of( + "tenantId", onboarding.getTenantId(), + "onboardingStatus", onboarding.getStatus() + ) + ); } - protected void handleOnboardingStackStatusChanged(Map event, Context context) { - // TODO stack events don't have the onboardingId, so we can't use OnboardingEvent::validate as written + protected void handleOnboardingProvisioned(Map event) { Map detail = (Map) event.get("detail"); - if (detail != null && detail.containsKey("tenantId") && detail.containsKey("stackId") - && detail.containsKey("stackStatus")) { - String tenantId = (String) detail.get("tenantId"); - String stackId = (String) detail.get("stackId"); - String stackStatus = (String) detail.get("stackStatus"); - OnboardingStatus status = OnboardingStatus.fromStackStatus(stackStatus); - - Onboarding onboarding = dal.getOnboardingByTenantId(tenantId); - if (onboarding != null) { - LOGGER.info("Updating onboarding stack status {} {}", onboarding.getId(), stackId); - for (OnboardingStack stack : onboarding.getStacks()) { - if (stackId.equals(stack.getArn())) { - if (!stackStatus.equals(stack.getStatus())) { - LOGGER.info("Stack status changing from {} to {}", stack.getStatus(), stackStatus); - stack.setStatus(stackStatus); - } - if (status != onboarding.getStatus()) { - if (OnboardingStatus.deleted == status && !stack.isBaseStack()) { - // If we're receiving a DELETE_COMPLETE status for one of the app stacks, - // the onboarding record is still in a deleting state because we have to - // delete the base stack after all the app stacks are complete - LOGGER.info("Skipping onboarding status deleted for app stack {}", stack.getName()); - onboarding.setStatus(OnboardingStatus.deleting); - } else { - onboarding.setStatus(status); - LOGGER.info("Onboarding status changing from {} to {}", onboarding.getStatus(), status); - } - } - dal.updateOnboarding(onboarding); - if (stack.isComplete()) { - if (stack.isBaseStack() && onboarding.baseStacksComplete()) { - if (stack.isCreated()) { - LOGGER.info("Onboarding base stacks provisioned"); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, "saas-boost", - OnboardingEvent.ONBOARDING_BASE_PROVISIONED.detailType(), - Map.of("onboardingId", onboarding.getId()) - ); - } else if (stack.isUpdated()) { - LOGGER.info("Onboarding base stacks updated"); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - OnboardingEvent.ONBOARDING_BASE_UPDATED.detailType(), - Map.of("onboardingId", onboarding.getId()) - ); - } - } else if (!stack.isBaseStack() && onboarding.stacksComplete()) { - LOGGER.info("All onboarding stacks provisioned"); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, "saas-boost", - OnboardingEvent.ONBOARDING_PROVISIONED.detailType(), - Map.of("onboardingId", onboarding.getId()) - ); - } - } else if (!stack.isBaseStack() && stack.isDeleted() && onboarding.appStacksDeleted()) { - LOGGER.info("All app stacks deleted"); - handleBaseProvisioningReadyToDelete(event, context); - } else if (stack.isBaseStack() && stack.isDeleted()) { - onboarding.setStatus(OnboardingStatus.deleted); - dal.updateOnboarding(onboarding); - // Let the tenant service know the onboarding status - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Tenant Onboarding Status Changed", - Map.of( - "tenantId", tenantId, - "onboardingStatus", onboarding.getStatus() - ) - ); - } - break; - } - } - } else { - // Can't find an onboarding record for this id - LOGGER.error("Can't find onboarding record for tenant {}", detail.get("tenantId")); - // TODO Throw here? Would end up in DLQ. - } - } else { - LOGGER.error("Missing tenantId and/or stackId in event detail {}", Utils.toJson(event.get("detail"))); - // TODO Throw here? Would end up in DLQ. - } + Onboarding onboarding = dal.updateStatus((String) detail.get("onboardingId"), + OnboardingStatus.provisioned.name()); + // Let the tenant service know the onboarding status + Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, + TENANT_ONBOARDING_STATUS_CHANGED, + Map.of( + "tenantId", onboarding.getTenantId(), + "onboardingStatus", onboarding.getStatus() + ) + ); } - protected void handleOnboardingBaseProvisioned(Map event, Context context) { - if (Utils.isBlank(AWS_REGION)) { - throw new IllegalStateException("Missing required environment variable AWS_REGION"); - } - if (Utils.isBlank(SAAS_BOOST_ENV)) { - throw new IllegalStateException("Missing required environment variable SAAS_BOOST_ENV"); - } - if (Utils.isBlank(API_GATEWAY_HOST)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_HOST"); - } - if (Utils.isBlank(API_GATEWAY_STAGE)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_STAGE"); - } - if (Utils.isBlank(API_TRUST_ROLE)) { - throw new IllegalStateException("Missing required environment variable API_TRUST_ROLE"); - } - if (Utils.isBlank(ONBOARDING_APP_STACK_SNS)) { - throw new IllegalStateException("Missing required environment variable ONBOARDING_APP_STACK_SNS"); - } - if (Utils.isBlank(RESOURCES_BUCKET)) { - throw new IllegalStateException("Missing required environment variable RESOURCES_BUCKET"); - } - - //Utils.logRequestEvent(event); - if (OnboardingEvent.validate(event)) { - Map detail = (Map) event.get("detail"); - Onboarding onboarding = dal.getOnboarding((String) detail.get("onboardingId")); - if (onboarding != null) { - OnboardingAppStackParameters parameters = new OnboardingAppStackParameters(); - parameters.setProperty("Environment", SAAS_BOOST_ENV); - // CloudFormation can't natively add more than one Capacity Provider to an ECS cluster - // and we need a different CP for each service in the shared cluster. We're using the - // Onboarding Service's database to hold state that the custom resource can read during - // each service provisioning in order to update the cluster's list of capacity providers. - parameters.setProperty("OnboardingDdbTable", ONBOARDING_TABLE); - // Currently using the SaaS Boost event bus for Billing Service metering calls - parameters.setProperty("EventBus", Objects.toString(SAAS_BOOST_EVENT_BUS, "")); - // Currently using a Kinesis Firehose for the Analytics Service metrics calls - parameters.setProperty("MetricsStream", Objects.toString(SAAS_BOOST_METRICS_STREAM, "")); - - // First get the tenant specific parameters created during base provisioning. - // This requires a call to the Tenant Service. - try { - onboardingAppStackTenantParams(onboarding, parameters, context); - } catch (RuntimeException e) { - LOGGER.error(e.getMessage()); - LOGGER.error(Utils.getFullStackTrace(e)); - failOnboarding(onboarding.getId(), e.getMessage()); - return; - } - - // Now, load up the app config from the Settings Service - Map appConfig = getAppConfig(context); - if (appConfig != null) { - // TODO Use the app name for tagging - String applicationName = (String) appConfig.get("name"); - - // Need to use these across all services so we'll pass them in to be mutated in place - // by the onboardingAppStack*Params methods - Map pathPriority = getPathPriority(appConfig); - Properties serviceDiscovery = new Properties(); - - // And for each service in the application, load up all of the CloudFormation template - // parameters and call create stack. - Map services = (Map) appConfig.get("services"); - for (Map.Entry serviceConfig : services.entrySet()) { - createOnboardingAppStack(onboarding, serviceConfig, pathPriority, parameters, - serviceDiscovery, context); - } - - // Write the application-wide environment variables to S3 so each service container can load it up - try { - savePropertiesFileToS3(s3, RESOURCES_BUCKET, "ServiceDiscovery.env", - parameters.getProperty("TenantId"), serviceDiscovery); - } catch (Exception e) { - LOGGER.error(e.getMessage()); - LOGGER.error(Utils.getFullStackTrace(e)); - failOnboarding(onboarding.getId(), e.getMessage()); - } - } - } else { - LOGGER.error("No onboarding record for {}", detail.get("onboardingId")); - } - } else { - LOGGER.error("Missing onboardingId in event detail {}", Utils.toJson(event.get("detail"))); - // TODO Throw here? Would end up in DLQ. - } + protected void handleOnboardingDeploying(Map event) { + Map detail = (Map) event.get("detail"); + Onboarding onboarding = dal.updateStatus((String) detail.get("onboardingId"), + OnboardingStatus.deploying.name()); + // Let the tenant service know the onboarding status + Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, + TENANT_ONBOARDING_STATUS_CHANGED, + Map.of( + "tenantId", onboarding.getTenantId(), + "onboardingStatus", onboarding.getStatus() + ) + ); } - protected void handleOnboardingBaseUpdated(Map event, Context context) { - // The AppConfig changed in some way with provisioned tenants. We've finished updating the base - // infrastructure and now we need to update the app services. - if (OnboardingEvent.validate(event)) { - Map detail = (Map) event.get("detail"); - Onboarding onboarding = dal.getOnboarding((String) detail.get("onboardingId")); - if (onboarding != null) { - // TODO we should probably cache appConfig so we don't have to make this call all the time - // TODO or should we have the event include the appConfig in its detail? - Map appConfig = getAppConfig(context); - Map services = (Map) appConfig.get("services"); - - // We need to recalculate the path priority rules for the public services - Map pathPriority = getPathPriority(appConfig); - - // We may need to update the service discovery environment variables if private services were added - Properties serviceDiscovery = new Properties(); - - List update = new ArrayList<>(); - for (Map.Entry serviceConfig : services.entrySet()) { - int found = 0; - String serviceName = serviceConfig.getKey(); - Map service = (Map) serviceConfig.getValue(); - for (OnboardingStack stack : onboarding.getStacks()) { - // Skip the base stack because we've already updated it - if (stack.isBaseStack()) { - continue; - } - if (serviceName.equals(stack.getService())) { - found++; - if ((Boolean) service.get("public")) { - Integer publicPathRulePriority = pathPriority.get(serviceName); - // TODO this will break if there's an existing ALB listener rule with this priority - LOGGER.info("Calling cloudFormation update-stack --stack-name {}", stack.getName()); - try { - UpdateStackResponse cfnResponse = cfn.updateStack(UpdateStackRequest.builder() - .stackName(stack.getArn()) - .usePreviousTemplate(Boolean.TRUE) - .capabilitiesWithStrings("CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND") - .parameters(new OnboardingAppStackParameters().forUpdate( - Map.of( - "PublicPathRulePriority", publicPathRulePriority.toString() - ) - )) - .build() - ); - String stackId = cfnResponse.stackId(); - if (!stack.getArn().equals(stackId)) { - LOGGER.error("Updating stack id does not equal existing stack arn"); - } - stack.setStatus("UPDATE_IN_PROGRESS"); - update.add(stackId); - onboarding.setStatus(OnboardingStatus.updating); - dal.updateOnboarding(onboarding); - } catch (SdkServiceException cfnError) { - // CloudFormation throws a 400 error if it doesn't detect any resources in a stack - // need to be updated. Swallow this error. - if (cfnError.getMessage().contains("No updates are to be performed")) { - LOGGER.warn("cloudformation::updateStack error {}", cfnError.getMessage()); - } else { - LOGGER.error("cloudformation::updateStack failed {}", cfnError.getMessage()); - LOGGER.error(Utils.getFullStackTrace(cfnError)); - throw cfnError; - } - } - } - break; - } - } - if (update.isEmpty()) { - LOGGER.warn("No publicly addressable services found to update"); - } - - if (found == 0) { - // New service config - LOGGER.info("Adding new service {} for tenant {}", serviceName, onboarding.getTenantId()); - OnboardingAppStackParameters parameters = new OnboardingAppStackParameters(); - parameters.setProperty("Environment", SAAS_BOOST_ENV); - parameters.setProperty("OnboardingDdbTable", ONBOARDING_TABLE); - parameters.setProperty("EventBus", SAAS_BOOST_EVENT_BUS); - parameters.setProperty("MetricsStream", SAAS_BOOST_METRICS_STREAM); - - // First get the tenant specific parameters created during base provisioning. - // This requires a call to the Tenant Service. - try { - onboardingAppStackTenantParams(onboarding, parameters, context); - } catch (RuntimeException e) { - LOGGER.error(e.getMessage()); - LOGGER.error(Utils.getFullStackTrace(e)); - failOnboarding(onboarding.getId(), e.getMessage()); - return; - } - if (!update.isEmpty()) { - // If we had to update existing public services, we really should wait for - // those stacks to be in UPDATE_COMPLETE prior to creating any new service - // stacks to avoid race conditions on the load balancer rule priority values. - //for (String updatingStack : update) { - // cfn.waiter().waitUntilStackUpdateComplete(request -> request - // .stackName(updatingStack)); - //} - // However, due to our current stack status change listener, we'd mark the - // onboarding record provisioned prematurely because it won't have the new - // service stack in it yet (happens has part of createOnboardingAppStack call - // below) but all of the non base stacks will be in a completed state. - // TODO create a more fine grained set of events to track this use case specifically - try { - // For now we'll arbitrarily give the update stack calls a head start - LOGGER.info("Pausing new service stack creation for existing services to update"); - Thread.sleep(10000 * update.size()); - } catch (InterruptedException cantSleep) { - LOGGER.error("Unable to pause thread"); - Thread.currentThread().interrupt(); - } - } - OnboardingStack newServiceStack = createOnboardingAppStack(onboarding, serviceConfig, - pathPriority, parameters, serviceDiscovery, context); - update.add(newServiceStack.getArn()); - } - } - - // Write the application-wide environment variables to S3 so each service container can load it up - try { - savePropertiesFileToS3(s3, RESOURCES_BUCKET, "ServiceDiscovery.env", - onboarding.getTenantId().toString(), serviceDiscovery); - } catch (Exception e) { - LOGGER.error(e.getMessage()); - LOGGER.error(Utils.getFullStackTrace(e)); - failOnboarding(onboarding.getId(), e.getMessage()); - return; - } - - if (!update.isEmpty()) { - onboarding.setStatus(OnboardingStatus.updating); - dal.updateOnboarding(onboarding); - - // Let the tenant service know the onboarding status - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Tenant Onboarding Status Changed", - Map.of( - "tenantId", onboarding.getTenantId(), - "onboardingStatus", onboarding.getStatus() - ) - ); - } - } else { - LOGGER.error("Can't find onboarding record for {}", detail.get("onboardingId")); - } - } else { - LOGGER.error("Missing onboardingId in event detail {}", Utils.toJson(event.get("detail"))); - } + protected void handleOnboardingDeployed(Map event) { + Map detail = (Map) event.get("detail"); + Onboarding onboarding = dal.updateStatus((String) detail.get("onboardingId"), + OnboardingStatus.deployed.name()); + // Let the tenant service know the onboarding status + Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, + TENANT_ONBOARDING_STATUS_CHANGED, + Map.of( + "tenantId", onboarding.getTenantId(), + "onboardingStatus", onboarding.getStatus() + ) + ); } - protected void handleOnboardingProvisioned(Map event, Context context) { - // Provisioning is complete so we can deploy the workloads. Doing this after all stacks have finished - // instead of as each non base stack finishes because until all services are up and ready the tenant - // can't use the solution. - if (OnboardingEvent.validate(event)) { - Map detail = (Map) event.get("detail"); - Onboarding onboarding = dal.getOnboarding((String) detail.get("onboardingId")); - if (onboarding != null) { - LOGGER.info("Triggering deployment pipelines for tenant {}", onboarding.getTenantId()); - - // Publish a deployment event for each of the configured services in appConfig - Map appConfig = getAppConfig(context); - Map services = (Map) appConfig.get("services"); - for (Map.Entry serviceConfig : services.entrySet()) { - Map service = (Map) serviceConfig.getValue(); - Map serviceCompute = (Map) service.get("compute"); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Workload Ready For Deployment", - Map.of( - "tenantId", onboarding.getTenantId(), - "repository-name", serviceCompute.get("containerRepo"), - "image-tag", serviceCompute.get("containerTag") - ) - ); - } + protected void handleOnboardingFailed(Map event) { + Map detail = (Map) event.get("detail"); + Onboarding onboarding = dal.updateStatus((String) detail.get("onboardingId"), + OnboardingStatus.failed.name()); + // Let the tenant service know the onboarding status + Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, + TENANT_ONBOARDING_STATUS_CHANGED, + Map.of( + "tenantId", onboarding.getTenantId(), + "onboardingStatus", onboarding.getStatus() + ) + ); + } - // Publish an event to subscribe this tenant to the billing system if needed - if (Utils.isNotBlank(onboarding.getRequest().getBillingPlan())) { - // TODO Onboarding probably shouldn't be sending the plan in -- just the tier and/or tenant - LOGGER.info("Publishing billing setup event for tenant {}", onboarding.getTenantId()); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Billing Tenant Setup", - Map.of("tenantId", onboarding.getTenantId(), - "planId", onboarding.getRequest().getBillingPlan() - ) - ); - } else { - LOGGER.info("Skipping billing setup, no billing plan for tenant {}", onboarding.getTenantId()); - } - } else { - LOGGER.error("Can't find onboarding record for {}", detail.get("onboardingId")); - } + protected void handleBillingEvent(Map event) { + String detailType = (String) event.get("detail-type"); + if ("Billing Subscribed".equals(detailType)) { + LOGGER.info("Handling Billing Subscribed Event"); + handleBillingSubscribedEvent(event); } else { - LOGGER.error("Missing onboardingId in event detail {}", Utils.toJson(event.get("detail"))); + LOGGER.error("Can't find billing event for detail-type {}", event.get("detail-type")); } } - protected void handleOnboardingDeploymentPipelineCreated(Map event, Context context) { - // TODO stack events don't have the onboardingId, so we can't use OnboardingEvent::validate as written + protected void handleBillingSubscribedEvent(Map event) { Map detail = (Map) event.get("detail"); - if (detail != null && detail.containsKey("tenantId") && detail.containsKey("stackId") - && detail.containsKey("stackName") && detail.containsKey("pipeline")) { - String tenantId = (String) detail.get("tenantId"); - String stackId = (String) detail.get("stackId"); - String stackName = (String) detail.get("stackName"); - String pipeline = (String) detail.get("pipeline"); - + String tenantId = (String) detail.get("tenantId"); + if (Utils.isNotBlank(tenantId)) { Onboarding onboarding = dal.getOnboardingByTenantId(tenantId); if (onboarding != null) { - for (OnboardingStack stack : onboarding.getStacks()) { - if (stackId.equals(stack.getArn())) { - LOGGER.info("Updating onboarding {} stack {} pipeline {}", onboarding.getId(), - stackName, pipeline); - stack.setPipeline(pipeline); - dal.updateOnboarding(onboarding); - break; - } - } - } else { - LOGGER.error("Can't find onboarding record for tenant {}", tenantId); - } - } else { - LOGGER.error("Missing required keys in event detail {}", Utils.toJson(event.get("detail"))); - } - } - - protected void handleOnboardingDeploymentPipelineChanged(Map event, Context context) { - if ("aws.codepipeline".equals(event.get("source"))) { - List resources = (List) event.get("resources"); - Map detail = (Map) event.get("detail"); - String pipeline = (String) detail.get("pipeline"); - try { - String pipelineArnFromResource = resources.get(0); - LOGGER.info("pipelineArnFromResource = {} when handle onboarding pipeline status changed", pipelineArnFromResource); - final String[] lambdaArn = context.getInvokedFunctionArn().split(":"); - final String partition = lambdaArn[1]; - String pipelineArn = pipelineArnFromResource.replace("arn:aws:", "arn:" + partition + ":"); - LOGGER.info("Fetching tenant id from CodePipeline tags"); - ListTagsForResourceResponse tagsResponse = codePipeline.listTagsForResource(request -> request - .resourceArn(pipelineArn) + onboarding = dal.updateStatus(onboarding.getId(), OnboardingStatus.completed); + // Let the tenant service know the onboarding status + Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, + TENANT_ONBOARDING_STATUS_CHANGED, + Map.of( + "tenantId", onboarding.getTenantId(), + "onboardingStatus", onboarding.getStatus() + ) + ); + // Let everyone know this onboarding request is completed + Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, + OnboardingEvent.ONBOARDING_COMPLETED.detailType(), + Map.of("onboardingId", onboarding.getId()) ); - Tag tenantTag = tagsResponse.tags().stream().filter(t -> "Tenant".equals(t.key())).findFirst().get(); - String tenantId = tenantTag.value(); - Onboarding onboarding = dal.getOnboardingByTenantId(tenantId); - if (onboarding != null) { - tenantId = onboarding.getTenantId().toString(); - - String pipelineState = (String) detail.get("state"); - for (OnboardingStack stack : onboarding.getStacks()) { - if (pipeline.equals(stack.getPipeline())) { - // When the pipeline is created it is automatically started (there's no way to prevent this) - // and will fail because the source for the pipeline is not available when it's created. Even - // if we made the source available (the docker image to deploy), there's no guarantee that the - // container infrastructure would be ready yet. We trigger the first run of the pipeline after - // all of the infrastructure is provisioned. - - // Skip setting the failed status the first time around - if ("FAILED".equals(pipelineState) && Utils.isEmpty(stack.getPipelineStatus())) { - LOGGER.info("Onboarding {} stack {} ignoring initial failed pipeline state", - onboarding.getId(), stack.getName()); - break; - } - - // Otherwise, update the pipeline status - LOGGER.info("Updating onboarding {} stack {} pipeline {} state to {}", onboarding.getId(), - stack.getName(), pipeline, pipelineState); - stack.setPipelineStatus(pipelineState); - onboarding = dal.updateOnboarding(onboarding); - break; - } - } - - if ("STARTED".equals(pipelineState)) { - dal.updateStatus(onboarding.getId(), OnboardingStatus.deploying); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Tenant Onboarding Status Changed", - Map.of( - "tenantId", tenantId, - "onboardingStatus", OnboardingStatus.deploying - ) - ); - } else if ("FAILED".equals(pipelineState) || "CANCELED".equals(pipelineState)) { - boolean firstFailure = false; - for (OnboardingStack stack : onboarding.getStacks()) { - if (pipeline.equals(stack.getPipeline())) { - if (Utils.isEmpty(stack.getPipelineStatus())) { - firstFailure = true; - } - break; - } - } - if (!firstFailure) { - failOnboarding(onboarding.getId(), "Pipeline " + pipeline + " failed"); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Tenant Onboarding Status Changed", - Map.of( - "tenantId", tenantId, - "onboardingStatus", "failed" - ) - ); - } - } else if ("SUCCEEDED".equals(pipelineState)) { - if (onboarding.stacksDeployed()) { - dal.updateStatus(onboarding.getId(), OnboardingStatus.deployed); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Tenant Onboarding Status Changed", - Map.of( - "tenantId", tenantId, - "onboardingStatus", OnboardingStatus.deployed - ) - ); - } - } - } else { - LOGGER.error("Can't find onboarding record for tenant {}", tenantId); - } - } catch (Exception e) { - LOGGER.error("Error fetching tenant id from pipeline {}", pipeline); - LOGGER.error(Utils.getFullStackTrace(e)); - } - } - } - - protected void handleOnboardingDeployed(Map event, Context context) { - - } - - protected void handleOnboardingFailed(Map event, Context context) { - - } - - public SQSBatchResponse processValidateOnboardingQueue(SQSEvent event, Context context) { - if (Utils.isBlank(SAAS_BOOST_EVENT_BUS)) { - throw new IllegalStateException("Missing required environment variable SAAS_BOOST_EVENT_BUS"); - } - if (Utils.isBlank(ONBOARDING_VALIDATION_DLQ)) { - throw new IllegalStateException("Missing required environment variable ONBOARDING_VALIDATION_DLQ"); - } - List retry = new ArrayList<>(); - List fatal = new ArrayList<>(); - sqsMessageLoop: - for (SQSEvent.SQSMessage message : event.getRecords()) { - String messageId = message.getMessageId(); - String messageBody = message.getBody(); - - LinkedHashMap detail = Utils.fromJson(messageBody, LinkedHashMap.class); - String onboardingId = (String) detail.get("onboardingId"); - LOGGER.info("Processing onboarding validation for {}", onboardingId); - Onboarding onboarding = dal.getOnboarding(onboardingId); - OnboardingRequest onboardingRequest = onboarding.getRequest(); - if (onboardingRequest == null) { - LOGGER.error("No onboarding request data for {}", onboardingId); - fatal.add(message); - failOnboarding(onboardingId, "Onboarding record has no request content"); - } else if (OnboardingStatus.validating != onboarding.getStatus()) { - LOGGER.warn("Onboarding in unexpected state for validation {} {}", onboardingId, onboarding.getStatus()); - fatal.add(message); - failOnboarding(onboardingId, "Onboarding can't be validated when in state " - + onboarding.getStatus()); } else { - Map appConfig = getAppConfig(context); - // Check to see if there are any images in the ECR repo before allowing onboarding - Map services = (Map) appConfig.get("services"); - if (services.isEmpty()) { - LOGGER.warn("No application services defined in AppConfig"); - retry.add(SQSBatchResponse.BatchItemFailure.builder() - .withItemIdentifier(messageId) - .build() - ); - } else { - int missingImages = 0; - for (Map.Entry serviceConfig : services.entrySet()) { - String serviceName = serviceConfig.getKey(); - Map service = (Map) serviceConfig.getValue(); - Map serviceCompute = (Map) service.get("compute"); - String ecrRepo = (String) serviceCompute.get("containerRepo"); - String imageTag = (String) serviceCompute.getOrDefault("containerTag", "latest"); - if (Utils.isNotBlank(ecrRepo)) { - try { - ListImagesResponse dockerImages = ecr.listImages(request -> request - .repositoryName(ecrRepo)); - boolean imageAvailable = false; - // ListImagesResponse::hasImageIds will return true if the imageIds object is not null - if (dockerImages.hasImageIds()) { - for (ImageIdentifier image : dockerImages.imageIds()) { - if (imageTag.equals(image.imageTag())) { - imageAvailable = true; - break; - } - } - } - if (!imageAvailable) { - // Not valid yet, no container image to deploy - LOGGER.warn("Application Service {} does not have an image tagged {}", - serviceName, imageTag); - missingImages++; - } - } catch (EcrException ecrError) { - LOGGER.error("ecr:ListImages error {}", ecrError.awsErrorDetails().errorMessage()); - LOGGER.error(Utils.getFullStackTrace(ecrError)); - // TODO do we bail here or retry? - failOnboarding(onboardingId, "Can't list images from ECR " - + ecrError.awsErrorDetails().errorMessage()); - fatal.add(message); - continue sqsMessageLoop; - } - } else { - // TODO no repo defined for this service yet... - LOGGER.warn("Application Service {} has no container image repository defined", - serviceName); - missingImages++; - } - } - if (missingImages > 0) { - retry.add(SQSBatchResponse.BatchItemFailure.builder() - .withItemIdentifier(messageId) - .build() - ); - continue; - } - - // Do we have any CIDR blocks left for a new tenant VPC - if (!dal.availableCidrBlock()) { - LOGGER.error("No CIDR blocks available for new VPC"); - failOnboarding(onboardingId, "No CIDR blocks available for new VPC"); - fatal.add(message); - continue; - } - - // Make sure we're using a unique subdomain per tenant - String subdomain = onboardingRequest.getSubdomain(); - if (Utils.isNotBlank(subdomain)) { - String hostedZoneId = (String) appConfig.get("hostedZone"); - String domainName = (String) appConfig.get("domainName"); - if (Utils.isBlank(hostedZoneId) || Utils.isBlank(domainName)) { - LOGGER.error("Can't onboard a subdomain without domain name and hosted zone"); - failOnboarding(onboardingId, "Can't define tenant subdomain " + subdomain - + " without a domain name and hosted zone."); - fatal.add(message); - continue; - } else { - // Ask Route53 for all the records of this hosted zone - try { - ListResourceRecordSetsResponse recordSets = route53.listResourceRecordSets(r -> r - .hostedZoneId(hostedZoneId) - ); - if (recordSets.hasResourceRecordSets()) { - boolean duplicateSubdomain = false; - for (ResourceRecordSet recordSet : recordSets.resourceRecordSets()) { - if (RRType.A == recordSet.type()) { - // Hosted Zone alias for the tenant subdomain - String recordSetName = recordSet.name(); - String existingSubdomain = recordSetName.substring(0, - recordSetName.indexOf(domainName) - 1); - if (subdomain.equalsIgnoreCase(existingSubdomain)) { - duplicateSubdomain = true; - break; - } - } - } - if (duplicateSubdomain) { - LOGGER.error("Tenant subdomain " + subdomain - + " is already in use for this hosted zone."); - failOnboarding(onboardingId, "Tenant subdomain " + subdomain - + " is already in use for this hosted zone."); - fatal.add(message); - continue; - } - } - } catch (Route53Exception route53Error) { - LOGGER.error("route53:ListResourceRecordSets error", route53Error); - LOGGER.error(Utils.getFullStackTrace(route53Error)); - failOnboarding(onboardingId, "Can't list Route53 record sets " - + route53Error.awsErrorDetails().errorMessage()); - fatal.add(message); - continue; - } - } - } - - // Check if Quotas will be exceeded. - try { - Map retMap = checkLimits(context); - Boolean passed = (Boolean) retMap.get("passed"); - String quotaMessage = (String) retMap.get("message"); - if (!passed) { - LOGGER.error("Provisioning will exceed limits. {}", quotaMessage); - failOnboarding(onboardingId, "Provisioning will exceed limits " + quotaMessage); - fatal.add(message); - continue; - } - } catch (Exception e) { - LOGGER.warn("Error checking Service Quotas with Private API quotas/check", e); - LOGGER.warn((Utils.getFullStackTrace(e))); - // TODO retry here and see if Quotas comes back online? - retry.add(SQSBatchResponse.BatchItemFailure.builder() - .withItemIdentifier(messageId) - .build() - ); - continue; - } - - // If we made it to the end without continuing on to the next SQS message, - // this message is valid - LOGGER.info("Onboarding request validated for {}", onboardingId); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, "saas-boost", - OnboardingEvent.ONBOARDING_VALID.detailType(), - Map.of("onboardingId", onboarding.getId()) - ); - } + LOGGER.error("Can't find onboarding record for tenant {}", detail.get("tenantId")); } + } else { + LOGGER.error("Missing tenantId in event detail {}", Utils.toJson(event.get("detail"))); } - if (!fatal.isEmpty()) { - LOGGER.info("Moving non-recoverable failures to DLQ"); - SendMessageBatchResponse dlq = sqs.sendMessageBatch(request -> request - .queueUrl(ONBOARDING_VALIDATION_DLQ) - .entries(fatal.stream() - .map(msg -> SendMessageBatchRequestEntry.builder() - .id(msg.getMessageId()) - .messageBody(msg.getBody()) - .build() - ) - .collect(Collectors.toList()) - ) - ); - LOGGER.info(dlq.toString()); - } - return SQSBatchResponse.builder().withBatchItemFailures(retry).build(); } public SQSBatchResponse processTenantConfigQueue(SQSEvent event, Context context) { @@ -1424,870 +611,70 @@ public SQSBatchResponse processTenantConfigQueue(SQSEvent event, Context context return SQSBatchResponse.builder().withBatchItemFailures(retry).build(); } - protected void handleAppConfigEvent(Map event, Context context) { - String detailType = (String) event.get("detail-type"); - if ("Application Configuration Changed".equals(detailType)) { - handleUpdateInfrastructure(event, context); - } else if ("Application Configuration Update Completed".equals(detailType)) { - handleUpdateTenantBaseInfrastructure(event, context); - } - } - - protected void handleUpdateInfrastructure(Map event, Context context) { - if (Utils.isBlank(API_GATEWAY_HOST)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_HOST"); - } - if (Utils.isBlank(API_GATEWAY_STAGE)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_STAGE"); - } - if (Utils.isBlank(API_TRUST_ROLE)) { - throw new IllegalStateException("Missing required environment variable API_TRUST_ROLE"); - } - LOGGER.info("Handling App Config Update Infrastructure Event"); - - String stackName = getSetting(context, "SAAS_BOOST_STACK"); - - Map appConfig = getAppConfig(context); - Map services = (Map) appConfig.get("services"); - - LOGGER.info("Calling cloudFormation update-stack --stack-name {}", stackName); - String stackId; - try { - UpdateStackResponse cfnResponse = cfn.updateStack(UpdateStackRequest.builder() - .stackName(stackName) - .usePreviousTemplate(Boolean.TRUE) - .capabilitiesWithStrings("CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND") - .parameters(new CoreStackParameters().forUpdate( - Map.of( - "ApplicationServices", String.join(",", services.keySet()), - "AppExtensions", collectAppExtensions(appConfig) - ) - )) - .build() - ); - stackId = cfnResponse.stackId(); - LOGGER.info("OnboardingService::updateAppConfig stack id " + stackId); - } catch (SdkServiceException cfnError) { - // CloudFormation throws a 400 error if it doesn't detect any resources in a stack - // need to be updated. Swallow this error. - if (cfnError.getMessage().contains("No updates are to be performed")) { - LOGGER.warn("cloudformation::updateStack error {}", cfnError.getMessage()); - } else { - LOGGER.error("cloudformation::updateStack failed {}", cfnError.getMessage()); - LOGGER.error(Utils.getFullStackTrace(cfnError)); - throw cfnError; - } - } - } - - protected void handleUpdateTenantBaseInfrastructure(Map event, Context context) { - if (Utils.isBlank(API_GATEWAY_HOST)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_HOST"); - } - if (Utils.isBlank(API_GATEWAY_STAGE)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_STAGE"); - } - if (Utils.isBlank(API_TRUST_ROLE)) { - throw new IllegalStateException("Missing required environment variable API_TRUST_ROLE"); - } - LOGGER.info("Handling App Config Update Tenant Base Infrastructure Event"); - - List> provisionedTenants = getProvisionedTenants(context); - if (!provisionedTenants.isEmpty()) { - LOGGER.info("Updating {} provisioned tenants", provisionedTenants.size()); - Map appConfig = getAppConfig(context); - - List parameters = new OnboardingBaseStackParameters().forUpdate( - Map.of( - "DomainName", (String) appConfig.get("domainName"), - "HostedZoneId", (String) appConfig.get("hostedZone"), - "SSLCertificateArn", (String) appConfig.get("sslCertificate"), - "PrivateServices", Boolean.toString(hasPrivateServices(appConfig)), - "DeployActiveDirectory", Boolean.toString(hasActiveDirectoryConfigured(appConfig)) - ) - ); - - for (Map tenant : provisionedTenants) { - Onboarding onboarding = dal.getOnboardingByTenantId((String) tenant.get("id")); - if (onboarding != null) { - OnboardingStack baseStack = onboarding.baseStack(); - if (baseStack != null) { - boolean update = false; - String stackName = baseStack.getName(); - LOGGER.info("Calling cloudFormation update-stack --stack-name {}", stackName); - try { - cfn.updateStack(UpdateStackRequest.builder() - .stackName(stackName) - .usePreviousTemplate(Boolean.TRUE) - .capabilitiesWithStrings("CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND") - .parameters(parameters) - .build() - ); - baseStack.setStatus("UPDATE_IN_PROGRESS"); - onboarding.setStatus(OnboardingStatus.updating); - dal.updateOnboarding(onboarding); - update = true; - } catch (SdkServiceException cfnError) { - // CloudFormation throws a 400 error if it doesn't detect any resources in a stack - // need to be updated. - if (cfnError.getMessage().contains("No updates are to be performed")) { - LOGGER.warn("cloudformation::updateStack {}", cfnError.getMessage()); - // However, there may be changes to the updated app config that effect - // the services, so we need to publish that the base stack has been - // updated successfully - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - OnboardingEvent.ONBOARDING_BASE_UPDATED.detailType(), - Map.of("onboardingId", onboarding.getId()) - ); - } else { - LOGGER.error("cloudformation::updateStack {}", cfnError.getMessage()); - LOGGER.error(Utils.getFullStackTrace(cfnError)); - throw cfnError; - } - } - - if (update) { - // Let the tenant service know the onboarding status - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Tenant Onboarding Status Changed", - Map.of( - "tenantId", tenant.get("id"), - "onboardingStatus", onboarding.getStatus() - ) - ); - } - } else { - LOGGER.error("Can't find base stack in onboarding record for tenant {}", tenant.get("id")); - } - } else { - LOGGER.error("Can't find onboarding record for tenant {}", tenant.get("id")); - } - } - } - } - - protected void handleTenantEvent(Map event, Context context) { - String detailType = (String) event.get("detail-type"); - if ("Tenant Deleted".equals(detailType)) { - handleTenantDeleted(event, context); - } else if ("Tenant Disabled".equals(detailType)) { - handleTenantDisabled(event, context); - } else if ("Tenant Enabled".equals(detailType)) { - handleTenantEnabled(event, context); - } - } - - protected void handleTenantDeleted(Map event, Context context) { - LOGGER.info("Handling Tenant Deleted Event"); - Map detail = (Map) event.get("detail"); - String tenantId = (String) detail.get("tenantId"); - Onboarding onboarding = dal.getOnboardingByTenantId(tenantId); - if (onboarding != null) { - LOGGER.info("Deleting application stacks for tenant {}", tenantId); - for (OnboardingStack stack : onboarding.getStacks()) { - if (!stack.isBaseStack() && !Arrays.asList("DELETE_COMPLETE", "DELETE_IN_PROGRESS") - .contains(stack.getStatus())) { - try { - LOGGER.info("Deleting stack {}", stack.getName()); - cfn.deleteStack(request -> request.stackName(stack.getArn())); - stack.setStatus("DELETE_IN_PROGRESS"); - } catch (SdkServiceException cfnError) { - if (cfnError.getMessage().contains("does not exist")) { - LOGGER.warn("Stack {} does not exist!", stack.getArn()); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Onboarding Stack Status Changed", - Map.of("tenantId", tenantId, - "stackId", stack.getArn(), - "stackStatus", "DELETE_COMPLETE") - ); - } else { - LOGGER.error("CloudFormation error", cfnError); - LOGGER.error(Utils.getFullStackTrace(cfnError)); - } - } - } - } - onboarding.setStatus(OnboardingStatus.deleting); - dal.updateOnboarding(onboarding); - - // Let the tenant service know the onboarding status - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Tenant Onboarding Status Changed", - Map.of( - "tenantId", tenantId, - "onboardingStatus", onboarding.getStatus() - ) - ); - - // Just in case we're called with no app stacks - if (onboarding.appStacksDeleted()) { - handleBaseProvisioningReadyToDelete(event, context); - } - } else { - // Can't find an onboarding record for this id - LOGGER.error("Can't find onboarding record for tenant {}", detail.get("tenantId")); - // TODO Throw here? Would end up in DLQ. - } - } - - protected void handleTenantDisabled(Map event, Context context) { - LOGGER.info("Handling Tenant Disabled Event"); - enableDisableTenant(event, context, true); - } - - protected void handleTenantEnabled(Map event, Context context) { - LOGGER.info("Handling Tenant Enabled Event"); - enableDisableTenant(event, context, false); - } - - private void enableDisableTenant(Map event, Context context, boolean disable) { - Map detail = (Map) event.get("detail"); - String tenantId = (String) detail.get("tenantId"); - Onboarding onboarding = dal.getOnboardingByTenantId(tenantId); - if (onboarding != null) { - boolean update = false; - for (OnboardingStack stack : onboarding.getStacks()) { - // We disable tenant access to the application by swapping the load balancer listener rules - // to a fixed response error string instead of a forward to the target group. We have to do - // this on each application service stack because the default load balancer rule in the base - // provisioning stack isn't used as long as there are any other listener rules on the ALB. - if (!stack.isBaseStack()) { - LOGGER.info("Calling cloudFormation update-stack --stack-name {}", stack.getName()); - try { - UpdateStackResponse cfnResponse = cfn.updateStack(UpdateStackRequest.builder() - .stackName(stack.getArn()) - .usePreviousTemplate(Boolean.TRUE) - .capabilitiesWithStrings("CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND") - .parameters(new OnboardingAppStackParameters().forUpdate( - Map.of( - "Disable", String.valueOf(disable) - ) - )) - .build() - ); - String stackId = cfnResponse.stackId(); - if (!stack.getArn().equals(stackId)) { - LOGGER.error("Updating stack id does not equal existing stack arn"); - } - update = true; - stack.setStatus("UPDATE_IN_PROGRESS"); - } catch (SdkServiceException cfnError) { - // CloudFormation throws a 400 error if it doesn't detect any resources in a stack - // need to be updated. Swallow this error. - if (cfnError.getMessage().contains("No updates are to be performed")) { - LOGGER.warn("cloudformation::updateStack error {}", cfnError.getMessage()); - } else { - LOGGER.error("cloudformation::updateStack failed {}", cfnError.getMessage()); - LOGGER.error(Utils.getFullStackTrace(cfnError)); - throw cfnError; - } - } - } - } - - if (update) { - onboarding.setStatus(OnboardingStatus.updating); - dal.updateOnboarding(onboarding); - - // Let the tenant service know the onboarding status - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Tenant Onboarding Status Changed", - Map.of( - "tenantId", tenantId, - "onboardingStatus", onboarding.getStatus() - ) - ); - } - } else { - // Can't find an onboarding record for this id - LOGGER.error("Can't find onboarding record for tenant {}", detail.get("tenantId")); - } - } - - protected void handleBaseProvisioningReadyToDelete(Map event, Context context) { - LOGGER.info("Handling Tenant Deleted Event"); - Map detail = (Map) event.get("detail"); - String tenantId = (String) detail.get("tenantId"); - Onboarding onboarding = dal.getOnboardingByTenantId(tenantId); - if (onboarding != null) { - boolean update = false; - if (onboarding.appStacksDeleted()) { - for (OnboardingStack stack : onboarding.getStacks()) { - if (stack.isBaseStack() && !Arrays.asList("DELETE_COMPLETE", "DELETE_IN_PROGRESS") - .contains(stack.getStatus())) { - try { - LOGGER.info("Deleting base stacks for tenant {}", tenantId); - cfn.deleteStack(request -> request.stackName(stack.getArn())); - update = true; - stack.setStatus("DELETE_IN_PROGRESS"); - } catch (SdkServiceException cfnError) { - if (cfnError.getMessage().contains("does not exist")) { - LOGGER.warn("Stack {} does not exist!", stack.getArn()); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Onboarding Stack Status Changed", - Map.of("tenantId", tenantId, - "stackId", stack.getArn(), - "stackStatus", "DELETE_COMPLETE") - ); - } else { - LOGGER.error("CloudFormation error", cfnError); - LOGGER.error(Utils.getFullStackTrace(cfnError)); - } - } - } - } - - if (update) { - onboarding.setStatus(OnboardingStatus.deleting); - dal.updateOnboarding(onboarding); - - // Let the tenant service know the onboarding status - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, - "Tenant Onboarding Status Changed", - Map.of( - "tenantId", tenantId, - "onboardingStatus", onboarding.getStatus() - ) - ); - } - } else { - LOGGER.error("App stacks still exist. Can't delete base stacks."); - } - } else { - // Can't find an onboarding record for this id - LOGGER.error("Can't find onboarding record for tenant {}", detail.get("tenantId")); - // TODO Throw here? Would end up in DLQ. - } - } - protected void failOnboarding(String onboardingId, String message) { failOnboarding(UUID.fromString(onboardingId), message); } protected void failOnboarding(UUID onboardingId, String message) { dal.updateStatus(onboardingId, OnboardingStatus.failed); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, "saas-boost", - OnboardingEvent.ONBOARDING_FAILED.detailType(), Map.of("onboardingId", onboardingId, - "message", message)); - } - - protected Map checkLimits(Context context) { - if (Utils.isBlank(API_GATEWAY_HOST)) { - throw new IllegalStateException("Missing environment variable API_GATEWAY_HOST"); - } - if (Utils.isBlank(API_GATEWAY_STAGE)) { - throw new IllegalStateException("Missing environment variable API_GATEWAY_STAGE"); - } - if (Utils.isBlank(API_TRUST_ROLE)) { - throw new IllegalStateException("Missing environment variable API_TRUST_ROLE"); - } - long startMillis = System.currentTimeMillis(); - Map valMap; - ApiRequest tenantsRequest = ApiRequest.builder() - .resource("quotas/check") - .method("GET") - .build(); - SdkHttpFullRequest apiRequest = ApiGatewayHelper.getApiRequest(API_GATEWAY_HOST, API_GATEWAY_STAGE, tenantsRequest); - String responseBody; - try { - LOGGER.info("API call for quotas/check"); - responseBody = ApiGatewayHelper.signAndExecuteApiRequest(apiRequest, API_TRUST_ROLE, context.getAwsRequestId()); - //LOGGER.info("API response for quoatas/check: " + responseBody); - valMap = Utils.fromJson(responseBody, HashMap.class); - } catch (Exception e) { - LOGGER.error("Error invoking API quotas/check"); - LOGGER.error(Utils.getFullStackTrace(e)); - throw new RuntimeException(e); - } - - LOGGER.debug("checkLimits: Total time to check service limits: " + (System.currentTimeMillis() - startMillis)); - return valMap; - } - - protected Map getAppConfig(Context context) { - // Fetch all of the services configured for this application - LOGGER.info("Calling settings service to fetch app config"); - String getAppConfigResponseBody = ApiGatewayHelper.signAndExecuteApiRequest( - ApiGatewayHelper.getApiRequest( - API_GATEWAY_HOST, - API_GATEWAY_STAGE, - ApiRequest.builder() - .resource("settings/config") - .method("GET") - .build() - ), - API_TRUST_ROLE, - context.getAwsRequestId() + Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, + OnboardingEvent.ONBOARDING_FAILED.detailType(), + Map.of( + "onboardingId", onboardingId, + "message", message) ); - Map appConfig = Utils.fromJson(getAppConfigResponseBody, LinkedHashMap.class); - return appConfig; } - protected String getSetting(Context context, String setting) { - LOGGER.info("Calling settings service to fetch setting {}", setting); - // Have to cheat here and ask for a secret until we can authenticate against the public api - // or we have to copy the settings get by id resource to the private api. - String getAppConfigResponseBody = ApiGatewayHelper.signAndExecuteApiRequest( - ApiGatewayHelper.getApiRequest( - API_GATEWAY_HOST, - API_GATEWAY_STAGE, - ApiRequest.builder() - .resource("settings/" + setting + "/secret") - .method("GET") - .build() - ), - API_TRUST_ROLE, - context.getAwsRequestId() - ); - Map settingObject = Utils.fromJson(getAppConfigResponseBody, LinkedHashMap.class); - return (String) settingObject.get("value"); - } + interface OnboardingServiceDependencyFactory { - protected Map getSettings(Context context, String... settings) { - LOGGER.info("Calling settings service to fetch settings {}", settings); - StringBuilder queryParams = new StringBuilder(); - for (Iterator iter = Arrays.stream(settings).iterator(); iter.hasNext();) { - queryParams.append("setting="); - queryParams.append(iter.next()); - if (iter.hasNext()) { - queryParams.append("&"); - } - } - String getSettingsResponseBody = ApiGatewayHelper.signAndExecuteApiRequest( - ApiGatewayHelper.getApiRequest( - API_GATEWAY_HOST, - API_GATEWAY_STAGE, - ApiRequest.builder() - .resource("settings?" + queryParams.toString()) - .method("GET") - .build() - ), - API_TRUST_ROLE, - context.getAwsRequestId() - ); - List> settingsList = Utils.fromJson(getSettingsResponseBody, ArrayList.class); - return settingsList.stream() - .collect(Collectors.toMap( - entry -> entry.get("name"), - entry -> entry.get("value") - ) - ); - } + S3Client s3(); - protected Map getTenant(UUID tenantId, Context context) { - if (tenantId == null) { - throw new IllegalArgumentException("Can't fetch blank tenant id"); - } - return getTenant(tenantId.toString(), context); - } + EventBridgeClient eventBridge(); - protected Map getTenant(String tenantId, Context context) { - if (Utils.isBlank(tenantId)) { - throw new IllegalArgumentException("Can't fetch blank tenant id"); - } - // Fetch the tenant for this onboarding - LOGGER.info("Calling tenant service to fetch tenant {}", tenantId); - String getTenantResponseBody = ApiGatewayHelper.signAndExecuteApiRequest( - ApiGatewayHelper.getApiRequest( - API_GATEWAY_HOST, - API_GATEWAY_STAGE, - ApiRequest.builder() - .resource("tenants/" + tenantId) - .method("GET") - .build() - ), - API_TRUST_ROLE, - context.getAwsRequestId() - ); - Map tenant = Utils.fromJson(getTenantResponseBody, LinkedHashMap.class); - return tenant; - } + S3Presigner s3Presigner(); - protected List> getProvisionedTenants(Context context) { - // Fetch all of the provisioned tenants - LOGGER.info("Calling tenant service to fetch all provisioned tenants"); - String getTenantsResponseBody = ApiGatewayHelper.signAndExecuteApiRequest( - ApiGatewayHelper.getApiRequest( - API_GATEWAY_HOST, - API_GATEWAY_STAGE, - ApiRequest.builder() - .resource("tenants?status=provisioned") - .method("GET") - .build() - ), - API_TRUST_ROLE, - context.getAwsRequestId() - ); - List> tenants = Utils.fromJson(getTenantsResponseBody, ArrayList.class); - if (tenants == null) { - tenants = new ArrayList<>(); - } - return tenants; - } - - protected static Map getPathPriority(Map appConfig) { - Map services = (Map) appConfig.get("services"); - Map pathLength = new HashMap<>(); + SqsClient sqs(); - // Collect the string length of the path for each public service - for (Map.Entry serviceConfig : services.entrySet()) { - String serviceName = serviceConfig.getKey(); - Map service = (Map) serviceConfig.getValue(); - Boolean isPublic = (Boolean) service.get("public"); - if (isPublic) { - String pathPart = Objects.toString(service.get("path"), ""); - pathLength.put(serviceName, pathPart.length()); - } - } - // Order the services by longest (most specific) to shortest (least specific) path length - LinkedHashMap pathPriority = pathLength.entrySet().stream() - .sorted(Map.Entry.comparingByValue(Collections.reverseOrder())) - .collect(Collectors.toMap( - Map.Entry::getKey, Map.Entry::getValue, (value1, value2) -> value1, LinkedHashMap::new - )); - // Set the ALB listener rule priority so that the most specific paths (the longest ones) have - // a higher priority than the less specific paths so the rules are evaluated in the proper order - // i.e. a path of /feature* needs to be evaluate before a catch all path of /* or you'll never - // route to the /feature* rule because /* will have already matched - int priority = 0; - for (String publicService : pathPriority.keySet()) { - pathPriority.put(publicService, ++priority); - } - return pathPriority; + OnboardingDataAccessLayer dal(); } - protected static String collectAppExtensions(Map appConfig) { - Set appExtensions = new HashSet<>(); - Map services = (Map) appConfig.get("services"); - for (Map.Entry service : services.entrySet()) { - Map serviceConfig = (Map) service.getValue(); - if (serviceConfig.containsKey("s3") && serviceConfig.get("s3") != null) { - appExtensions.add("s3"); - } - } - return String.join(",", appExtensions); - } + private static final class DefaultDependencyFactory implements OnboardingServiceDependencyFactory { - protected static boolean hasPrivateServices(Map appConfig) { - Map> services = - (Map>) appConfig.get("services"); - boolean privateServices = false; - for (Map service : services.values()) { - privateServices = privateServices || !(Boolean) service.get("public"); + @Override + public S3Client s3() { + return Utils.sdkClient(S3Client.builder(), S3Client.SERVICE_NAME); } - return privateServices; - } - protected static boolean hasActiveDirectoryConfigured(Map appConfig) { - Map> services = - (Map>) appConfig.get("services"); - boolean configureAd = false; - for (Map service : services.values()) { - Map filesystem = (Map) service.get("filesystem"); - if (filesystem != null) { - configureAd = configureAd || (Boolean) filesystem.getOrDefault("configureManagedAd", false); - } + @Override + public EventBridgeClient eventBridge() { + return Utils.sdkClient(EventBridgeClient.builder(), EventBridgeClient.SERVICE_NAME); } - return configureAd; - } - protected void onboardingAppStackTenantParams(Onboarding onboarding, OnboardingAppStackParameters parameters, - Context context) { - String tenantId = onboarding.getTenantId().toString(); - Map tenant = getTenant(tenantId, context); - // TODO tenant == null means tenant API call failed? retry? - if (tenant != null) { - Map> tenantResources = (Map>) tenant.get("resources"); + @Override + public S3Presigner s3Presigner() { try { - parameters.setProperty("TenantId", (String) tenant.get("id")); - parameters.setProperty("Tier", (String) tenant.get("tier")); - parameters.setProperty("VPC", tenantResources.get("VPC").get("name")); - parameters.setProperty("SubnetPrivateA", tenantResources.get("PRIVATE_SUBNET_A").get("name")); - parameters.setProperty("SubnetPrivateB", tenantResources.get("PRIVATE_SUBNET_B").get("name")); - parameters.setProperty("PrivateRouteTable", tenantResources.get("PRIVATE_ROUTE_TABLE").get("name")); - parameters.setProperty("ECSCluster", tenantResources.get("ECS_CLUSTER").get("name")); - parameters.setProperty("ECSSecurityGroup", tenantResources.get("ECS_SECURITY_GROUP").get("name")); - - // Will only exist if private services are defined - if (tenantResources.containsKey("PRIVATE_SERVICE_DISCOVERY_NAMESPACE")) { - parameters.setProperty("ServiceDiscoveryNamespace", - tenantResources.get("PRIVATE_SERVICE_DISCOVERY_NAMESPACE").get("name")); - } - - // Depending on the SSL certificate configuration, one of these 2 listeners must exist - if (tenantResources.containsKey("HTTP_LISTENER")) { - parameters.setProperty("ECSLoadBalancerHttpListener", - tenantResources.get("HTTP_LISTENER").get("arn")); - } - if (tenantResources.containsKey("HTTPS_LISTENER")) { - parameters.setProperty("ECSLoadBalancerHttpsListener", - tenantResources.get("HTTPS_LISTENER").get("arn")); - } - - if (tenantResources.containsKey("ACTIVE_DIRECTORY_ID")) { - String directoryId = tenantResources.get("ACTIVE_DIRECTORY_ID").get("name"); - DescribeDirectoriesResponse directoriesResponse = ds.describeDirectories(request -> request - .directoryIds(directoryId)); - DirectoryDescription directory = directoriesResponse.directoryDescriptions().get(0); - parameters.setProperty("ActiveDirectoryId", directoryId); - parameters.setProperty("ActiveDirectoryDnsIps", String.join(",", directory.dnsIpAddrs())); - parameters.setProperty("ActiveDirectoryDnsName", directory.name()); // Not Access URL - if (tenantResources.containsKey("ACTIVE_DIRECTORY_CREDENTIALS")) { - parameters.setProperty("ActiveDirectoryCredentials", - tenantResources.get("ACTIVE_DIRECTORY_CREDENTIALS").get("arn")); - } - } - } catch (Exception e) { - throw new RuntimeException("Error parsing resources for tenant " + tenantId, e); - } - } else { - throw new RuntimeException("Can't fetch tenant " + tenantId); - } - } - - protected void onboardingAppStackServiceParams(Map.Entry serviceConfig, - Map pathPriority, - OnboardingAppStackParameters parameters, - Properties serviceDiscovery, - Context context) { - // CloudFormation resource names can only contain alpha numeric characters or a dash - String serviceName = serviceConfig.getKey(); - String serviceResourceName = serviceName.replaceAll("[^0-9A-Za-z-]", "").toLowerCase(); - parameters.setProperty("ServiceName", serviceName); - parameters.setProperty("ServiceResourceName", serviceResourceName); - - Map service = (Map) serviceConfig.getValue(); - // Load all the compute parameters for this service and tier the tenant is onboarding into - onboardingAppStackComputeParams(service, parameters); - - // Setup the network routing for this service - Boolean isPublic = (Boolean) service.get("public"); - parameters.setProperty("PubliclyAddressable", isPublic.toString()); - if (isPublic) { - parameters.setProperty("PublicPathRoute", (String) service.get("path")); - parameters.setProperty("PublicPathRoute", (String) service.get("path")); - parameters.setProperty("PublicPathRulePriority", pathPriority.get(serviceName).toString()); - } else { - // If there are any private services, we will create an environment variables called - // SERVICE__HOST and SERVICE__PORT to pass to the task definitions - String serviceEnvName = Utils.toUpperSnakeCase(serviceName); - String serviceHost = "SERVICE_" + serviceEnvName + "_HOST"; - String servicePort = "SERVICE_" + serviceEnvName + "_PORT"; - LOGGER.debug("Creating service discovery environment variables {}, {}", serviceHost, servicePort); - serviceDiscovery.put(serviceHost, serviceResourceName + ".local"); - serviceDiscovery.put(servicePort, parameters.getProperty("ContainerPort")); - } - - // Load all the object storage parameters for this service and tier the tenant is onboarding into - onboardingAppStackObjectStorageParams(service, parameters); - - // Load up all the shared file system parameters for this service and tier the tenant is onboarding into - onboardingAppStackFileSystemParams(service, parameters, context); - - // Load up all the relational database parameters for this service and tier the tenant is onboarding into - onboardingAppStackDatabaseParams(service, parameters); - } - - protected void onboardingAppStackComputeParams(Map service, - OnboardingAppStackParameters parameters) { - Map compute = (Map) service.get("compute"); - parameters.setProperty("ContainerRepository", (String) compute.get("containerRepo")); - parameters.setProperty("ContainerRepositoryTag", (String) compute.get("containerTag")); - parameters.setProperty("TaskLaunchType", (String) compute.get("ecsLaunchType")); - // CloudFormation won't let you use dashes or underscores in Mapping second level key names - // And it won't let you use Fn::Join or Fn::Split in Fn::FindInMap... so we will mangle this - // parameter before we send it in. - String containerOperatingSystem = ((String) compute.getOrDefault("operatingSystem", "")) - .replace("_", ""); - parameters.setProperty("ContainerOS", containerOperatingSystem); - parameters.setProperty("ContainerPort", ((Integer) compute.get("containerPort")).toString()); - parameters.setProperty("ContainerHealthCheckPath", (String) compute.get("healthCheckUrl")); - parameters.setProperty("EnableECSExec", ((Boolean) compute.get("ecsExecEnabled")).toString()); - - String tier = parameters.getProperty("Tier"); - Map tiers = (Map) compute.get("tiers"); - if (!tiers.containsKey(tier)) { - throw new RuntimeException("Missing compute definition for tier " + tier); - } - Map computeTier = (Map) tiers.get(tier); - parameters.setProperty("ClusterInstanceType", (String) computeTier.get("instanceType")); - parameters.setProperty("TaskMemory", ((Integer) computeTier.get("memory")).toString()); - parameters.setProperty("TaskCPU", ((Integer) computeTier.get("cpu")).toString()); - parameters.setProperty("MinTaskCount", ((Integer) computeTier.get("min")).toString()); - parameters.setProperty("MaxTaskCount", ((Integer) computeTier.get("max")).toString()); - parameters.setProperty("MinAutoScalingGroupSize", ((Integer) computeTier.get("ec2min")).toString()); - parameters.setProperty("MaxAutoScalingGroupSize", ((Integer) computeTier.get("ec2max")).toString()); - } - - protected void onboardingAppStackFileSystemParams(Map service, - OnboardingAppStackParameters parameters, - Context context) { - Map filesystem = (Map) service.get("filesystem"); - // Does this service use a shared filesystem? - if (filesystem != null && !filesystem.isEmpty()) { - parameters.setProperty("FileSystemMountPoint", (String) filesystem.get("mountPoint")); - - String tier = parameters.getProperty("Tier"); - Map tiers = (Map) filesystem.get("tiers"); - if (!tiers.containsKey(tier)) { - throw new RuntimeException("Missing compute definition for tier " + tier); - } - Map filesystemTierConfig = (Map) tiers.get(tier); - - String fileSystemType = (String) filesystem.get("type"); - if ("EFS".equals(fileSystemType)) { - parameters.setProperty("UseEFS", "true"); - parameters.setProperty("EncryptEFS", ((Boolean) filesystemTierConfig.get("encrypt")).toString()); - parameters.setProperty("EFSLifecyclePolicy", (String) filesystemTierConfig.get("lifecycle")); - } else if ("FSX_WINDOWS".equals(fileSystemType) || "FSX_ONTAP".equals(fileSystemType)) { - parameters.setProperty("UseFSx", "true"); - parameters.setProperty("FSxFileSystemType", fileSystemType); - parameters.setProperty("FileSystemStorage", ((Integer) filesystemTierConfig.get("storageGb")).toString()); - parameters.setProperty("FileSystemThroughput", - ((Integer) filesystemTierConfig.get("throughputMbs")).toString()); - parameters.setProperty("FSxWindowsMountDrive", (String) filesystemTierConfig.get("windowsMountDrive")); - parameters.setProperty("FSxDailyBackupTime", (String) filesystemTierConfig.get("dailyBackupTime")); - parameters.setProperty("FSxBackupRetention", - ((Integer) filesystemTierConfig.get("backupRetentionDays")).toString()); - parameters.setProperty("FSxWeeklyMaintenanceTime", - (String) filesystemTierConfig.get("weeklyMaintenanceTime")); - if ("FSX_ONTAP".equals(fileSystemType)) { - parameters.setProperty("OntapVolumeSize", ((Integer) filesystemTierConfig.get("volumeSize")).toString()); - } - } else { - parameters.setProperty("UseEFS", "false"); - parameters.setProperty("UseFSx", "false"); - } - } - } - - protected void onboardingAppStackDatabaseParams(Map service, - OnboardingAppStackParameters parameters) { - Map database = (Map) service.get("database"); - // Does this service use a relational database? - if (database != null && !database.isEmpty()) { - parameters.setProperty("UseRDS", "true"); - parameters.setProperty("RDSEngine", (String) database.get("engineName")); - parameters.setProperty("RDSEngineVersion", (String) database.get("version")); - parameters.setProperty("RDSParameterGroupFamily", (String) database.get("family")); - parameters.setProperty("RDSDatabase", (String) database.get("database")); - parameters.setProperty("RDSPort", ((Integer) database.get("port")).toString()); - parameters.setProperty("RDSUsername", (String) database.get("username")); - parameters.setProperty("RDSPasswordParam", (String) database.get("passwordParam")); - parameters.setProperty("RDSBootstrap", (String) database.get("bootstrapFilename")); - - String tier = parameters.getProperty("Tier"); - Map tiers = (Map) database.get("tiers"); - if (!tiers.containsKey(tier)) { - throw new RuntimeException("Missing database definition for tier " + tier); + return S3Presigner.builder() + .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) + .region(Region.of(AWS_REGION)) + .endpointOverride(new URI("https://" + S3Client.SERVICE_NAME + "." + + Region.of(AWS_REGION) + + "." + + Utils.endpointSuffix(AWS_REGION))) + .build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); } - Map databaseTierConfig = (Map) tiers.get(tier); - parameters.setProperty("RDSInstanceClass", (String) databaseTierConfig.get("instanceClass")); - } else { - parameters.setProperty("UseRDS", "false"); - } - } - - protected void onboardingAppStackObjectStorageParams(Map service, - OnboardingAppStackParameters parameters) { - Map s3 = (Map) service.get("s3"); - if (s3 != null) { - parameters.setProperty("TenantStorageBucket", (String) s3.get("bucketName")); - } - } - - protected OnboardingStack createOnboardingAppStack(Onboarding onboarding, - Map.Entry serviceConfig, - Map pathPriority, - OnboardingAppStackParameters parameters, - Properties serviceDiscovery, - Context context) { - OnboardingStack stack = null; - try { - onboardingAppStackServiceParams(serviceConfig, pathPriority, parameters, - serviceDiscovery, context); - } catch (RuntimeException e) { - LOGGER.error(e.getMessage()); - LOGGER.error(Utils.getFullStackTrace(e)); - failOnboarding(onboarding.getId(), e.getMessage()); - return stack; } - // Make the stack name look like what CloudFormation would have done for a nested stack - String tenantId = onboarding.getTenantId().toString(); - String tenantShortId = tenantId.substring(0, 8); - String stackName = "sb-" + SAAS_BOOST_ENV + "-tenant-" + tenantShortId + "-app-" - + parameters.getProperty("ServiceResourceName") + "-" - + Utils.randomString(12).toUpperCase(); - if (stackName.length() > 128) { - stackName = stackName.substring(0, 128); + @Override + public SqsClient sqs() { + return Utils.sdkClient(SqsClient.builder(), SqsClient.SERVICE_NAME); } - try { - // Now run the onboarding stack to provision the infrastructure for this application service - LOGGER.info("OnboardingService create stack " + stackName); - String templateUrl = "https://" + SAAS_BOOST_BUCKET + ".s3." + AWS_REGION - + "." + Utils.endpointSuffix(AWS_REGION) + "/tenant-onboarding-app.yaml"; - String stackId; - try { - CreateStackResponse cfnResponse = cfn.createStack(CreateStackRequest.builder() - .stackName(stackName) - .disableRollback(false) - .capabilitiesWithStrings("CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND") - .notificationARNs(ONBOARDING_APP_STACK_SNS) - .templateURL(templateUrl) - .parameters(parameters.forCreate()) - .build() - ); - stackId = cfnResponse.stackId(); - - // Save state in the Onboarding database - stack = OnboardingStack.builder() - .service(parameters.getProperty("ServiceName")) - .name(stackName) - .arn(stackId) - .baseStack(false) - .status("CREATE_IN_PROGRESS") - .build(); - onboarding.addStack(stack); - onboarding.setStatus(OnboardingStatus.provisioning); - onboarding = dal.updateOnboarding(onboarding); - LOGGER.info("OnboardingService stack id " + stackId); - } catch (CloudFormationException cfnError) { - LOGGER.error("cloudformation::createStack failed", cfnError); - LOGGER.error(Utils.getFullStackTrace(cfnError)); - failOnboarding(onboarding.getId(), cfnError.awsErrorDetails().errorMessage()); - } - } catch (RuntimeException e) { - // Template parameters validation failed - LOGGER.error(e.getMessage()); - LOGGER.error(Utils.getFullStackTrace(e)); - failOnboarding(onboarding.getId(), e.getMessage()); - } - return stack; - } - - protected void savePropertiesFileToS3(S3Client s3, String bucket, String filename, - String tenantId, Properties properties) { - // Write the application-wide environment variables to S3 so each service container can load it up - String environmentFile = "tenants/" + tenantId + "/" + filename; - ByteArrayOutputStream environmentFileContents = new ByteArrayOutputStream(); - try (Writer writer = new BufferedWriter(new OutputStreamWriter( - environmentFileContents, StandardCharsets.UTF_8) - )) { - properties.store(writer, null); - s3.putObject(request -> request - .bucket(bucket) - .key(environmentFile) - .build(), - RequestBody.fromBytes(environmentFileContents.toByteArray()) - ); - } catch (S3Exception s3Error) { - LOGGER.error("Error putting service discovery file to S3 {}", s3Error.awsErrorDetails().errorMessage()); - throw s3Error; - } catch (IOException ioe) { - LOGGER.error("Error writing data to output stream"); - throw new RuntimeException(ioe); + @Override + public OnboardingDataAccessLayer dal() { + return new OnboardingDataAccessLayer(Utils.sdkClient(DynamoDbClient.builder(), DynamoDbClient.SERVICE_NAME), + ONBOARDING_TABLE); } } } \ No newline at end of file diff --git a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingStack.java b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingStack.java deleted file mode 100644 index 8032c069..00000000 --- a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingStack.java +++ /dev/null @@ -1,174 +0,0 @@ -package com.amazon.aws.partners.saasfactory.saasboost; - -public class OnboardingStack { - - private String service; - private String name; - private String arn; - private boolean baseStack; - private String status; - private String pipeline; - private String pipelineStatus; - - private OnboardingStack() { - } - - private OnboardingStack(Builder builder) { - this.service = builder.service; - this.name = builder.name; - this.arn = builder.arn; - this.baseStack = builder.baseStack; - this.status = builder.status; - this.pipeline = builder.pipeline; - this.pipelineStatus = builder.pipelineStatus; - } - - public static Builder builder() { - return new Builder(); - } - - public static Builder builder(OnboardingStack copyMe) { - return new Builder() - .service(copyMe.service) - .name(copyMe.name) - .arn(copyMe.arn) - .baseStack(copyMe.baseStack) - .status(copyMe.status) - .pipeline(copyMe.pipeline) - .pipelineStatus(copyMe.pipelineStatus); - } - - public String getService() { - return service; - } - - public String getName() { - return name; - } - - public String getArn() { - return arn; - } - - public boolean isBaseStack() { - return baseStack; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } - - public String getPipeline() { - return pipeline; - } - - public void setPipeline(String pipeline) { - this.pipeline = pipeline; - } - - public String getPipelineStatus() { - return pipelineStatus; - } - - public void setPipelineStatus(String pipelineStatus) { - this.pipelineStatus = pipelineStatus; - } - - public boolean isComplete() { - return "CREATE_COMPLETE".equals(getStatus()) || "UPDATE_COMPLETE".equals(getStatus()); - } - - public boolean isDeployed() { - return (isComplete() && isBaseStack()) || (isComplete() && "SUCCEEDED".equals(getPipelineStatus())); - } - - public boolean isDeleted() { - return "DELETE_COMPLETE".equals(getStatus()); - } - - public boolean isCreated() { - return "CREATE_COMPLETE".equals(getStatus()); - } - - public boolean isUpdated() { - return "UPDATE_COMPLETE".equals(getStatus()); - } - - public String getCloudFormationUrl() { - String url = null; - if (arn != null) { - String[] stackId = arn.split(":"); - if (stackId.length > 4) { - String region = stackId[3]; - url = String.format( - "https://%s.console.aws.amazon.com/cloudformation/home?" - + "region=%s" - + "#/stacks/stackinfo?filteringText=&filteringStatus=active" - + "&viewNested=true&hideStacks=false" - + "&stackId=%s", - region, - region, - arn - ); - } - } - return url; - } - - public static final class Builder { - - private String service; - private String name; - private String arn; - private boolean baseStack; - private String status; - private String pipeline; - private String pipelineStatus; - - private Builder() { - } - - public Builder service(String service) { - this.service = service; - return this; - } - - public Builder name(String name) { - this.name = name; - return this; - } - - public Builder arn(String arn) { - this.arn = arn; - return this; - } - - public Builder baseStack(boolean baseStack) { - this.baseStack = baseStack; - return this; - } - - public Builder status(String status) { - this.status = status; - return this; - } - - public Builder pipeline(String pipeline) { - this.pipeline = pipeline; - return this; - } - - public Builder pipelineStatus(String pipelineStatus) { - this.pipelineStatus = pipelineStatus; - return this; - } - - public OnboardingStack build() { - return new OnboardingStack(this); - } - } -} diff --git a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingStatus.java b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingStatus.java index 6ebb94d0..71ea9488 100644 --- a/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingStatus.java +++ b/services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingStatus.java @@ -29,6 +29,7 @@ public enum OnboardingStatus { failed, deleting, deleted, + completed, unknown; public static OnboardingStatus fromStackStatus(String stackStatus) { diff --git a/services/onboarding-service/src/main/resources/appConfig.json b/services/onboarding-service/src/main/resources/appConfig.json deleted file mode 100644 index 601283e0..00000000 --- a/services/onboarding-service/src/main/resources/appConfig.json +++ /dev/null @@ -1,146 +0,0 @@ -{ - "name": "Multi Container", - "domainName": "", - "hostedZone": "", - "sslCertificate": "", - "billing": null, - "application": { - "services": { - "main": { - "name": "main", - "description": "Main Service", - "public": true, - "path": "/*", - "healthCheckUrl": "/", - "operatingSystem": "LINUX", - "containerPort": 80, - "containerRepo": "", - "containerTag": "latest", - "tiers": { - "default": { - "instanceType": "t3.medium", - "cpu": 512, - "memory": 1024, - "min": 1, - "max": 2, - "database": { - "engine": "MARIADB", - "instance": "T3_MICRO", - "version": "10.5.12", - "family": "mariadb10.5", - "database": "boost", - "username": "boost", - "password": "/saas-boost/mc/DB_MASTER_PASSWORD", - "bootstrapFilename": "", - "port": 3306, - "engineName": "mariadb", - "instanceClass": "db.t3.micro" - }, - "filesystem": { - "fileSystemType": "EFS", - "mountPoint": "/mnt", - "fsx": null, - "efs": { - "encryptAtRest": true, - "lifecycle": 0, - "filesystemLifecycle": "NEVER" - } - } - }, - "premium": { - "instanceType": "m5.xlarge", - "cpu": 2048, - "memory": 4096, - "min": 2, - "max": 6, - "database": { - "engine": "MARIADB", - "instance": "T3_LARGE", - "version": "10.5.12", - "family": "mariadb10.5", - "database": "boost", - "username": "boost", - "password": "/saas-boost/mc/DB_MASTER_PASSWORD", - "bootstrapFilename": "", - "port": 3306, - "engineName": "mariadb", - "instanceClass": "db.t3.large" - }, - "filesystem": { - "fileSystemType": "EFS", - "mountPoint": "/mnt", - "fsx": null, - "efs": { - "encryptAtRest": true, - "lifecycle": 0, - "filesystemLifecycle": "NEVER" - } - } - } - } - }, - "internal": { - "name": "internal", - "description": "Internal Service", - "public": false, - "path": null, - "healthCheckUrl": "/", - "operatingSystem": "LINUX", - "containerPort": 80, - "containerRepo": "sb-mc-core-1dpih9lvcvyuf-internal-r1sjma8wjygs", - "containerTag": "latest", - "tiers": { - "default": { - "instanceType": "t3.micro", - "cpu": 512, - "memory": 1024, - "min": 1, - "max": 2, - "database": null, - "filesystem": null - }, - "premium": { - "instanceType": "m5.medium", - "cpu": 1024, - "memory": 2048, - "min": 2, - "max": 4, - "database": null, - "filesystem": null - } - } - }, - "feature": { - "name": "feature", - "description": "Feature Service", - "public": true, - "path": "/feature*", - "healthCheckUrl": "/", - "operatingSystem": "LINUX", - "containerPort": 80, - "containerRepo": "sb-mc-core-1dpih9lvcvyuf-feature-yudogr4llitc", - "containerTag": "latest", - "tiers": { - "default": { - "instanceType": "t3.micro", - "cpu": 512, - "memory": 1024, - "min": 1, - "max": 2, - "database": null, - "filesystem": null - }, - "premium": { - "instanceType": "m5.medium", - "cpu": 1024, - "memory": 2048, - "min": 2, - "max": 4, - "database": null, - "filesystem": null - } - } - } - } - } -} \ No newline at end of file diff --git a/services/onboarding-service/src/main/resources/appConfigSingle.json b/services/onboarding-service/src/main/resources/appConfigSingle.json deleted file mode 100644 index 60d51733..00000000 --- a/services/onboarding-service/src/main/resources/appConfigSingle.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "name": "Multi Container", - "domainName": "", - "hostedZone": "", - "sslCertificate": "", - "services": { - "main": { - "name": "main", - "description": "Main Service", - "public": true, - "path": "/*", - "healthCheckUrl": "/health", - "operatingSystem": "LINUX", - "containerPort": 7000, - "containerRepo": "", - "containerTag": "latest", - "tiers": { - "default": { - "instanceType": "t3.medium", - "cpu": 512, - "memory": 1024, - "min": 1, - "max": 2, - "database": null, - "filesystem": null - } - } - }, - "feature": { - "name": "feature", - "description": "Feature service public path routing", - "public": true, - "path": "/feature*", - "healthCheckUrl": "/health", - "operatingSystem": "LINUX", - "containerPort": 7000, - "containerRepo": "", - "containerTag": "latest", - "tiers": { - "default": { - "instanceType": "t3.medium", - "cpu": 512, - "memory": 1024, - "min": 1, - "max": 2, - "database": null, - "filesystem": null - } - } - }, - "internal": { - "name": "internal", - "description": "Internal private service", - "public": false, - "path": "", - "healthCheckUrl": "/health", - "operatingSystem": "LINUX", - "containerPort": 7000, - "containerRepo": "", - "containerTag": "latest", - "tiers": { - "default": { - "instanceType": "t3.medium", - "cpu": 512, - "memory": 1024, - "min": 1, - "max": 2, - "database": null, - "filesystem": null - } - } - } - }, - "billing": null -} \ No newline at end of file diff --git a/services/onboarding-service/src/main/resources/spotbugs-exclude.xml b/services/onboarding-service/src/main/resources/spotbugs-exclude.xml new file mode 100644 index 00000000..e7f7feea --- /dev/null +++ b/services/onboarding-service/src/main/resources/spotbugs-exclude.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/MockDependencyFactory.java b/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/MockDependencyFactory.java new file mode 100644 index 00000000..639f9851 --- /dev/null +++ b/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/MockDependencyFactory.java @@ -0,0 +1,60 @@ +package com.amazon.aws.partners.saasfactory.saasboost; + +import software.amazon.awssdk.services.eventbridge.EventBridgeClient; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.sqs.SqsClient; + +public class MockDependencyFactory implements OnboardingService.OnboardingServiceDependencyFactory { + + private S3Client s3; + private EventBridgeClient eventBridge; + private S3Presigner s3Presigner; + private SqsClient sqs; + private OnboardingDataAccessLayer dal; + + @Override + public S3Client s3() { + return s3; + } + + @Override + public EventBridgeClient eventBridge() { + return eventBridge; + } + + @Override + public S3Presigner s3Presigner() { + return s3Presigner; + } + + @Override + public SqsClient sqs() { + return sqs; + } + + @Override + public OnboardingDataAccessLayer dal() { + return dal; + } + + public void setS3(S3Client s3) { + this.s3 = s3; + } + + public void setEventBridge(EventBridgeClient eventBridge) { + this.eventBridge = eventBridge; + } + + public void setS3Presigner(S3Presigner s3Presigner) { + this.s3Presigner = s3Presigner; + } + + public void setSqs(SqsClient sqs) { + this.sqs = sqs; + } + + public void setDal(OnboardingDataAccessLayer dal) { + this.dal = dal; + } +} diff --git a/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingServiceDALTest.java b/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingDataAccessLayerTest.java similarity index 55% rename from services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingServiceDALTest.java rename to services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingDataAccessLayerTest.java index 5b235898..63a2ae41 100644 --- a/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingServiceDALTest.java +++ b/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingDataAccessLayerTest.java @@ -16,73 +16,71 @@ package com.amazon.aws.partners.saasfactory.saasboost; -import org.junit.BeforeClass; -import org.junit.Test; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; -import java.util.stream.Collectors; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; -public class OnboardingServiceDALTest { +public class OnboardingDataAccessLayerTest { private static UUID onboardingId; private static UUID tenantId; - private static List stacks; - @BeforeClass + @BeforeAll public static void setup() throws Exception { onboardingId = UUID.fromString("f11cadd8-9c3c-40be-9106-4d64e2478daf"); tenantId = UUID.fromString("c9a437c5-68bc-47ab-a4d5-4e6bbd089914"); - stacks = new ArrayList<>(); - stacks.add(OnboardingStack.builder().baseStack(true).name("BaseStack").build()); - stacks.add(OnboardingStack.builder().baseStack(false).name("AppStack").build()); } + @Test public void testToAttributeValueMap() { - Onboarding onboarding = new Onboarding(); LocalDateTime created = LocalDateTime.now(); LocalDateTime modified = LocalDateTime.now(); + OnboardingRequest onboardingRequest = OnboardingRequest.builder() + .name("Unit Test") + .tier("default") + .subdomain("tenant-subdomain") + .build(); + + Onboarding onboarding = new Onboarding(); onboarding.setId(onboardingId); onboarding.setCreated(created); onboarding.setModified(modified); onboarding.setStatus(OnboardingStatus.created); onboarding.setTenantId(tenantId); - onboarding.setRequest(new OnboardingRequest("Unit Test", "default")); - onboarding.setStacks(stacks); + onboarding.setRequest(onboardingRequest); onboarding.setZipFile("foobar"); - onboarding.setEcsClusterLocked(false); Map expected = new HashMap<>(); expected.put("id", AttributeValue.builder().s(onboardingId.toString()).build()); - expected.put("created", AttributeValue.builder().s(created.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); - expected.put("modified", AttributeValue.builder().s(modified.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); + expected.put("created", AttributeValue.builder().s( + created.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); + expected.put("modified", AttributeValue.builder().s( + modified.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); expected.put("status", AttributeValue.builder().s(OnboardingStatus.created.name()).build()); expected.put("tenant_id", AttributeValue.builder().s(tenantId.toString()).build()); expected.put("zip_file", AttributeValue.builder().s("foobar").build()); - expected.put("request", AttributeValue.builder().m(Map.of( - "name", AttributeValue.builder().s("Unit Test").build(), - "tier", AttributeValue.builder().s("default").build()) - ).build()); - expected.put("stacks", AttributeValue.builder().l(stacks.stream() - .map(stack -> AttributeValue.builder().m(Map.of( - "name", AttributeValue.builder().s(stack.getName()).build(), - "baseStack", AttributeValue.builder().bool(stack.isBaseStack()).build() - )).build()) - .collect(Collectors.toList()) + expected.put("request", AttributeValue.builder().m( + Map.of( + "name", AttributeValue.builder().s(onboardingRequest.getName()).build(), + "tier", AttributeValue.builder().s(onboardingRequest.getTier()).build(), + "subdomain", AttributeValue.builder().s(onboardingRequest.getSubdomain()).build() + ) ).build()); - expected.put("ecs_cluster_locked", AttributeValue.builder().bool(false).build()); - Map actual = OnboardingServiceDAL.toAttributeValueMap(onboarding); + Map actual = OnboardingDataAccessLayer.toAttributeValueMap(onboarding); // DynamoDB marshalling - assertEquals("Size unequal", expected.size(), actual.size()); + assertEquals(expected.size(), actual.size(), + () -> "Expected size " + expected.size() + " != actual size " + actual.size()); expected.keySet().stream().forEach(key -> { - assertEquals("Value mismatch for '" + key + "'", expected.get(key), actual.get(key)); + assertEquals(expected.get(key), actual.get(key), () -> "Value mismatch for '" + key + "'"); }); // Have we reflected all class properties we serialize for API calls in DynamoDB? @@ -90,7 +88,8 @@ public void testToAttributeValueMap() { json.keySet().stream() .map(key -> Utils.toSnakeCase(key)) .forEach(key -> { - assertTrue("Class property '" + key + "' does not exist in DynamoDB attribute map", actual.containsKey(key)); + assertTrue(actual.containsKey(key), + () -> "Class property '" + key + "' does not exist in DynamoDB attribute map"); }); } } diff --git a/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingEventTest.java b/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingEventTest.java new file mode 100644 index 00000000..020b7258 --- /dev/null +++ b/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingEventTest.java @@ -0,0 +1,27 @@ +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; + +public class OnboardingEventTest { + + @Test + public void testValidate() { + String event = "{\n" + + " \"version\": \"0\",\n" + + " \"id\": \"c99f48e7-d0cb-5bfe-0adc-e0413809028b\",\n" + + " \"detail-type\": \"Onboarding Validated\",\n" + + " \"source\": \"saas-boost\",\n" + + " \"account\": \"012345678901\",\n" + + " \"time\": \"2023-01-01T00:00:00Z\",\n" + + " \"region\": \"us-east-1\",\n" + + " \"resources\": [],\n" + + " \"detail\": {\n" + + " \"onboardingId\": \"2f2ffb71-972d-494d-bd95-e832b07db690\"\n" + + " }\n" + + "}"; + Assertions.assertTrue(OnboardingEvent.validate(Utils.fromJson(event, LinkedHashMap.class))); + } +} diff --git a/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingServiceTest.java b/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingServiceTest.java index 0bc893e0..ac0b5b69 100644 --- a/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingServiceTest.java +++ b/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingServiceTest.java @@ -16,104 +16,72 @@ package com.amazon.aws.partners.saasfactory.saasboost; -import org.junit.Test; -import org.mockito.ArgumentMatcher; -import software.amazon.awssdk.services.cloudformation.CloudFormationClient; -import software.amazon.awssdk.services.cloudformation.model.CloudFormationException; -import software.amazon.awssdk.services.cloudformation.model.DescribeStackResourceRequest; -import software.amazon.awssdk.services.cloudformation.model.DescribeStackResourceResponse; -import software.amazon.awssdk.services.cloudformation.model.StackResourceDetail; -import software.amazon.awssdk.services.route53.Route53Client; -import software.amazon.awssdk.services.route53.model.HostedZone; -import software.amazon.awssdk.services.route53.model.HostedZoneConfig; -import software.amazon.awssdk.services.route53.model.ListHostedZonesByNameRequest; -import software.amazon.awssdk.services.route53.model.ListHostedZonesByNameResponse; - -import java.io.InputStream; -import java.util.*; - -import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.amazonaws.services.lambda.runtime.tests.EventLoader; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class OnboardingServiceTest { - @Test - public void cidrBlockPrefixTest() { - String cidr = "10.255.32.3"; - String prefix = cidr.substring(0, cidr.indexOf(".", cidr.indexOf(".") + 1)); - assertTrue("10.255".equals(prefix)); - } + private Context context; + private static UUID onboardingId; + private static Onboarding onboarding; -// @Test -// public void testBatchIteration() { -// final int maxBatchSize = 50; -// List objects = new ArrayList<>(); -// for (int i = 0; i < (maxBatchSize * 3.7); i++) { -// objects.add("Item " + i); -// } -// System.out.println("Objects contains " + objects.size() + " items"); -// int batchStart = 0; -// int batchEnd = 0; -// int loop = 0; -// while (batchEnd < objects.size()) { -// batchStart = batchEnd; -// batchEnd += maxBatchSize; -// if (batchEnd > objects.size()) { -// batchEnd = objects.size(); -// } -// List batch = objects.subList(batchStart, batchEnd); -// System.out.println(String.format("Loop %d. Start %d End %d", ++loop, batchStart, batchEnd)); -// batch.forEach(System.out::println); -// } -// } + @BeforeAll + public static void setup() throws Exception { + onboardingId = UUID.fromString("f11cadd8-9c3c-40be-9106-4d64e2478daf"); + onboarding = new Onboarding(); + onboarding.setId(onboardingId); + } @Test - public void testSubdomainCheck() { - String domainName = "saas-example.com"; - String subdomain = "tenant2"; - String existingSubdomain = "tenant2.saas-example.com."; - existingSubdomain = existingSubdomain.substring(0, existingSubdomain.indexOf(domainName) - 1); - assertTrue("Subdomain Exists", subdomain.equalsIgnoreCase(existingSubdomain)); + public void testGetOnboardingById() { + OnboardingDataAccessLayer mockDal = mock(OnboardingDataAccessLayer.class); + when(mockDal.getOnboarding(onboardingId.toString())).thenReturn(onboarding); + + MockDependencyFactory init = new MockDependencyFactory(); + init.setDal(mockDal); + + OnboardingService onboardingService = new OnboardingService(init); + + APIGatewayProxyRequestEvent request = EventLoader + .loadApiGatewayRestEvent("getOnboardingByIdEvent.json"); + assertEquals(onboardingId.toString(), request.getPathParameters().get("id")); + + APIGatewayProxyResponseEvent response = onboardingService.getOnboarding(request, context); + Onboarding onboarding = Utils.fromJson(response.getBody(), Onboarding.class); + + assertNotNull(onboarding); + assertEquals(onboardingId, onboarding.getId()); } @Test - public void testGetPathPriority() { - InputStream json = getClass().getClassLoader().getResourceAsStream("appConfig.json"); - Map appConfig = Utils.fromJson(json, LinkedHashMap.class); - - Map applicationServices = new HashMap<>(); - for (int i = 1; i <= 10; i++) { - applicationServices.put(String.format("Service%02d", i), Map.of( - "public", Boolean.TRUE, - "path", Utils.randomString((i * 10)) - )); - } - appConfig.put("services", applicationServices); - - Map expected = Map.of( - "Service01", 10, - "Service02", 9, - "Service03", 8, - "Service04", 7, - "Service05", 6, - "Service06", 5, - "Service07", 4, - "Service08", 3, - "Service09", 2, - "Service10", 1 - ); - - Map actual = OnboardingService.getPathPriority(appConfig); - - assertEquals("Size unequal", expected.size(), actual.size()); - expected.keySet().stream().forEach(key -> { - assertEquals("Value mismatch for '" + key + "'", expected.get(key), actual.get(key)); - }); + public void testGetOnboardingByIdInvalid() { + String invalidId = "1234"; + OnboardingDataAccessLayer mockDal = mock(OnboardingDataAccessLayer.class); + when(mockDal.getOnboarding(invalidId)).thenReturn(null); + + MockDependencyFactory init = new MockDependencyFactory(); + init.setDal(mockDal); + + OnboardingService onboardingService = new OnboardingService(init); + + APIGatewayProxyRequestEvent request = EventLoader + .loadApiGatewayRestEvent("getOnboardingByIdInvalidEvent.json"); + assertEquals(invalidId, request.getPathParameters().get("id")); + + APIGatewayProxyResponseEvent response = onboardingService.getOnboarding(request, context); + Onboarding onboarding = Utils.fromJson(response.getBody(), Onboarding.class); + + assertNull(onboarding); + assertEquals(404, response.getStatusCode()); } -} \ No newline at end of file +} diff --git a/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingStackTest.java b/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingStackTest.java deleted file mode 100644 index 272c99b2..00000000 --- a/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingStackTest.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import org.junit.Test; - -import java.util.Arrays; -import java.util.List; - -import static org.junit.Assert.*; - -public class OnboardingStackTest { - - private static final List CLOUDFORMAION_STACK_STATUSES = Arrays.asList( - "CREATE_COMPLETE", - "CREATE_IN_PROGRESS", - "CREATE_FAILED", - "DELETE_COMPLETE", - "DELETE_FAILED", - "DELETE_IN_PROGRESS", - "REVIEW_IN_PROGRESS", - "ROLLBACK_COMPLETE", - "ROLLBACK_FAILED", - "ROLLBACK_IN_PROGRESS", - "UPDATE_COMPLETE", - "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "UPDATE_IN_PROGRESS", - "UPDATE_ROLLBACK_COMPLETE", - "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS", - "UPDATE_ROLLBACK_FAILED", - "UPDATE_ROLLBACK_IN_PROGRESS"); - - private static final List CODEPIPELINE_STATES = Arrays.asList( - "CANCELED", - "FAILED", - "RESUMED", - "STARTED", - "STOPPED", - "STOPPING", - "SUCCEEDED", - "SUPERSEDED" - ); - - @Test - public void testIsComplete() { - for (String status : CLOUDFORMAION_STACK_STATUSES) { - OnboardingStack stack = OnboardingStack.builder().build(); - assertFalse(stack.isComplete()); - stack.setStatus(status); - if ("CREATE_COMPLETE".equals(status)) { - assertTrue(stack.isComplete()); - } else if ("UPDATE_COMPLETE".equals(status)) { - assertTrue(stack.isComplete()); - } else { - assertFalse(stack.isComplete()); - } - } - } - - @Test - public void testIsDeployed() { - for (String status : CLOUDFORMAION_STACK_STATUSES) { - for (String pipelineStatus : CODEPIPELINE_STATES) { - OnboardingStack stack = OnboardingStack.builder().build(); - assertFalse(stack.isDeployed()); - stack.setStatus(status); - stack.setPipelineStatus(pipelineStatus); - - // Base stacks don't get workloads deployed to them, they just need to be complete - OnboardingStack baseStack = OnboardingStack.builder().baseStack(true).build(); - baseStack.setStatus(status); - baseStack.setPipelineStatus(pipelineStatus); - if ("CREATE_COMPLETE".equals(status)) { - assertTrue(baseStack.isDeployed()); - } else if ("UPDATE_COMPLETE".equals(status)) { - assertTrue(baseStack.isDeployed()); - } else { - assertFalse(baseStack.isDeployed()); - } - - // Application stacks get workloads deployed - OnboardingStack appStack = OnboardingStack.builder().baseStack(false).build(); - appStack.setStatus(status); - appStack.setPipelineStatus(pipelineStatus); - if ("SUCCEEDED".equals(appStack.getPipelineStatus())) { - if ("CREATE_COMPLETE".equals(status)) { - assertTrue(appStack.isDeployed()); - } else if ("UPDATE_COMPLETE".equals(status)) { - assertTrue(appStack.isDeployed()); - } else { - assertFalse(appStack.isDeployed()); - } - } else { - assertFalse(appStack.isDeployed()); - } - } - } - } -} diff --git a/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingTest.java b/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingTest.java deleted file mode 100644 index 9bedfd48..00000000 --- a/services/onboarding-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingTest.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import org.junit.Test; - -import java.util.Arrays; - -import static org.junit.Assert.*; - -public class OnboardingTest { - - @Test - public void testBaseStacksComplete() { - OnboardingStack stack1 = OnboardingStack.builder().baseStack(true).build(); - OnboardingStack stack2 = OnboardingStack.builder().baseStack(false).build(); - - Onboarding onboarding = new Onboarding(); - assertFalse("No stacks", onboarding.baseStacksComplete()); - - onboarding.setStacks(Arrays.asList(stack1, stack2)); - assertFalse("Empty stacks", onboarding.baseStacksComplete()); - - stack1.setStatus("CREATE_COMPLETE"); - stack2.setStatus("UPDATE_COMPLETE"); - assertTrue("All base stacks complete", onboarding.baseStacksComplete()); - - OnboardingStack stack3 = OnboardingStack.builder().baseStack(true).build(); - onboarding.addStack(stack3); - assertFalse("Not every base stack is complete", onboarding.baseStacksComplete()); - } - - @Test - public void testStacksComplete() { - OnboardingStack stack1 = OnboardingStack.builder().build(); - OnboardingStack stack2 = OnboardingStack.builder().build(); - - Onboarding onboarding = new Onboarding(); - assertFalse("No stacks", onboarding.stacksComplete()); - - onboarding.setStacks(Arrays.asList(stack1, stack2)); - assertFalse("Empty stacks", onboarding.stacksComplete()); - - stack1.setStatus("CREATE_COMPLETE"); - assertFalse("Not every stack is complete", onboarding.stacksComplete()); - - stack2.setStatus("UPDATE_COMPLETE"); - assertTrue("All stacks complete", onboarding.stacksComplete()); - - onboarding.addStack(OnboardingStack.builder().build()); - assertFalse("Not every stack is complete", onboarding.stacksComplete()); - } - - @Test - public void testHasAppStacks() { - OnboardingStack baseStack = OnboardingStack.builder().baseStack(true).build(); - OnboardingStack appStack = OnboardingStack.builder().baseStack(false).build(); - - Onboarding onboarding = new Onboarding(); - assertFalse("No stacks", onboarding.hasAppStacks()); - - onboarding.addStack(baseStack); - assertFalse("Only base stacks", onboarding.hasAppStacks()); - - onboarding.addStack(appStack); - assertTrue("App stacks", onboarding.hasAppStacks()); - } - - @Test - public void testAppStacksDeleted() { - OnboardingStack baseStack = OnboardingStack.builder().baseStack(true).status("CREATE_COMPLETE").build(); - OnboardingStack appStack1 = OnboardingStack.builder().baseStack(false).status("DELETE_IN_PROGRESS").build(); - OnboardingStack appStack2 = OnboardingStack.builder().baseStack(false).status("DELETE_COMPLETE").build(); - OnboardingStack appStack3 = OnboardingStack.builder().baseStack(false).status("DELETE_COMPLETE").build(); - - Onboarding onboarding = new Onboarding(); - assertTrue("No Stacks", onboarding.appStacksDeleted()); - - onboarding.addStack(baseStack); - onboarding.appStacksDeleted(); - assertTrue("Only base stacks", onboarding.appStacksDeleted()); - - onboarding.addStack(appStack1); - assertFalse("App stacks not deleted", onboarding.appStacksDeleted()); - - onboarding.addStack(appStack2); - assertFalse("App stacks not deleted", onboarding.appStacksDeleted()); - - onboarding.addStack(appStack3); - assertFalse("App stacks not deleted", onboarding.appStacksDeleted()); - - appStack1.setStatus("DELETE_COMPLETE"); - assertTrue("App stacks deleted", onboarding.appStacksDeleted()); - } -} diff --git a/services/onboarding-service/src/test/resources/appConfig.json b/services/onboarding-service/src/test/resources/appConfig.json deleted file mode 100644 index 58c55129..00000000 --- a/services/onboarding-service/src/test/resources/appConfig.json +++ /dev/null @@ -1,144 +0,0 @@ -{ - "name": "Multi Container", - "domainName": "", - "hostedZone": "", - "sslCertificate": "", - "billing": null, - "services": { - "main": { - "name": "main", - "description": "Main Service", - "public": true, - "path": "/*", - "healthCheckURL": "/health", - "operatingSystem": "LINUX", - "containerPort": 80, - "containerRepo": "", - "containerTag": "latest", - "tiers": { - "default": { - "instanceType": "t3.medium", - "cpu": 512, - "memory": 1024, - "minCount": 1, - "maxCount": 2, - "database": { - "engine": "MARIADB", - "instance": "T3_MICRO", - "version": "10.5.12", - "family": "mariadb10.5", - "database": "boost", - "username": "boost", - "password": "/saas-boost/mc/DB_MASTER_PASSWORD", - "bootstrapFilename": "", - "port": 3306, - "engineName": "mariadb", - "instanceClass": "db.t3.micro" - }, - "filesystem": { - "fileSystemType": "EFS", - "mountPoint": "/mnt", - "fsx": null, - "efs": { - "encryptAtRest": true, - "lifecycle": 0, - "filesystemLifecycle": "NEVER" - } - } - }, - "premium": { - "instanceType": "m5.xlarge", - "cpu": 2048, - "memory": 4096, - "minCount": 2, - "maxCount": 6, - "database": { - "engine": "MARIADB", - "instance": "T3_LARGE", - "version": "10.5.12", - "family": "mariadb10.5", - "database": "boost", - "username": "boost", - "password": "/saas-boost/mc/DB_MASTER_PASSWORD", - "bootstrapFilename": "", - "port": 3306, - "engineName": "mariadb", - "instanceClass": "db.t3.large" - }, - "filesystem": { - "fileSystemType": "EFS", - "mountPoint": "/mnt", - "fsx": null, - "efs": { - "encryptAtRest": true, - "lifecycle": 0, - "filesystemLifecycle": "NEVER" - } - } - } - } - }, - "internal": { - "name": "internal", - "description": "Internal Service", - "public": false, - "path": null, - "healthCheckURL": "/", - "operatingSystem": "LINUX", - "containerPort": 80, - "containerRepo": "sb-mc-core-1dpih9lvcvyuf-internal-r1sjma8wjygs", - "containerTag": "latest", - "tiers": { - "default": { - "instanceType": "t3.micro", - "cpu": 512, - "memory": 1024, - "minCount": 1, - "maxCount": 2, - "database": null, - "filesystem": null - }, - "premium": { - "instanceType": "m5.medium", - "cpu": 1024, - "memory": 2048, - "minCount": 2, - "maxCount": 4, - "database": null, - "filesystem": null - } - } - }, - "feature": { - "name": "feature", - "description": "Feature Service", - "public": true, - "path": "/feature*", - "healthCheckURL": "/", - "operatingSystem": "LINUX", - "containerPort": 80, - "containerRepo": "sb-mc-core-1dpih9lvcvyuf-feature-yudogr4llitc", - "containerTag": "latest", - "tiers": { - "default": { - "instanceType": "t3.micro", - "cpu": 512, - "memory": 1024, - "minCount": 1, - "maxCount": 2, - "database": null, - "filesystem": null - }, - "premium": { - "instanceType": "m5.medium", - "cpu": 1024, - "memory": 2048, - "minCount": 2, - "maxCount": 4, - "database": null, - "filesystem": null - } - } - } - } -} \ No newline at end of file diff --git a/services/onboarding-service/src/test/resources/getOnboardingByIdEvent.json b/services/onboarding-service/src/test/resources/getOnboardingByIdEvent.json new file mode 100644 index 00000000..5484c83d --- /dev/null +++ b/services/onboarding-service/src/test/resources/getOnboardingByIdEvent.json @@ -0,0 +1,67 @@ +{ + "resource": "/onboarding/{id}", + "path": "/onboarding/f11cadd8-9c3c-40be-9106-4d64e2478daf", + "httpMethod": "GET", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + "Host": "hija4ve530.execute-api.us-west-2.amazonaws.com", + "User-Agent": "PostmanRuntime/7.32.3", + "X-Amzn-Trace-Id": "Root=1-64cd2e83-39ddcf49101abb5e35038d44", + "X-Forwarded-For": "127.0.0.1", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate, br" + ], + "Authorization": [ + "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + ], + "Host": [ + "hija4ve530.execute-api.us-west-2.amazonaws.com" + ], + "User-Agent": [ + "PostmanRuntime/7.32.3" + ], + "X-Amzn-Trace-Id": [ + "Root=1-64cd2e83-39ddcf49101abb5e35038d44" + ], + "X-Forwarded-For": [ + "127.0.0.1" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "pathParameters": { + "id": "f11cadd8-9c3c-40be-9106-4d64e2478daf" + }, + "requestContext": { + "accountId": "123456789012", + "stage": "v1", + "resourceId": "31hb73", + "requestId": "8eb3b13f-25d7-45b4-84da-5fb9906f9337", + "identity": { + "sourceIp": "127.0.0.1", + "userAgent": "PostmanRuntime/7.32.3" + }, + "resourcePath": "/onboarding/{id}", + "httpMethod": "GET", + "apiId": "hija4ve530", + "path": "/v1/onboarding/f11cadd8-9c3c-40be-9106-4d64e2478daf", + "authorizer": { + "principalId": "123456789012", + "integrationLatency": 3165 + } + }, + "isBase64Encoded": false +} \ No newline at end of file diff --git a/services/onboarding-service/src/test/resources/getOnboardingByIdInvalidEvent.json b/services/onboarding-service/src/test/resources/getOnboardingByIdInvalidEvent.json new file mode 100644 index 00000000..85783882 --- /dev/null +++ b/services/onboarding-service/src/test/resources/getOnboardingByIdInvalidEvent.json @@ -0,0 +1,67 @@ +{ + "resource": "/onboarding/{id}", + "path": "/onboarding/1234", + "httpMethod": "GET", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + "Host": "hija4ve530.execute-api.us-west-2.amazonaws.com", + "User-Agent": "PostmanRuntime/7.32.3", + "X-Amzn-Trace-Id": "Root=1-64cd2e83-39ddcf49101abb5e35038d44", + "X-Forwarded-For": "127.0.0.1", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate, br" + ], + "Authorization": [ + "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + ], + "Host": [ + "hija4ve530.execute-api.us-west-2.amazonaws.com" + ], + "User-Agent": [ + "PostmanRuntime/7.32.3" + ], + "X-Amzn-Trace-Id": [ + "Root=1-64cd2e83-39ddcf49101abb5e35038d44" + ], + "X-Forwarded-For": [ + "127.0.0.1" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "pathParameters": { + "id": "1234" + }, + "requestContext": { + "accountId": "123456789012", + "stage": "v1", + "resourceId": "31hb73", + "requestId": "8eb3b13f-25d7-45b4-84da-5fb9906f9337", + "identity": { + "sourceIp": "127.0.0.1", + "userAgent": "PostmanRuntime/7.32.3" + }, + "resourcePath": "/onboarding/{id}", + "httpMethod": "GET", + "apiId": "hija4ve530", + "path": "/v1/onboarding/1234", + "authorizer": { + "principalId": "123456789012", + "integrationLatency": 3165 + } + }, + "isBase64Encoded": false +} \ No newline at end of file diff --git a/services/pom.xml b/services/pom.xml index 1545d886..cb14d42d 100644 --- a/services/pom.xml +++ b/services/pom.xml @@ -15,12 +15,15 @@ metrics-service onboarding-service - quotas-service settings-service tenant-service tier-service system-user-service + identity-service + + ${project.basedir}/.. + Apache-2.0 @@ -45,8 +48,16 @@ aws-lambda-java-log4j2
    - junit - junit + org.junit.jupiter + junit-jupiter-engine + + + org.junit.jupiter + junit-jupiter-api + + + com.amazonaws + aws-lambda-java-tests org.mockito @@ -56,5 +67,12 @@ org.slf4j slf4j-nop + + com.amazon.aws.partners.saasfactory.saasboost + Utils + 1.0.0 + + provided +
    diff --git a/services/quotas-service/Limits to Check.txt b/services/quotas-service/Limits to Check.txt deleted file mode 100644 index 34c27c1e..00000000 --- a/services/quotas-service/Limits to Check.txt +++ /dev/null @@ -1,142 +0,0 @@ -#Limits to Check - -## elasticloadbalancing - -aws service-quotas list-aws-default-service-quotas --query 'Quotas[*].{Adjustable:Adjustable,Name:QuotaName,Value:Value,Code:QuotaCode}' --service-code elasticloadbalancing --output table -aws service-quotas get-service-quota --service-code elasticloadbalancing --quota-code L-53DA6B97 - True | L-53DA6B97 | Application Load Balancers per Region | 20.0 - - -ALB - AWS::ElasticLoadBalancingV2::LoadBalancer -x ALB Rule - AWS::ElasticLoadBalancingV2::ListenerRule -x Target Group - AWS::ElasticLoadBalancingV2::TargetGroup - - -## autoscaling -aws service-quotas list-aws-default-service-quotas --query 'Quotas[*].{Adjustable:Adjustable,Name:QuotaName,Value:Value,Code:QuotaCode}' --service-code autoscaling --output table -x Autoscaling Target - AWS::ApplicationAutoScaling::ScalableTarget -x Autoscaling ScalingPolicy - AWS::ApplicationAutoScaling::ScalingPolicy - - True | L-CDE20ADC | Auto Scaling groups per region | 200.0 - - - https://github.com/devops-israel/aws-inventory/blob/master/index.html - aws autoscaling describe-account-limits - - { service: "ApplicationAutoScaling", - api: "describeScalableTargets", - params: { ServiceNamespace: "ecs" }, - title: "Application Auto Scaling ECS Scalable Targets", - id: "appscaling-scalable-targets-ecs", - jmespath: "ScalableTargets", - headings: ["ResourceId", "ScalableDimension", "MinCapacity", "MaxCapacity", "RoleARN", "CreationTime"] - }, - -## ecs -Clusters per account is 10000 - True | L-21C621EB | Clusters per account | 10000.0 -Cluster - AWS::ECS::Cluster, default is 10000 per account -ECS task definition - AWS::ECS::TaskDefinition 100 default per region per account - True | L-46458851 | Tasks using the Fargate launch type, per Region, per account | 100.0 - -aws ecs list-clusters - -## fargate -aws service-quotas list-aws-default-service-quotas --query 'Quotas[*].{Adjustable:Adjustable,Name:QuotaName,Value:Value,Code:QuotaCode}' --service-code fargate --output table - -aws service-quotas get-service-quota --service-code fargate --quota-code L-790AF391 - -https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-quotas.html - - True | L-790AF391 | Fargate On-Demand resource count | 100.0 - - - - -ECS Service - AWS::ECS::Service 2000 per cluster - - -## vpc - - True | L-7E9ECCDB | Active VPC peering connections per VPC | 50.0 - - True | L-F678F1CE | VPCs per Region | 5.0 - -x Attach Gateway AWS::EC2::VPCGatewayAttachment - -Internet Gateway AWS::EC2::InternetGateway - True | L-A4707A72 | Internet gateways per Region | 5.0 - -x Route AWS::EC2::Route - -x RouteTable AWS::EC2::RouteTable - -x Subnet AWS::EC2::Subnet -x Route table association AWS::EC2::SubnetRouteTableAssociation -? Transit gateway route: AWS::EC2::TransitGatewayRoute -? Transit Gateway route table association AWS::EC2::TransitGatewayRouteTableAssociation - -x Route 53 Alias AWS::Route53::RecordSet -x CodePipeline AWS::CodePipeline::Pipeline - - -## rds -RDS Aurora instance AWS::RDS::DBInstance -RDS cluster AWS::RDS::DBCluster -| True | L-952B80B8 | DB clusters | 40.0 | -| True | L-7B6409FD | DB instances | 40.0 - -aws rds describe-db-clusters - -x EFS AWS::EFS::FileSystem -x EFS mount target AWS::EFS::MountTarget - -EC2s - -Amazon Elastic Compute Cloud (Amazon EC2) Quota: Customer gateways per region Value: 50.0 - -aws service-quotas list-aws-default-service-quotas --query 'Quotas[*].{Adjustable:Adjustable,Name:QuotaName,Value:Value,Code:QuotaCode}' --service-code ec2 --output table -EC2 on-demand standard VCPUs -aws service-quotas get-service-quota --service-code ec2 --quota-code L-1216C47A - - -Cloudwatcch to get the the EC2 usage metrics - MetricDataQueries: [{ - Id: 'm1', - MetricStat: { - Metric: { - Namespace: 'AWS/Usage', - MetricName: 'ResourceCount', - Dimensions: [{ - Name: 'Service', - Value: 'EC2' - }, - { - Name: 'Resource', - Value: 'vCPU' - }, - { - Name: 'Type', - Value: 'Resource' - }, - { - Name: 'Class', - Value: '' - } - ] - }, - Period: 300, - Stat: 'Maximum' - } - }] - -VCPUs -x EC2 security group AWS::EC2::SecurityGroup -x EC2 security group ingress AWS::EC2::SecurityGroupIngress - - -IAM roles -aws iam get-account-summary - - - diff --git a/services/quotas-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/QuotasService.java b/services/quotas-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/QuotasService.java deleted file mode 100644 index ede54e1e..00000000 --- a/services/quotas-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/QuotasService.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class QuotasService implements RequestHandler, APIGatewayProxyResponseEvent> { - - private static final Logger LOGGER = LoggerFactory.getLogger(QuotasService.class); - private static final String AWS_REGION = System.getenv("AWS_REGION"); - private static final Map CORS = Stream - .of(new AbstractMap.SimpleEntry("Access-Control-Allow-Origin", "*")) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - private final QuotasServiceDAL dal; - - public QuotasService() { - long startTimeMillis = System.currentTimeMillis(); - LOGGER.info("Version Info: {}", Utils.version(this.getClass())); - this.dal = new QuotasServiceDAL(); - LOGGER.info("Constructor init: {}", System.currentTimeMillis() - startTimeMillis); - } - - @Override - public APIGatewayProxyResponseEvent handleRequest(Map event, Context context) { - //Utils.logRequestEvent(event); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); - } - - public APIGatewayProxyResponseEvent checkQuotas(Map event, Context context) { - long startTimeMillis = System.currentTimeMillis(); - if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); - } - - QuotasServiceDAL.QuotaCheck quotaCheck; - if (Utils.isChinaRegion(AWS_REGION)) { - quotaCheck = dal.checkQuotasForCNRegion(); - } else { - quotaCheck = dal.checkQuotas(); - } - - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("SettingsService::getSettings exec " + totalTimeMillis); - return new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(200) - .withBody(Utils.toJson(quotaCheck)); - } - -} \ No newline at end of file diff --git a/services/quotas-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/QuotasServiceDAL.java b/services/quotas-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/QuotasServiceDAL.java deleted file mode 100644 index 68179d88..00000000 --- a/services/quotas-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/QuotasServiceDAL.java +++ /dev/null @@ -1,558 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.core.exception.SdkServiceException; -import software.amazon.awssdk.services.cloudwatch.CloudWatchClient; -import software.amazon.awssdk.services.cloudwatch.model.*; -import software.amazon.awssdk.services.ec2.Ec2Client; -import software.amazon.awssdk.services.ec2.model.DescribeNatGatewaysResponse; -import software.amazon.awssdk.services.ec2.model.NatGateway; -import software.amazon.awssdk.services.ec2.model.NatGatewayState; -import software.amazon.awssdk.services.elasticloadbalancingv2.ElasticLoadBalancingV2Client; -import software.amazon.awssdk.services.elasticloadbalancingv2.model.Limit; -import software.amazon.awssdk.services.rds.RdsClient; -import software.amazon.awssdk.services.rds.model.AccountQuota; -import software.amazon.awssdk.services.servicequotas.ServiceQuotasClient; -import software.amazon.awssdk.services.servicequotas.model.ListServiceQuotasRequest; -import software.amazon.awssdk.services.servicequotas.model.ListServiceQuotasResponse; -import software.amazon.awssdk.services.servicequotas.model.ServiceQuota; - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.*; - -public class QuotasServiceDAL { - - private static final Logger LOGGER = LoggerFactory.getLogger(QuotasServiceDAL.class); - private final ElasticLoadBalancingV2Client elb; - private final Ec2Client ec2; - private final ServiceQuotasClient serviceQuotas; - private final RdsClient rds; - private final CloudWatchClient cloudWatch; - - public QuotasServiceDAL() { - final long startTimeMillis = System.currentTimeMillis(); - this.elb = Utils.sdkClient(ElasticLoadBalancingV2Client.builder(), ElasticLoadBalancingV2Client.SERVICE_NAME); - this.ec2 = Utils.sdkClient(Ec2Client.builder(), Ec2Client.SERVICE_NAME); - this.serviceQuotas = Utils.sdkClient(ServiceQuotasClient.builder(), ServiceQuotasClient.SERVICE_NAME); - this.rds = Utils.sdkClient(RdsClient.builder(), RdsClient.SERVICE_NAME); - this.cloudWatch = Utils.sdkClient(CloudWatchClient.builder(), CloudWatchClient.SERVICE_NAME); - LOGGER.info("Constructor init: {}", System.currentTimeMillis() - startTimeMillis); - } - - public QuotaCheck checkQuotas() { - String serviceCode; - Map deployedCountMap = new LinkedHashMap<>(); - Map quotasMap = new LinkedHashMap<>(); - StringBuilder builder = new StringBuilder(); - - boolean reportBackError = false; - boolean exceedsLimit = false; - List retList = new ArrayList<>(); - // RDS - serviceCode = "rds"; - deployedCountMap.clear(); - deployedCountMap.put("DB clusters", Double.valueOf(getRdsClusters())); - deployedCountMap.put("DB instances", Double.valueOf(getRdsInstances())); - quotasMap = getQuotas(serviceCode); - exceedsLimit = compareValues(retList, deployedCountMap, serviceCode, quotasMap, builder); - reportBackError = reportBackError || exceedsLimit; - - // load balancers - serviceCode = "elasticloadbalancing"; - deployedCountMap.clear(); - deployedCountMap.put("Application Load Balancers per Region", Double.valueOf(getAlbs())); - quotasMap = getQuotas(serviceCode); - exceedsLimit = compareValues(retList, deployedCountMap, serviceCode, quotasMap, builder); - reportBackError = reportBackError || exceedsLimit; - - // fargate - serviceCode = "fargate"; - deployedCountMap.clear(); - deployedCountMap.put("Fargate On-Demand vCPU resource count", getFargateResourceCount()); - deployedCountMap.put("Fargate Spot vCPU resource count", getFargateSpotResourceCount()); - quotasMap = getQuotas(serviceCode); - // Remove old on demand quota that have been replaced with the new vCPU quota - quotasMap.remove("Fargate On-Demand resource count"); - quotasMap.remove("Fargate Spot resource count"); - exceedsLimit = compareValues(retList, deployedCountMap, serviceCode, quotasMap, builder); - reportBackError = reportBackError || exceedsLimit; - - // vpc - serviceCode = "vpc"; - deployedCountMap.clear(); - deployedCountMap.put("VPCs per Region", Double.valueOf(getVpcs())); - deployedCountMap.put("Internet gateways per Region", Double.valueOf(getInternetGateways())); - deployedCountMap.put("NAT gateways per Availability Zone", Double.valueOf(getNatGateways())); - quotasMap = getQuotas(serviceCode); - exceedsLimit = compareValues(retList, deployedCountMap, serviceCode, quotasMap, builder); - reportBackError = reportBackError || exceedsLimit; - - // ec2 vCPU - serviceCode = "ec2"; - deployedCountMap.clear(); - deployedCountMap.put("Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances", getVCpuCount()); - quotasMap = getQuotas(serviceCode); - exceedsLimit = compareValues(retList, deployedCountMap, serviceCode, quotasMap, builder); - reportBackError = reportBackError || exceedsLimit; - - QuotaCheck quotaCheck = new QuotaCheck(); - quotaCheck.setPassed(!reportBackError); - quotaCheck.setServiceList(retList); - quotaCheck.setMessage(builder.toString()); - return quotaCheck; - } - - private static boolean compareValues(List retList, Map deployedCountMap, String serviceCode, Map quotasMap, StringBuilder builder) { - //now compare and build list of messages - boolean exceedsLimit = false; - for (Map.Entry entry : deployedCountMap.entrySet()) { - Service service = new Service(serviceCode, entry.getKey(), entry.getValue()); - if (quotasMap.containsKey(entry.getKey())) { - Double quotaValue = quotasMap.get(entry.getKey()); - LOGGER.info("Entry key : {}, Entry value: {}, quotaValue: {}", entry.getKey(), entry.getValue(), quotaValue); - service.setQuotaValue(quotaValue); - if (null != quotaValue) { - if (quotaValue.compareTo(entry.getValue()) < 1) { - builder.append("Quota will be exceeded for service "); - builder.append(entry.getKey()); - builder.append(". You are currently consuming "); - builder.append(entry.getValue()); - builder.append(", and Service Quota is "); - builder.append(quotaValue); - builder.append("."); - exceedsLimit = true; - } else { - service.setPassed(true); - } - } else { - throw new RuntimeException("Unexpected null value for Quota: " + entry.getKey()); - } - } else { - LOGGER.info("No Quota found for key: {}", entry.getKey()); - } - retList.add(service); - } - return exceedsLimit; - } - - private int getRdsClusters() { - int clusters = 0; - try { - clusters = rds.describeDBClusters().dbClusters().size(); - } catch (SdkServiceException rdsError) { - LOGGER.error("rds::DescribeClusters", rdsError); - LOGGER.error(Utils.getFullStackTrace(rdsError)); - throw rdsError; - } - return clusters; - } - - private int getRdsInstances() { - int instances = 0; - try { - instances = rds.describeDBInstances().dbInstances().size(); - } catch (SdkServiceException rdsError) { - LOGGER.error("rds::DescribeDBInstances", rdsError); - LOGGER.error(Utils.getFullStackTrace(rdsError)); - throw rdsError; - } - return instances; - } - - private int getAlbs() { - int loadBalancers = 0; - try { - loadBalancers = elb.describeLoadBalancers().loadBalancers().size(); - } catch (SdkServiceException elbError) { - LOGGER.error("elasticloadbalancing::DescribeLoadBalancers", elbError); - LOGGER.error(Utils.getFullStackTrace(elbError)); - throw elbError; - } - return loadBalancers; - } - - private int getVpcs() { - int vpcs = 0; - try { - vpcs = ec2.describeVpcs().vpcs().size(); - } catch (SdkServiceException ec2Error) { - LOGGER.error("ec2::DescribeVpcs", ec2Error); - LOGGER.error(Utils.getFullStackTrace(ec2Error)); - throw ec2Error; - } - return vpcs; - } - - private int getInternetGateways() { - int gateways = 0; - try { - gateways = ec2.describeInternetGateways().internetGateways().size(); - } catch (SdkServiceException ec2Error) { - LOGGER.error("ec2::DescribeInternetGateways", ec2Error); - LOGGER.error(Utils.getFullStackTrace(ec2Error)); - throw ec2Error; - } - return gateways; - } - - private int getNatGateways() { - int natGateways = 0; - try { - DescribeNatGatewaysResponse response = ec2.describeNatGateways(); - if (response.hasNatGateways()) { - for (NatGateway natGateway : response.natGateways()) { - if (NatGatewayState.AVAILABLE == natGateway.state() - || NatGatewayState.PENDING == natGateway.state()) { - natGateways++; - } - } - } - } catch (SdkServiceException ec2Error) { - LOGGER.error("ec2::DescribeNatGateways", ec2Error); - LOGGER.error(Utils.getFullStackTrace(ec2Error)); - throw ec2Error; - } - return natGateways; - } - - private Double getFargateResourceCount() { - final long startTime = System.currentTimeMillis(); - Double count = 0d; - try { - Metric metric = Metric.builder() - .metricName("ResourceCount") - .namespace("AWS/Usage") - .dimensions(Arrays.asList( - Dimension.builder().name("Type").value("Resource").build(), - Dimension.builder().name("Resource").value("vCPU").build(), - Dimension.builder().name("Service").value("Fargate").build(), - Dimension.builder().name("Class").value("Standard/OnDemand").build() - )) - .build(); - - MetricStat metricStat = MetricStat.builder() - .stat("Maximum") - .period(600) - .metric(metric) - .build(); - - MetricDataQuery dataQuery = MetricDataQuery.builder() - .metricStat(metricStat) - .id("fargate") - .returnData(true) - .build(); - - Instant end = Instant.now(); - Instant start = end.minus(600, ChronoUnit.SECONDS); - - GetMetricDataRequest getMetricDataRequest = GetMetricDataRequest.builder() - .maxDatapoints(10000) - .startTime(start) - .endTime(end) - .metricDataQueries(Arrays.asList(dataQuery)) - .build(); - - GetMetricDataResponse response = cloudWatch.getMetricData(getMetricDataRequest); - for (MetricDataResult item : response.metricDataResults()) { - //get the last value as it is the most current - if (!item.values().isEmpty()) { - count = item.values().get(item.values().size() - 1); - break; - } - } - LOGGER.info("Time to process: " + (System.currentTimeMillis() - startTime)); - } catch (CloudWatchException cloudWatchError) { - LOGGER.error("cloudwatch::GetMetricData", cloudWatchError); - LOGGER.error(Utils.getFullStackTrace(cloudWatchError)); - throw cloudWatchError; - } - return count; - } - private Double getFargateSpotResourceCount() { - final long startTime = System.currentTimeMillis(); - Double count = 0d; - try { - Metric metric = Metric.builder() - .metricName("ResourceCount") - .namespace("AWS/Usage") - .dimensions(Arrays.asList( - Dimension.builder().name("Type").value("Resource").build(), - Dimension.builder().name("Resource").value("vCPU").build(), - Dimension.builder().name("Service").value("Fargate").build(), - Dimension.builder().name("Class").value("Standard/Spot").build() - )) - .build(); - - MetricStat metricStat = MetricStat.builder() - .stat("Maximum") - .period(600) - .metric(metric) - .build(); - - MetricDataQuery dataQuery = MetricDataQuery.builder() - .metricStat(metricStat) - .id("fargate") - .returnData(true) - .build(); - - Instant end = Instant.now(); - Instant start = end.minus(600, ChronoUnit.SECONDS); - - GetMetricDataRequest getMetricDataRequest = GetMetricDataRequest.builder() - .maxDatapoints(10000) - .startTime(start) - .endTime(end) - .metricDataQueries(Arrays.asList(dataQuery)) - .build(); - - GetMetricDataResponse response = cloudWatch.getMetricData(getMetricDataRequest); - for (MetricDataResult item : response.metricDataResults()) { - //get the last value as it is the most current - if (!item.values().isEmpty()) { - count = item.values().get(item.values().size() - 1); - break; - } - } - LOGGER.info("Time to process: " + (System.currentTimeMillis() - startTime)); - } catch (CloudWatchException cloudWatchError) { - LOGGER.error("cloudwatch::GetMetricData", cloudWatchError); - LOGGER.error(Utils.getFullStackTrace(cloudWatchError)); - throw cloudWatchError; - } - return count; - } - private Double getVCpuCount() { - final long startTime = System.currentTimeMillis(); - Double count = 0d; - try { - Metric metric = Metric.builder() - .metricName("ResourceCount") - .namespace("AWS/Usage") - .dimensions(Arrays.asList( - Dimension.builder().name("Type").value("Resource").build(), - Dimension.builder().name("Resource").value("vCPU").build(), - Dimension.builder().name("Service").value("EC2").build(), - Dimension.builder().name("Class").value("Standard/OnDemand").build() - )) - .build(); - - MetricStat metricStat = MetricStat.builder() - .stat("Maximum") - .period(600) - .metric(metric) - .build(); - - MetricDataQuery dataQuery = MetricDataQuery.builder() - .metricStat(metricStat) - .id("vcpu") - .returnData(true) - .build(); - - Instant end = Instant.now(); - Instant start = end.minus(600, ChronoUnit.SECONDS); - - GetMetricDataRequest getMetricDataRequest = GetMetricDataRequest.builder() - .maxDatapoints(10000) - .startTime(start) - .endTime(end) - .metricDataQueries(Arrays.asList(dataQuery)) - .build(); - - GetMetricDataResponse response = cloudWatch.getMetricData(getMetricDataRequest); - for (MetricDataResult item : response.metricDataResults()) { - //get the last value as it is the most current - if (!item.values().isEmpty()) { - count = item.values().get(item.values().size() - 1); - break; - } - } - LOGGER.info("Time to process: " + (System.currentTimeMillis() - startTime)); - } catch (CloudWatchException cloudWatchError) { - LOGGER.error("cloudwatch::GetMetricData", cloudWatchError); - LOGGER.error(Utils.getFullStackTrace(cloudWatchError)); - throw cloudWatchError; - } - return count; - } - - // Get the Quota - private Map getQuotas(String serviceCode) { - // Possible language parameters: "en" (English), "ja" (Japanese), "fr" (French), "zh" (Chinese) - Map retVals = new LinkedHashMap<>(); - String nextToken = null; - LOGGER.info("Service: {}", serviceCode); - try { - do { - ListServiceQuotasRequest request = ListServiceQuotasRequest.builder() - .serviceCode(serviceCode) - .nextToken(nextToken) - .build(); - ListServiceQuotasResponse response = serviceQuotas.listServiceQuotas(request); - nextToken = response.nextToken(); - - for (ServiceQuota quota : response.quotas()) { - // Do something with check description. - //LOGGER.info("Service: " + quota.serviceName() + " Quota: " + quota.quotaName() + " Value: " + quota.value()); - retVals.put(quota.quotaName(), quota.value()); - if (null == quota.value()) { - LOGGER.debug(quota.toString()); //this is for permissions error troubleshooting - } - } - } while (nextToken != null && !nextToken.isEmpty()); - return retVals; - } catch (Exception e) { - LOGGER.error("Error fetching quota for service {} with message {}", serviceCode, e.getMessage()); - LOGGER.error((Utils.getFullStackTrace(e))); - throw e; - } - } - - // AWS Service Quotas is currently unavailable in the GCR regions, so we use the quota from service itself for check. - public QuotaCheck checkQuotasForCNRegion() { - String serviceCode; - Map deployedCountMap = new LinkedHashMap<>(); - Map quotasMap = new LinkedHashMap<>(); - StringBuilder builder = new StringBuilder(); - - boolean reportBackError = false; - boolean exceedsLimit = false; - List retList = new ArrayList<>(); - // RDS - serviceCode = "rds"; - deployedCountMap.clear(); - deployedCountMap.put("DB clusters", Double.valueOf(getRdsClusters())); - deployedCountMap.put("DB instances", Double.valueOf(getRdsInstances())); - quotasMap = getRdsInstancesQuota(); - exceedsLimit = compareValues(retList, deployedCountMap, serviceCode, quotasMap, builder); - reportBackError = reportBackError || exceedsLimit; - - // load balancers - serviceCode = "elasticloadbalancing"; - deployedCountMap.clear(); - deployedCountMap.put("Application Load Balancers per Region", Double.valueOf(getAlbs())); - quotasMap = getELBQuota(); - exceedsLimit = compareValues(retList, deployedCountMap, serviceCode, quotasMap, builder); - reportBackError = reportBackError || exceedsLimit; - - QuotaCheck quotaCheck = new QuotaCheck(); - quotaCheck.setPassed(!reportBackError); - quotaCheck.setServiceList(retList); - quotaCheck.setMessage(builder.toString()); - return quotaCheck; - } - - private Map getRdsInstancesQuota() { - long instances; - long clusters; - try { - List accountQuotas = rds.describeAccountAttributes().accountQuotas(); - clusters = accountQuotas.stream().filter(quota -> quota.accountQuotaName().equals("DBClusters")) - .findFirst().map(AccountQuota::max).orElse(40L); - instances = accountQuotas.stream().filter(quota -> quota.accountQuotaName().equals("DBInstances")) - .findFirst().map(AccountQuota::max).orElse(40L); - } catch (SdkServiceException rdsError) { - LOGGER.error("rds::describeAccountAttributes", rdsError); - LOGGER.error(Utils.getFullStackTrace(rdsError)); - throw rdsError; - } - Map retVals = new LinkedHashMap<>(); - retVals.put("DB clusters", Double.valueOf(clusters)); - retVals.put("DB instances", Double.valueOf(instances)); - return retVals; - } - - private Map getELBQuota() { - long instances; - try { - instances = elb.describeAccountLimits().limits().stream() - .filter(x -> x.name().equals("application-load-balancers")) - .findFirst().map(Limit::max).map(Long::valueOf).orElse(50L); - } catch (SdkServiceException elbError) { - LOGGER.error("elb::describeAccountLimits", elbError); - LOGGER.error(Utils.getFullStackTrace(elbError)); - throw elbError; - } - Map retVals = new LinkedHashMap<>(); - retVals.put("Application Load Balancers per Region", Double.valueOf(instances)); - return retVals; - } - -// // Get the List of Available Trusted Advisor Checks -// private void getServices() { -// // Possible language parameters: "en" (English), "ja" (Japanese), "fr" (French), "zh" (Chinese) -// -// String nextToken = null; -// -// //build a list of services that we are interested in with a list of the quota names and iterate -// Map> serviceMap = new LinkedHashMap<>(); -// List quotas = new ArrayList<>(); -// -// do { -// ListServicesRequest request = ListServicesRequest.builder().nextToken(nextToken).build(); -// ListServicesResponse response = serviceQuotasClient.listServices(request); -// nextToken = response.nextToken(); -// for (ServiceInfo info : response.services()) { -// System.out.println(info.toString()); -// getQuotas(info.serviceCode()); -// } -// } while (nextToken != null && !nextToken.isEmpty()); -// } - - public static class Service { - private String serviceCode; - private String serviceName; - private Double quotaValue = 0d; - private Double serviceCount; - private boolean passed = false; - - public Service(String serviceCode, String serviceName, Double serviceCount) { - this.serviceCode = serviceCode; - this.serviceName = serviceName; - this.serviceCount = serviceCount; - } - - public void setPassed(boolean ok) { - passed = ok; - } - - public void setQuotaValue(Double quotaValue) { - this.quotaValue = quotaValue; - } - } - - public static class QuotaCheck { - private List serviceList; - private boolean passed = false; - private String message = ""; - - public void setServiceList(List serviceList) { - this.serviceList = serviceList; - } - - public void setPassed(boolean checkPassed) { - this.passed = checkPassed; - } - - public void setMessage(String message) { - this.message = message; - } - } -} diff --git a/services/quotas-service/src/main/resources/lambda-assembly.xml b/services/quotas-service/src/main/resources/lambda-assembly.xml deleted file mode 100644 index 26364854..00000000 --- a/services/quotas-service/src/main/resources/lambda-assembly.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - lambda - - zip - - false - - - - ${project.build.outputDirectory} - - com/amazon/aws/partners/saasfactory/** - log4j2.xml - git.properties - - - - - - false - true - lib - - - \ No newline at end of file diff --git a/services/quotas-service/src/main/resources/log4j2.xml b/services/quotas-service/src/main/resources/log4j2.xml deleted file mode 100644 index 04128110..00000000 --- a/services/quotas-service/src/main/resources/log4j2.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} %X{AWSRequestId} %-5p %C{1} - %m%n - - - - - - - - - - - - diff --git a/services/quotas-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/LimitTest.java b/services/quotas-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/LimitTest.java deleted file mode 100644 index 2865460a..00000000 --- a/services/quotas-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/LimitTest.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; - -import java.net.URISyntaxException; -import java.util.HashMap; -import java.util.Map; - -public class LimitTest { - - public static void main(String args[]) throws URISyntaxException { -/* - LimitServiceDAL dal = new LimitServiceDAL(); - dal.handleRequest(); -*/ - QuotasService service = new QuotasService(); - Map event = new HashMap<>(); - APIGatewayProxyResponseEvent responseBody = service.checkQuotas(event, null); - System.out.println("body: " + responseBody.getBody()); - Map valMap = Utils.fromJson(responseBody.getBody(), HashMap.class); - Boolean passed = (Boolean) valMap.get("passed"); - System.out.println("passed " + passed); - String message = (String) valMap.get("message"); - } -} diff --git a/services/quotas-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/QuotaTest.java b/services/quotas-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/QuotaTest.java deleted file mode 100644 index 7f7e0c86..00000000 --- a/services/quotas-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/QuotaTest.java +++ /dev/null @@ -1,339 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.amazon.aws.partners.saasfactory.saasboost; - -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; -import software.amazon.awssdk.core.SdkSystemSetting; -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; -import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.ec2.Ec2Client; -import software.amazon.awssdk.services.ec2.model.DescribeHostsResponse; -import software.amazon.awssdk.services.ec2.model.DescribeInternetGatewaysResponse; -import software.amazon.awssdk.services.ec2.model.DescribeNatGatewaysResponse; -import software.amazon.awssdk.services.ec2.model.DescribeVpcsResponse; -import software.amazon.awssdk.services.elasticloadbalancingv2.ElasticLoadBalancingV2Client; -import software.amazon.awssdk.services.elasticloadbalancingv2.model.DescribeLoadBalancersResponse; -import software.amazon.awssdk.services.rds.RdsClient; -import software.amazon.awssdk.services.rds.model.DescribeDbClustersResponse; -import software.amazon.awssdk.services.rds.model.DescribeDbInstancesResponse; -import software.amazon.awssdk.services.servicequotas.ServiceQuotasClient; -import software.amazon.awssdk.services.servicequotas.model.*; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -public class QuotaTest { - - private final static Region AWS_REGION = Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable())); - - - public static void main(String args[]) throws URISyntaxException { - - //SupportClient client = createClient(); - // getServices(); - - List retList = new ArrayList<>(); - Map deployedCountMap = new LinkedHashMap<>(); - String serviceCode = "rds"; - deployedCountMap.put("DB clusters", Double.valueOf(getRdsClusters())); - deployedCountMap.put("DB instances", Double.valueOf(getRdsInstances())); - Map quotasMap = getQuotas(serviceCode); - - StringBuilder builder = new StringBuilder(); - compareValues(retList, deployedCountMap, serviceCode, quotasMap, builder); - -//load balancers - serviceCode = "elasticloadbalancing"; - deployedCountMap.clear(); - deployedCountMap.put("Application Load Balancers per Region", Double.valueOf(getAlbs())); - quotasMap = getQuotas(serviceCode); - compareValues(retList, deployedCountMap, serviceCode, quotasMap, builder); - -//fargate - serviceCode = "fargate"; - deployedCountMap.clear(); - deployedCountMap.put("Fargate On-Demand resource count", Double.valueOf(getAlbs())); - quotasMap = getQuotas(serviceCode); - compareValues(retList, deployedCountMap, serviceCode, quotasMap, builder); - - -// System.out.println("Application Load Balancers per Region: " + ); - -//ecs **TODO: does not return anything -/* - serviceCode = "ecs"; - deployedCountMap.clear(); - deployedCountMap.put("Clusters per account", Double.valueOf(getEcsClusters())); - //Limit is 10000 so we don't need - quotasMap = getQuotas(serviceCode); - compareValues(retList, deployedCountMap, serviceCode, quotasMap, builder); -*/ - - -//vpc - serviceCode = "vpc"; - deployedCountMap.clear(); - deployedCountMap.put("VPCs per Region", Double.valueOf(getVpcs())); - deployedCountMap.put("Internet gateways per Region", Double.valueOf(getInternetGateways())); - deployedCountMap.put("NAT gateways per Availability Zone", Double.valueOf(getNatGateways())); - quotasMap = getQuotas(serviceCode); - compareValues(retList, deployedCountMap, serviceCode, quotasMap, builder); - - System.out.println(builder.toString()); - System.out.println("Returned values: " + retList.size()); -/*//Limit is 10000 so we don't need - System.out.println("ECS Clusters: " + getEcsClusters()); - System.out.println("ECS Task Definitions: " + getEcsTaskDefinitions()); - - System.out.println("VPCs per Region: " + getVpcs()); - System.out.println("Internet gateways per Region: " + getInternetGateways()); - System.out.println("NAT Gateways: " + getNatGateways()); - System.out.println("EC2 Hosts: " + getEc2Instances());*/ - } - - private static void compareValues(List retList, Map deployedCountMap, String serviceCode, Map quotasMap, StringBuilder builder) { - //now compare and build list of messages - for (Map.Entry entry : deployedCountMap.entrySet()) { - Service service = new Service(serviceCode, entry.getKey(), 0d, entry.getValue()); - if (quotasMap.containsKey(entry.getKey())) { - Double quotaValue = quotasMap.get(entry.getKey()); - service.setQuotaValue(quotaValue); - if (quotaValue.compareTo(entry.getValue()) < 1) { - builder.append("\nError, number of deployed " + entry.getKey() + " is " + entry.getValue() - + " and Service Quota is " + quotaValue); - } else { - builder.append("\nInfo, number of deployed " + entry.getKey() + " is " + entry.getValue() - + " and Service Quota is " + quotaValue); - } - } else { - System.out.println("No Quota found for key: " + entry.getKey()); - } - retList.add(service); - } - } - - private static ServiceQuotasClient createQuotasClient() { - - Region region = Region.US_EAST_1; - // static ApplicationAutoScalingClient aaClient = (ApplicationAutoScalingClient) ApplicationAutoScalingClient.builder() ; - ServiceQuotasClient client1 = ServiceQuotasClient.builder() - .httpClientBuilder(UrlConnectionHttpClient.builder()) -// .region(region) - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .build(); - return client1; - } - - private static RdsClient createRdsClient() throws URISyntaxException { - - RdsClient rdsClient = RdsClient.builder() - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .region(AWS_REGION) - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .endpointOverride(new URI("https://rds." + AWS_REGION.id() + "." + Utils.endpointSuffix(AWS_REGION.id()))) - .overrideConfiguration(ClientOverrideConfiguration.builder().build()) - .build(); - return rdsClient; - } - - public static int getRdsClusters() throws URISyntaxException { - DescribeDbClustersResponse response = createRdsClient().describeDBClusters(); - return response.dbClusters().size(); - //for ( DBCluster cluster : response.dbClusters()) { - } - - public static int getRdsInstances() throws URISyntaxException { - DescribeDbInstancesResponse response = createRdsClient().describeDBInstances(); - return response.dbInstances().size(); - //for ( DBCluster cluster : response.dbClusters()) { - } - - private static ElasticLoadBalancingV2Client createAlbClient() throws URISyntaxException { - - ElasticLoadBalancingV2Client client = ElasticLoadBalancingV2Client.builder() - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .region(AWS_REGION) - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .endpointOverride(new URI("https://elasticloadbalancing." + AWS_REGION.id() + "." + Utils.endpointSuffix(AWS_REGION.id()))) - .overrideConfiguration(ClientOverrideConfiguration.builder().build()) - .build(); - return client; - } - - public static int getAlbs() throws URISyntaxException { - DescribeLoadBalancersResponse response = createAlbClient().describeLoadBalancers(); - return response.loadBalancers().size(); - //for ( DBCluster cluster : response.dbClusters()) { - } - /* - private static EcsClient createEcsClient() throws URISyntaxException { - - EcsClient client = EcsClient.builder() - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .region(AWS_REGION) - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .endpointOverride(new URI("https://ecs." + AWS_REGION.id() + ".amazonaws.com")) // will break in China regions - .overrideConfiguration(ClientOverrideConfiguration.builder().build()) - .build(); - return client; - } - - public static int getEcsClusters() throws URISyntaxException { - ListClustersResponse response = createEcsClient().listClusters(); - return response.clusterArns().size(); - //for ( DBCluster cluster : response.dbClusters()) { - } - - public static int getEcsTaskDefinitions() throws URISyntaxException { - ListTaskDefinitionsResponse response = createEcsClient().listTaskDefinitions(); - return response.taskDefinitionArns().size(); - //for ( DBCluster cluster : response.dbClusters()) { - } - */ - private static Ec2Client createEc2Client() throws URISyntaxException { - - Ec2Client client = Ec2Client.builder() - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .region(AWS_REGION) - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .endpointOverride(new URI("https://ec2." + AWS_REGION.id() + "." + Utils.endpointSuffix(AWS_REGION.id()))) - .overrideConfiguration(ClientOverrideConfiguration.builder().build()) - .build(); - return client; - } - - public static int getVpcs() throws URISyntaxException { - DescribeVpcsResponse response = createEc2Client().describeVpcs(); - return response.vpcs().size(); - //for ( DBCluster cluster : response.dbClusters()) { - } - - public static int getInternetGateways() throws URISyntaxException { - DescribeInternetGatewaysResponse response = createEc2Client().describeInternetGateways(); - return response.internetGateways().size(); - //for ( DBCluster cluster : response.dbClusters()) { - } - - public static int getNatGateways() throws URISyntaxException { - DescribeNatGatewaysResponse response = createEc2Client().describeNatGateways(); - return response.natGateways().size(); - //for ( DBCluster cluster : response.dbClusters()) { - } - - public static int getEc2Instances() throws URISyntaxException { - DescribeHostsResponse response = createEc2Client().describeHosts(); - return response.hosts().size(); - //for ( DBCluster cluster : response.dbClusters()) { - } - - - // Get the Quota - public static Map getQuotas(String serviceCode) { - // Possible language parameters: "en" (English), "ja" (Japanese), "fr" (French), "zh" (Chinese) - Map retVals = new LinkedHashMap<>(); - String nextToken = null; - do { - ListServiceQuotasRequest request = ListServiceQuotasRequest.builder() - .serviceCode(serviceCode) - .nextToken(nextToken) - .build(); - ListServiceQuotasResponse response = createQuotasClient().listServiceQuotas(request); - nextToken = response.nextToken(); - - for (ServiceQuota quota : response.quotas()) { - // Do something with check description. -// System.out.println("Service: " + quota.serviceName() + " Quota: " + quota.quotaName() + " Value: " + quota.value()); - retVals.put(quota.quotaName(), quota.value()); -// System.out.println(quota.toString()); - } - } while (nextToken != null && !nextToken.isEmpty()); - return retVals; - } - - // Get the List of Available Trusted Advisor Checks - public static void getServices() { - // Possible language parameters: "en" (English), "ja" (Japanese), "fr" (French), "zh" (Chinese) - - String nextToken = null; - - //build a list of services that we are interested in with a list of the quota names and iterate - Map> serviceMap = new LinkedHashMap<>(); - List quotas = new ArrayList<>(); - - do { - ListServicesRequest request = ListServicesRequest.builder().nextToken(nextToken).build(); - ListServicesResponse response = createQuotasClient().listServices(request); - nextToken = response.nextToken(); - for (ServiceInfo info : response.services()) { - System.out.println(info.toString()); - getQuotas(info.serviceCode()); - } - } while (nextToken != null && !nextToken.isEmpty()); - } - - - private static class Service { - private String serviceCode; - private String serviceName; - private Double quotaValue; - private Double serviceCount; - - public Service(String serviceCode, String serviceName, Double quotaValue, Double serviceCount) { - this.serviceCode = serviceCode; - this.serviceName = serviceName; - this.quotaValue = quotaValue; - this.serviceCount = serviceCount; - } - - public String getServiceCode() { - return serviceCode; - } - - public void setServiceCode(String serviceCode) { - this.serviceCode = serviceCode; - } - - public String getServiceName() { - return serviceName; - } - - public void setServiceName(String serviceName) { - this.serviceName = serviceName; - } - - public Double getQuotaValue() { - return quotaValue; - } - - public void setQuotaValue(Double quotaValue) { - this.quotaValue = quotaValue; - } - - public Double getServiceCount() { - return serviceCount; - } - - public void setServiceCount(Double serviceCount) { - this.serviceCount = serviceCount; - } - } - -} diff --git a/services/quotas-service/update.sh b/services/quotas-service/update.sh deleted file mode 100755 index f29c42da..00000000 --- a/services/quotas-service/update.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -if [ -z $1 ]; then - echo "Usage: $0 [Lambda Folder]" - exit 2 -fi - -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -echo "AWS Region = $MY_AWS_REGION" - -ENVIRONMENT=$1 -LAMBDA_STAGE_FOLDER=$2 -if [ -z $LAMBDA_STAGE_FOLDER ]; then - LAMBDA_STAGE_FOLDER="lambdas" -fi -LAMBDA_CODE=QuotasService-lambda.zip - -#set this for V2 AWS CLI to disable paging -export AWS_PAGER="" - -SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) -echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" -if [ -z $SAAS_BOOST_BUCKET ]; then - echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" - exit 1 -fi - -# Do a fresh build of the project -mvn -if [ $? -ne 0 ]; then - echo "Error building project" - exit 1 -fi - -# And copy it up to S3 -aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ - -FUNCTIONS=("sb-${ENVIRONMENT}-quotas-check" -) - -for FX in ${FUNCTIONS[@]}; do - aws lambda --region $MY_AWS_REGION update-function-code --function-name $FX --s3-bucket $SAAS_BOOST_BUCKET --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE -done diff --git a/services/settings-service/pom.xml b/services/settings-service/pom.xml index cf5cf38a..ba0bfd02 100644 --- a/services/settings-service/pom.xml +++ b/services/settings-service/pom.xml @@ -33,7 +33,8 @@ limitations under the License. - 17 + ${project.basedir}/../.. + 0 @@ -56,21 +57,6 @@ limitations under the License. org.jacoco jacoco-maven-plugin - 0.8.7 - - - - prepare-agent - - - - report - prepare-package - - report - - - org.apache.maven.plugins @@ -80,24 +66,26 @@ limitations under the License. io.github.git-commit-id git-commit-id-maven-plugin + + com.github.spotbugs + spotbugs-maven-plugin + + Max + medium + + + software.amazon.lambda.snapstart + aws-lambda-snapstart-java-rules + 0.1.0 + + + ${project.basedir}/src/main/resources/spotbugs-exclude.xml + + - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - - - com.amazon.aws.partners.saasfactory.saasboost - ApiGatewayHelper - 1.0.0 - - provided - software.amazon.awssdk ssm @@ -113,81 +101,6 @@ limitations under the License. - - software.amazon.awssdk - acm - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - software.amazon.awssdk - dynamodb - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - software.amazon.awssdk - eventbridge - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - software.amazon.awssdk - s3 - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - software.amazon.awssdk - route53 - ${aws.java.sdk.version} - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ParameterStoreFacade.java b/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ParameterStoreFacade.java deleted file mode 100644 index e6d12177..00000000 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ParameterStoreFacade.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.core.exception.SdkServiceException; -import software.amazon.awssdk.services.ssm.SsmClient; -import software.amazon.awssdk.services.ssm.model.*; - -import java.util.ArrayList; -import java.util.List; - -public class ParameterStoreFacade { - - private static final Logger LOGGER = LoggerFactory.getLogger(ParameterStoreFacade.class); - - private final SsmClient ssm; - - public ParameterStoreFacade(final SsmClient ssm) { - this.ssm = ssm; - } - - public Parameter getParameter(String parameterName, boolean decrypt) { - Parameter parameter = null; - try { - parameter = ssm.getParameter(request -> request - .name(parameterName) - .withDecryption(decrypt)).parameter(); - } catch (ParameterNotFoundException pnfe) { - LOGGER.warn("Parameter {} does not exist", parameterName); - } catch (SdkServiceException ssmError) { - LOGGER.error("Error fetching parameter", ssmError); - throw ssmError; - } - return parameter; - } - - public List getParameters(List parameterNames) { - List batch = new ArrayList<>(); - List parameters = new ArrayList<>(); - try { - for (String parameterName : parameterNames) { - if (batch.size() < 10) { - batch.add(parameterName); - } else { - // Batch has reached max size of 10, make the request - GetParametersResponse response = ssm.getParameters(request -> request.names(batch)); - parameters.addAll(response.parameters()); - - // Clear the batch so we can fill it up for the next request - batch.clear(); - } - } - if (!batch.isEmpty()) { - // get the last batch - GetParametersResponse response = ssm.getParameters(request -> request.names(batch)); - parameters.addAll(response.parameters()); - } - } catch (SdkServiceException ssmError) { - LOGGER.error("ssm:GetParameters error", ssmError); - LOGGER.error(Utils.getFullStackTrace(ssmError)); - throw ssmError; - } - return parameters; - } - - public List getParametersByPath(String parameterPathPrefix, boolean recursive, boolean decrypt) { - List parameters = new ArrayList<>(); - String nextToken = null; - do { - try { - GetParametersByPathResponse response = ssm.getParametersByPath(GetParametersByPathRequest - .builder() - .path(parameterPathPrefix) - .recursive(recursive) - .withDecryption(decrypt) - .nextToken(nextToken) - .build() - ); - nextToken = response.nextToken(); - parameters.addAll(response.parameters()); - } catch (ParameterNotFoundException notFoundException) { - LOGGER.warn("Can't find parameters for {}", parameterPathPrefix); - } catch (SdkServiceException ssmError) { - LOGGER.error("ssm:GetParametersByPath error ", ssmError); - LOGGER.error(Utils.getFullStackTrace(ssmError)); - throw ssmError; - } - } while (nextToken != null && !nextToken.isEmpty()); - return parameters; - } - - public Parameter putParameter(Parameter parameter) { - Parameter updated; - try { - PutParameterResponse response = ssm.putParameter(request -> request - .type(parameter.type()) - .overwrite(true) - .name(parameter.name()) - .value(parameter.value()) - ); - updated = Parameter.builder() - .name(parameter.name()) - .value(parameter.value()) - .type(parameter.type()) - .version(response.version()) - .build(); - } catch (SdkServiceException ssmError) { - LOGGER.error("ssm:PutParameter error " + ssmError.getMessage()); - throw ssmError; - } - return updated; - } - - public void deleteParameter(Parameter parameter) { - try { - ssm.deleteParameter(request -> request - .name(parameter.name()) - ); - } catch (SdkServiceException ssmError) { - LOGGER.error("ssm:DeleteParameter error " + ssmError.getMessage()); - } - } - - public void deleteParameters(List parametersToDelete) { - List batch = new ArrayList<>(); - try { - for (String parameterName : parametersToDelete) { - if (batch.size() < 10) { - batch.add(parameterName); - } else { - DeleteParametersResponse response = ssm.deleteParameters(req -> req.names(batch)); - if (response.hasInvalidParameters() && !response.invalidParameters().isEmpty()) { - LOGGER.warn("Could not delete invalid parameters " + response.invalidParameters()); - } - batch.clear(); - } - } - - if (!batch.isEmpty()) { - // delete the last batch - DeleteParametersResponse response = ssm.deleteParameters(req -> req.names(batch)); - if (response.hasInvalidParameters() && !response.invalidParameters().isEmpty()) { - LOGGER.warn("Could not delete invalid parameters " + response.invalidParameters()); - } - } - } catch (SdkServiceException ssmError) { - LOGGER.error("ssm:DeleteParameters error", ssmError); - LOGGER.error(Utils.getFullStackTrace(ssmError)); - throw ssmError; - } - } -} diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Setting.java b/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Setting.java index 8bdd31e5..e9a5545f 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Setting.java +++ b/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Setting.java @@ -97,25 +97,24 @@ public boolean equals(Object obj) { return false; } final Setting other = (Setting) obj; - return (((name == null && other.name == null) || (name != null && name.equals(other.name))) // Parameter Store is case sensitive - && ((value == null && other.value == null) || (value != null && value.equals(other.value))) - && ((description == null && other.description == null) - || (description != null && description.equalsIgnoreCase(other.description))) - && ((version == null && other.version == null) || (version != null && version.equals(other.version))) + return (Objects.equals(name, other.name) // Parameter Store is case sensitive + && Objects.equals(value, other.value) + && Objects.equals(description, other.description) + && Objects.equals(version, other.version) && (readOnly == other.readOnly) && (secure == other.secure)); } @Override public int hashCode() { - return Objects.hash(name, value, (description != null ? description.toUpperCase() : null), version, readOnly, secure); + return Objects.hash(name, value, description, version, readOnly, secure); } @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property] public static final class Builder { private String name; private String value; - private boolean readOnly = true; + private boolean readOnly = false; private boolean secure = false; private Long version; private String description; @@ -125,7 +124,8 @@ private Builder() { public Builder name(String name) { if (!isValidSettingName(name)) { - throw new IllegalArgumentException("Only a mix of letters, numbers and the following 4 symbols .-_/ are allowed."); + throw new IllegalArgumentException("Only a mix of letters, numbers" + + " and the following 4 symbols .-_/ are allowed."); } this.name = name; return this; diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SettingsDataAccessLayer.java b/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SettingsDataAccessLayer.java new file mode 100644 index 00000000..8aab706f --- /dev/null +++ b/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SettingsDataAccessLayer.java @@ -0,0 +1,251 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.exception.SdkServiceException; +import software.amazon.awssdk.services.ssm.SsmClient; +import software.amazon.awssdk.services.ssm.model.*; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class SettingsDataAccessLayer { + + private static final Logger LOGGER = LoggerFactory.getLogger(SettingsDataAccessLayer.class); + private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); + private static final String AWS_REGION = System.getenv("AWS_REGION"); + + // Package private for testing + static final String PARAMETER_STORE_PREFIX = "/saas-boost/" + SAAS_BOOST_ENV + "/"; + // e.g. /saas-boost/production/SAAS_BOOST_BUCKET + static final Pattern SAAS_BOOST_PARAMETER_PATTERN = Pattern.compile("^" + PARAMETER_STORE_PREFIX + "(.+)$"); + + private final SsmClient ssm; + + public SettingsDataAccessLayer(SsmClient ssm) { + if (Utils.isBlank(AWS_REGION)) { + throw new IllegalStateException("Missing environment variable AWS_REGION"); + } + if (Utils.isBlank(SAAS_BOOST_ENV)) { + throw new IllegalStateException("Missing environment variable SAAS_BOOST_ENV"); + } + this.ssm = ssm; + // Warm up SSM for cold start hack + ssm.getParametersByPath(request -> request.path(PARAMETER_STORE_PREFIX + "JUNK")); + } + + public List getAllSettings() { + Map parameterStore = new TreeMap<>(); + String nextToken = null; + do { + try { + GetParametersByPathResponse response = ssm.getParametersByPath(GetParametersByPathRequest + .builder() + .path(PARAMETER_STORE_PREFIX) + .recursive(false) + .withDecryption(false) // don't expose secrets by default + .nextToken(nextToken) + .build() + ); + nextToken = response.nextToken(); + + for (Parameter parameter : response.parameters()) { + LOGGER.info("SettingsServiceDAL::getAllSettings loading Parameter Store param " + parameter.name()); + Setting setting = fromParameterStore(parameter); + parameterStore.put(setting.getName(), setting); + } + } catch (SdkServiceException ssmError) { + LOGGER.error("ssm:GetParametersByPath error " + ssmError.getMessage()); + throw ssmError; + } + } while (nextToken != null && !nextToken.isEmpty()); + + return List.copyOf(parameterStore.values()); + } + + public List getNamedSettings(List namedSettings) { + List parameterNames = namedSettings + .stream() + .map(settingName -> toParameterStore(Setting.builder().name(settingName).build()).name()) + .collect(Collectors.toList()); + List batch = new ArrayList<>(); + List parameters = new ArrayList<>(); + try { + for (String parameterName : parameterNames) { + if (batch.size() < 10) { + batch.add(parameterName); + } else { + // Batch has reached max size of 10, make the request + GetParametersResponse response = ssm.getParameters(request -> request.names(batch)); + parameters.addAll(response.parameters()); + + // Clear the batch so we can fill it up for the next request + batch.clear(); + } + } + if (!batch.isEmpty()) { + // get the last batch + GetParametersResponse response = ssm.getParameters(request -> request.names(batch)); + parameters.addAll(response.parameters()); + } + } catch (SdkServiceException ssmError) { + LOGGER.error("ssm:GetParameters error", ssmError); + LOGGER.error(Utils.getFullStackTrace(ssmError)); + throw ssmError; + } + return parameters.stream() + .map(SettingsDataAccessLayer::fromParameterStore) + .collect(Collectors.toList()); + } + + public Setting getSetting(String settingName) { + return getSetting(settingName, false); + } + + public Setting getSetting(String settingName, boolean decrypt) { + Setting setting = null; + try { + Parameter parameter = toParameterStore(Setting.builder().name(settingName).build()); + GetParameterResponse response = ssm.getParameter(request -> request + .name(parameter.name()) + .withDecryption(decrypt) + ); + setting = fromParameterStore(response.parameter()); + } catch (ParameterNotFoundException pnf) { + LOGGER.warn("Parameter {} does not exist", settingName); + } catch (SdkServiceException ssmError) { + LOGGER.error("Error fetching parameter", ssmError); + throw ssmError; + } + return setting; + } + + public Setting getSecret(String settingName) { + return getSetting(settingName, true); + } + + public String getParameterStoreReference(String settingName) { + Setting setting = getSetting(settingName); + return PARAMETER_STORE_PREFIX + setting.getName() + ":" + setting.getVersion(); + } + + public Setting updateSetting(Setting setting) { + LOGGER.info("Updating setting {} to {}", setting.getName(), setting.getValue()); + if (setting.isSecure()) { + // If we were passed the encrypted string for a secret (from the UI), + // don't overwrite the secret with that gibberish... + Setting existing = getSetting(setting.getName()); + if (existing != null && existing.getValue().equals(setting.getValue())) { + // Nothing has changed, don't overwrite the value in Parameter Store + LOGGER.info("Skipping update of secret because encrypted values are the same"); + return setting; + } + } + LOGGER.info("Calling put parameter {}", setting.getName()); + Setting updated = fromParameterStore(putParameter(toParameterStore(setting))); + if (updated.isSecure()) { + // we don't want to return the unencrypted value, so replace this + // setting with the encrypted representation we just placed in ParameterStore + updated = getSetting(updated.getName()); + } + return updated; + } + + public void deleteSetting(Setting setting) { + deleteParameter(toParameterStore(setting)); + } + + private Parameter putParameter(Parameter parameter) { + Parameter updated = null; + try { + PutParameterResponse response = ssm.putParameter(request -> request + .type(parameter.type()) + .overwrite(true) + .name(parameter.name()) + .value(parameter.value()) + ); + updated = Parameter.builder() + .name(parameter.name()) + .value(parameter.value()) + .type(parameter.type()) + .version(response.version()) + .build(); + } catch (SdkServiceException ssmError) { + LOGGER.error("ssm:PutParameter error {}", ssmError); + throw ssmError; + } + return updated; + } + + private void deleteParameter(Parameter parameter) { + try { + ssm.deleteParameter(request -> request + .name(parameter.name()) + ); + } catch (SdkServiceException ssmError) { + LOGGER.error("ssm:DeleteParameter error {}", ssmError); + throw ssmError; + } + return; + } + + protected static Setting fromParameterStore(Parameter parameter) { + Setting setting = null; + if (parameter != null) { + String parameterStoreName = parameter.name(); + if (Utils.isEmpty(parameterStoreName)) { + throw new RuntimeException("Parameter name can't be empty"); + } + String settingName = null; + Matcher regex = SAAS_BOOST_PARAMETER_PATTERN.matcher(parameterStoreName); + if (regex.matches()) { + settingName = regex.group(1); + } + if (settingName == null) { + throw new RuntimeException("Parameter " + parameter.name() + " does not match SaaS Boost pattern"); + } + + setting = Setting.builder() + .name(settingName) + .value(!"N/A".equals(parameter.value()) ? parameter.value() : "") + .readOnly(false) + .secure(ParameterType.SECURE_STRING == parameter.type()) + .version(parameter.version()) + .build(); + } + return setting; + } + + protected static Parameter toParameterStore(Setting setting) { + if (setting == null || !Setting.isValidSettingName(setting.getName())) { + throw new RuntimeException("Can't create Parameter Store parameter with invalid Setting name"); + } + String parameterName = PARAMETER_STORE_PREFIX + setting.getName(); + String parameterValue = (Utils.isEmpty(setting.getValue())) ? "N/A" : setting.getValue(); + Parameter parameter = Parameter.builder() + .type(setting.isSecure() ? ParameterType.SECURE_STRING : ParameterType.STRING) + .name(parameterName) + .value(parameterValue) + .build(); + return parameter; + } + +} diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SettingsService.java b/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SettingsService.java index ce720633..f02fcdb7 100644 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SettingsService.java +++ b/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SettingsService.java @@ -16,718 +16,192 @@ package com.amazon.aws.partners.saasfactory.saasboost; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.*; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.compute.AbstractCompute; import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.eventbridge.EventBridgeClient; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.*; -import software.amazon.awssdk.services.s3.presigner.S3Presigner; -import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest; +import software.amazon.awssdk.services.ssm.SsmClient; -import java.net.URI; -import java.net.URISyntaxException; -import java.time.Duration; +import java.net.HttpURLConnection; import java.util.*; import java.util.stream.Collectors; -public class SettingsService implements RequestHandler, APIGatewayProxyResponseEvent> { +public class SettingsService { private static final Logger LOGGER = LoggerFactory.getLogger(SettingsService.class); private static final String AWS_REGION = System.getenv("AWS_REGION"); - private static final String SAAS_BOOST_EVENT_BUS = System.getenv("SAAS_BOOST_EVENT_BUS"); - private static final String RESOURCES_BUCKET = System.getenv("RESOURCES_BUCKET"); - private static final String API_GATEWAY_HOST = System.getenv("API_GATEWAY_HOST"); - private static final String API_GATEWAY_STAGE = System.getenv("API_GATEWAY_STAGE"); - private static final String API_TRUST_ROLE = System.getenv("API_TRUST_ROLE"); private static final Map CORS = Map.of("Access-Control-Allow-Origin", "*"); - - static final List REQUIRED_PARAMS = Collections.unmodifiableList( - Arrays.asList("SAAS_BOOST_BUCKET", "CODE_PIPELINE_BUCKET", "CODE_PIPELINE_ROLE", "ECR_REPO", "ONBOARDING_WORKFLOW", - "ONBOARDING_SNS", "ONBOARDING_TEMPLATE", "TRANSIT_GATEWAY", "TRANSIT_GATEWAY_ROUTE_TABLE", "EGRESS_ROUTE_TABLE", - "SAAS_BOOST_ENVIRONMENT", "SAAS_BOOST_STACK", "SAAS_BOOST_LAMBDAS_FOLDER") - ); - static final List READ_WRITE_PARAMS = Collections.unmodifiableList( - Arrays.asList("DOMAIN_NAME", "HOSTED_ZONE", "SSL_CERT_ARN", "APP_NAME", "METRICS_STREAM", "BILLING_API_KEY", - "SERVICE_NAME", "IS_PUBLIC", "PATH", "COMPUTE_SIZE", "TASK_CPU", "TASK_MEMORY", "CONTAINER_PORT", "HEALTH_CHECK", - "FILE_SYSTEM_MOUNT_POINT", "FILE_SYSTEM_ENCRYPT", "FILE_SYSTEM_LIFECYCLE", "MIN_COUNT", "MAX_COUNT", "DB_ENGINE", - "DB_VERSION", "DB_PARAM_FAMILY", "DB_INSTANCE_TYPE", "DB_NAME", "DB_HOST", "DB_PORT", "DB_MASTER_USERNAME", - "DB_PASSWORD", "DB_BOOTSTRAP_FILE", "CLUSTER_OS", "CLUSTER_INSTANCE_TYPE", - //Added for FSX - "FILE_SYSTEM_TYPE", // EFS or FSX - "FSX_STORAGE_GB", // GB 32 to 65,536 - "FSX_THROUGHPUT_MBS", // MB/s - "FSX_BACKUP_RETENTION_DAYS", // 7 to 35 - "FSX_DAILY_BACKUP_TIME", //HH:MM in UTC - "FSX_WEEKLY_MAINTENANCE_TIME",//d:HH:MM in UTC - "FSX_WINDOWS_MOUNT_DRIVE") - ); - - private final SettingsServiceDAL dal; - private final EventBridgeClient eventBridge; - private final S3Client s3; - private final S3Presigner presigner; + private final SettingsDataAccessLayer dal; public SettingsService() { - final long startTimeMillis = System.currentTimeMillis(); + this(new DefaultDependencyFactory()); + } + + // Facilitates testing by being able to mock out AWS SDK dependencies + public SettingsService(SettingsServiceDependencyFactory init) { if (Utils.isBlank(AWS_REGION)) { throw new IllegalStateException("Missing environment variable AWS_REGION"); } LOGGER.info("Version Info: {}", Utils.version(this.getClass())); - this.dal = new SettingsServiceDAL(); - - this.eventBridge = Utils.sdkClient(EventBridgeClient.builder(), EventBridgeClient.SERVICE_NAME); - this.s3 = Utils.sdkClient(S3Client.builder(), S3Client.SERVICE_NAME); - try { - String presignerEndpoint = "https://" + s3.serviceName() + "." - + Region.of(AWS_REGION) - + "." - + Utils.endpointSuffix(AWS_REGION); - this.presigner = S3Presigner.builder() - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .region(Region.of(AWS_REGION)) - .endpointOverride(new URI(presignerEndpoint)) - .build(); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - LOGGER.info("Constructor init: {}", System.currentTimeMillis() - startTimeMillis); - } - - @Override - public APIGatewayProxyResponseEvent handleRequest(Map event, Context context) { - //Utils.logRequestEvent(event); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + this.dal = init.dal(); } - public APIGatewayProxyResponseEvent getSettings(Map event, Context context) { + public APIGatewayProxyResponseEvent getSettings(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } - final long startTimeMillis = System.currentTimeMillis(); //Utils.logRequestEvent(event); - List settings = new ArrayList<>(); + List settings; + // Normal query string params are key/value pairs ?key1=val1&key2=val2 - Map queryParams = (Map) event.get("queryStringParameters"); // Multi-value params are a list of the same key with diff values ?key=val1&key=val2&key=val3 - Map> multiValueQueryParams = (Map>) event.get("multiValueQueryStringParameters"); - // Only return one set of params - LOGGER.info("getSettings queryParams: " + queryParams); - LOGGER.info("getSettings multiValueQueryParams: " + multiValueQueryParams); - if (queryParams != null && queryParams.containsKey("readOnly")) { - LOGGER.error("queryParams included readOnly, but we're ignoring readOnly!"); - //TODO why has this changed? -// if (Boolean.parseBoolean(queryParams.get("readOnly"))) { -// settings = dal.getImmutableSettings(); -// } else { -// settings = dal.getMutableSettings(); -// } - } - // Or, filter to return just a few params (ideally, less than 10) + Map> multiValueQueryParams = event.getMultiValueQueryStringParameters(); + + // Filter to return just a few params (ideally, less than 10) if (multiValueQueryParams != null && multiValueQueryParams.containsKey("setting")) { List namedSettings = multiValueQueryParams.get("setting"); settings = dal.getNamedSettings(namedSettings); - } - // Otherwise, return all params - if (settings.isEmpty()) { + if (settings.isEmpty()) { + return new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_NOT_FOUND); + } + } else { + // Otherwise, return all params settings = dal.getAllSettings(); } - - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("SettingsService::getSettings exec " + totalTimeMillis); return new APIGatewayProxyResponseEvent() .withHeaders(CORS) - .withStatusCode(200) + .withStatusCode(HttpURLConnection.HTTP_OK) .withBody(Utils.toJson(settings)); } - public APIGatewayProxyResponseEvent getSetting(Map event, Context context) { + public APIGatewayProxyResponseEvent getSetting(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } - final long startTimeMillis = System.currentTimeMillis(); - //Utils.logRequestEvent(event); - APIGatewayProxyResponseEvent response = null; - Map params = (Map) event.get("pathParameters"); + Utils.logRequestEvent(event); + APIGatewayProxyResponseEvent response; + Map params = event.getPathParameters(); String settingName = params.get("id"); - LOGGER.info("SettingsService::getSetting " + settingName); Setting setting = dal.getSetting(settingName); if (setting != null) { response = new APIGatewayProxyResponseEvent() - .withStatusCode(200) + .withStatusCode(HttpURLConnection.HTTP_OK) .withHeaders(CORS) .withBody(Utils.toJson(setting)); } else { - response = new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(404); + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_NOT_FOUND); } - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("SettingsService::getSetting exec " + totalTimeMillis); return response; } - public APIGatewayProxyResponseEvent getSecret(Map event, Context context) { + public APIGatewayProxyResponseEvent getSecret(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } - final long startTimeMillis = System.currentTimeMillis(); //Utils.logRequestEvent(event); - APIGatewayProxyResponseEvent response = null; - Map params = (Map) event.get("pathParameters"); + APIGatewayProxyResponseEvent response; + Map params = event.getPathParameters(); String settingName = params.get("id"); - LOGGER.info("SettingsService::getSecret " + settingName); Setting setting = dal.getSecret(settingName); if (setting != null) { response = new APIGatewayProxyResponseEvent() - .withStatusCode(200) + .withStatusCode(HttpURLConnection.HTTP_OK) .withHeaders(CORS) .withBody(Utils.toJson(setting)); } else { - response = new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(404); + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_NOT_FOUND); } - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("SettingsService::getSecret exec " + totalTimeMillis); return response; } - public APIGatewayProxyResponseEvent getParameterStoreReference(Map event, Context context) { + public APIGatewayProxyResponseEvent getParameterStoreReference(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } - final long startTimeMillis = System.currentTimeMillis(); //Utils.logRequestEvent(event); - APIGatewayProxyResponseEvent response = null; - Map params = (Map) event.get("pathParameters"); + APIGatewayProxyResponseEvent response; + Map params = event.getPathParameters(); String settingName = params.get("id"); - LOGGER.info("SettingsService::getParameterStoreReference " + settingName); String parameterStoreRef = dal.getParameterStoreReference(settingName); if (parameterStoreRef != null) { - Map body = new HashMap<>(); - body.put("reference-key", parameterStoreRef); response = new APIGatewayProxyResponseEvent() - .withStatusCode(200) + .withStatusCode(HttpURLConnection.HTTP_OK) .withHeaders(CORS) - .withBody(Utils.toJson(body)); + .withBody(Utils.toJson(Map.of("reference-key", parameterStoreRef))); } else { - response = new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(404); + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_NOT_FOUND); } - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("SettingsService::getParameterStoreReference exec " + totalTimeMillis); return response; } - public APIGatewayProxyResponseEvent updateSetting(Map event, Context context) { + public APIGatewayProxyResponseEvent updateSetting(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } - final long startTimeMillis = System.currentTimeMillis(); - LOGGER.info("SettingsService::updateSetting"); - //Utils.logRequestEvent(event); - APIGatewayProxyResponseEvent response = null; - Map params = (Map) event.get("pathParameters"); + Utils.logRequestEvent(event); + APIGatewayProxyResponseEvent response; + Map params = event.getPathParameters(); String key = params.get("id"); - LOGGER.info("SettingsService::updateSetting " + key); try { - Setting setting = Utils.fromJson((String) event.get("body"), Setting.class); + Setting setting = Utils.fromJson(event.getBody(), Setting.class); if (setting == null) { response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) .withHeaders(CORS) - .withStatusCode(400) - .withBody("{\"message\":\"Empty request body.\"}"); + .withBody(Utils.toJson(Map.of("message", "Invalid request body"))); } else { if (setting.getName() == null || !setting.getName().equals(key)) { - LOGGER.error("SettingsService::updateSetting Can't update setting " - + setting.getName() + " at resource " + key); + LOGGER.error("Can't update setting {} at resource {}", setting.getName(), key); response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) .withHeaders(CORS) - .withStatusCode(400) - .withBody("{\"message\":\"Invalid resource for setting.\"}"); - } else if (!SettingsService.READ_WRITE_PARAMS.contains(key)) { - LOGGER.error("SettingsService::updateSetting Setting " + key + " cannot be modified"); - response = new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(400) - .withBody("{\"message\":\"Can't modify immutable setting " + key + ".\"}"); + .withBody(Utils.toJson(Map.of("message", "Request body must include name"))); } else { setting = dal.updateSetting(setting); response = new APIGatewayProxyResponseEvent() - .withStatusCode(200) + .withStatusCode(HttpURLConnection.HTTP_OK) .withHeaders(CORS) .withBody(Utils.toJson(setting)); } } } catch (Exception e) { - LOGGER.error("Unable to update"); - response = new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(400) - .withBody("{\"message\":\"Invalid JSON\"}"); - } - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("SettingsService::updateSetting exec " + totalTimeMillis); - return response; - } - - public APIGatewayProxyResponseEvent options(Map event, Context context) { - if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); - } - - final long startTimeMillis = System.currentTimeMillis(); - LOGGER.info("SettingsService::configOptions"); - //Utils.logRequestEvent(event); - - Map options = new HashMap<>(); - options.put("osOptions", Arrays.stream(OperatingSystem.values()) - .collect( - Collectors.toMap(OperatingSystem::name, OperatingSystem::getDescription) - )); - options.put("dbOptions", dal.rdsOptions()); - options.put("acmOptions", dal.acmCertificateOptions()); - options.put("hostedZoneOptions", dal.hostedZoneOptions()); - - APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() - .withStatusCode(200) - .withHeaders(CORS) - .withBody(Utils.toJson(options)); - - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("SettingsService::configOptions exec " + totalTimeMillis); - return response; - } - - public APIGatewayProxyResponseEvent getAppConfig(Map event, Context context) { - if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); - } - - final long startTimeMillis = System.currentTimeMillis(); - LOGGER.info("SettingsService::getAppConfig"); - //Utils.logRequestEvent(event); - APIGatewayProxyResponseEvent response = null; - - AppConfig appConfig = dal.getAppConfig(); - response = new APIGatewayProxyResponseEvent() - .withStatusCode(200) - .withHeaders(CORS) - .withBody(Utils.toJson(appConfig)); - - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("SettingsService::getAppConfig exec " + totalTimeMillis); - return response; - } - - public APIGatewayProxyResponseEvent updateAppConfig(Map event, Context context) { - if (Utils.isBlank(SAAS_BOOST_EVENT_BUS)) { - throw new IllegalStateException("Missing environment variable SAAS_BOOST_EVENT_BUS"); - } - if (Utils.isBlank(RESOURCES_BUCKET)) { - throw new IllegalStateException("Missing environment variable RESOURCES_BUCKET"); - } - if (Utils.isBlank(API_GATEWAY_HOST)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_HOST"); - } - if (Utils.isBlank(API_GATEWAY_STAGE)) { - throw new IllegalStateException("Missing required environment variable API_GATEWAY_STAGE"); - } - if (Utils.isBlank(API_TRUST_ROLE)) { - throw new IllegalStateException("Missing required environment variable API_TRUST_ROLE"); - } - if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); - } - - final long startTimeMillis = System.currentTimeMillis(); - LOGGER.info("SettingsService::updateAppConfig"); - Utils.logRequestEvent(event); - APIGatewayProxyResponseEvent response; - - AppConfig updatedAppConfig = Utils.fromJson((String) event.get("body"), AppConfig.class); - if (updatedAppConfig == null) { - response = new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(400) - .withBody("{\"message\":\"Invalid request body.\"}"); - } else if (updatedAppConfig.getName() == null || updatedAppConfig.getName().isEmpty()) { - LOGGER.error("Can't update application configuration without an app name"); - response = new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(400) - .withBody("{\"message\":\"Application name is required.\"}"); - } else { - AppConfig currentAppConfig = dal.getAppConfig(); - if (currentAppConfig.isEmpty()) { - LOGGER.info("Processing first time app config save"); - // First time setting the app config object don't bother going through all of the validation - updatedAppConfig = dal.setAppConfig(updatedAppConfig); - - // If the app config has any databases, get the presigned S3 urls to upload bootstrap files - generateDatabaseBootstrapFileUrl(updatedAppConfig); - - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, "saas-boost", - AppConfigEvent.APP_CONFIG_CHANGED.detailType(), - Collections.EMPTY_MAP - ); - - if (AppConfigHelper.isBillingFirstTime(currentAppConfig, updatedAppConfig)) { - // 1. We didn't have a billing provider and now we do, trigger setup - // Existing provisioned tenants won't be subscribed to a billing plan - // so we don't need to update the tenant stacks. - LOGGER.info("AppConfig now has a billing provider. Triggering billing setup."); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, "saas-boost", - "Billing System Setup", - Map.of("message", "System Setup") - ); - } - - response = new APIGatewayProxyResponseEvent() - .withStatusCode(200) - .withHeaders(CORS) - .withBody(Utils.toJson(updatedAppConfig)); - } else { - LOGGER.info("Processing update to existing app config"); - List> provisionedTenants = getProvisionedTenants(context); - boolean provisioned = !provisionedTenants.isEmpty(); - boolean okToUpdate = validateAppConfigUpdate(currentAppConfig, updatedAppConfig, provisioned); - boolean fireUpdateAppConfigEvent = false; - - if (okToUpdate) { - LOGGER.info("Ok to proceed with app config update"); - if (AppConfigHelper.isDomainChanged(currentAppConfig, updatedAppConfig)) { - LOGGER.info("AppConfig domain name has changed"); - fireUpdateAppConfigEvent = true; - } - - if (AppConfigHelper.isBillingChanged(currentAppConfig, updatedAppConfig)) { - String apiKey1 = currentAppConfig.getBilling() != null - ? currentAppConfig.getBilling().getApiKey() : null; - String apiKey2 = updatedAppConfig.getBilling() != null - ? updatedAppConfig.getBilling().getApiKey() : null; - LOGGER.info("AppConfig billing provider has changed {} != {}", apiKey1, apiKey2); - if (AppConfigHelper.isBillingFirstTime(currentAppConfig, updatedAppConfig)) { - // 1. We didn't have a billing provider and now we do, trigger setup - // Existing provisioned tenants won't be subscribed to a billing plan - // so we don't need to update the tenant stacks. - LOGGER.info("AppConfig now has a billing provider. Triggering billing setup."); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, "saas-boost", - "Billing System Setup", - Map.of("message", "System Setup") - ); - } else if (AppConfigHelper.isBillingRemoved(currentAppConfig, updatedAppConfig)) { - // 2. We had a billing provider and now we don't, disable integration - LOGGER.info("AppConfig has removed the billing provider."); - // TODO how do we cleanup the billing provider integration? - } else { - // 3. We had a billing provider and we're just changing the value of the key, that is - // taken care of by dal.setAppConfig and we don't need to trigger a setup because - // it's already been done. - LOGGER.info("AppConfig billing provider API key in-place change."); - } - } - - if (AppConfigHelper.isServicesChanged(currentAppConfig, updatedAppConfig)) { - LOGGER.info("AppConfig application services changed"); - // Currently you can only remove services if there are no provisioned tenants - Set removedServices = AppConfigHelper.removedServices(currentAppConfig, updatedAppConfig); - if (!removedServices.isEmpty()) { - LOGGER.info("Services {} were removed from AppConfig: deleting their parameters.", - removedServices); - for (String serviceName : removedServices) { - dal.deleteServiceConfig(currentAppConfig, serviceName); - } - } - fireUpdateAppConfigEvent = true; - } - - // TODO how do we want to deal with tier settings changes? - - LOGGER.info("Persisting updated app config"); - updatedAppConfig = dal.setAppConfig(updatedAppConfig); - - // If the app config has any databases, get the presigned S3 urls to upload bootstrap files - if (!provisioned) { - generateDatabaseBootstrapFileUrl(updatedAppConfig); - } - - if (fireUpdateAppConfigEvent) { - // The provisioning system can take care of modifying/adding/removing infra - // due to changes in the app config - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, "saas-boost", - AppConfigEvent.APP_CONFIG_CHANGED.detailType(), - Collections.EMPTY_MAP - ); - } - - response = new APIGatewayProxyResponseEvent() - .withStatusCode(200) - .withHeaders(CORS) - .withBody(Utils.toJson(updatedAppConfig)); - } else { - LOGGER.info("App config update validation failed"); - response = new APIGatewayProxyResponseEvent() - .withStatusCode(400) - .withHeaders(CORS) - .withBody("{\"message\":\"Application config update validation failed.\"}"); - } - } - } - - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("SettingsService::updateAppConfig exec " + totalTimeMillis); - return response; - } - - public APIGatewayProxyResponseEvent deleteAppConfig(Map event, Context context) { - if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); - } - - final long startTimeMillis = System.currentTimeMillis(); - LOGGER.info("SettingsService::deleteAppConfig"); - Utils.logRequestEvent(event); - APIGatewayProxyResponseEvent response = null; - - try { - dal.deleteAppConfig(); - response = new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(200); - } catch (Exception e) { + LOGGER.error(Utils.getFullStackTrace(e)); response = new APIGatewayProxyResponseEvent() .withHeaders(CORS) - .withStatusCode(400) - .withBody("{\"message\":\"Error deleting application settings.\"}"); + .withStatusCode(HttpURLConnection.HTTP_INTERNAL_ERROR); } - - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("SettingsService::deleteAppConfig exec " + totalTimeMillis); return response; } - public void handleAppConfigEvent(Map event, Context context) { - if ("saas-boost".equals(event.get("source"))) { - AppConfigEvent appConfigEvent = AppConfigEvent.fromDetailType((String) event.get("detail-type")); - if (appConfigEvent != null) { - switch (appConfigEvent) { - case APP_CONFIG_RESOURCE_CHANGED: - LOGGER.info("Handling App Config Resource Changed"); - handleAppConfigResourceChanged(event, context); - break; - case APP_CONFIG_CHANGED: - // We produce this event, but currently aren't consuming it - break; - case APP_CONFIG_UPDATE_COMPLETED: - // We produce this event, but currently aren't consuming it - break; - default: { - LOGGER.error("Can't find app config event for detail-type {}", event.get("detail-type")); - // TODO Throw here? Would end up in DLQ. - } - } - } else { - LOGGER.error("Can't find app config event for detail-type {}", event.get("detail-type")); - // TODO Throw here? Would end up in DLQ. - } - } else if ("aws.s3".equals(event.get("source"))) { - LOGGER.info("Handling App Config Resources File S3 Event"); - handleAppConfigResourcesFileEvent(event, context); - } else { - LOGGER.error("Unknown event source " + event.get("source")); - // TODO Throw here? Would end up in DLQ. - } - } - - protected void handleAppConfigResourceChanged(Map event, Context context) { - Utils.logRequestEvent(event); - Map detail = (Map) event.get("detail"); - String json = Utils.toJson(detail); - if (json != null) { - AppConfig changedAppConfig = Utils.fromJson(json, AppConfig.class); - if (changedAppConfig != null) { - boolean update = false; - AppConfig existingAppConfig = dal.getAppConfig(); - // Only update the services if they were passed in - if (json.contains("services") && changedAppConfig.getServices() != null) { - for (Map.Entry changedService : changedAppConfig.getServices().entrySet()) { - String changedServiceName = changedService.getKey(); - ServiceConfig changedServiceConfig = changedService.getValue(); - ServiceConfig requestedService = existingAppConfig.getServices().get(changedServiceName); - ServiceConfig.Builder newServiceConfigBuilder = ServiceConfig.builder(requestedService); - if (requestedService != null && changedServiceConfig != null) { - // change container repo if passed - if (requestedService.getCompute() != null && changedServiceConfig.getCompute() != null) { - String changedContainerRepo = changedServiceConfig.getCompute().getContainerRepo(); - String existingContainerRepo = requestedService.getCompute().getContainerRepo(); - if (!Utils.nullableEquals(existingContainerRepo, changedContainerRepo)) { - LOGGER.info("Updating service {} ECR repo from {} to {}", changedServiceName, - requestedService.getCompute().getContainerRepo(), - changedServiceConfig.getCompute().getContainerRepo()); - // TODO what if the service shouldn't have a container repo, because compute - // TODO is of the wrong type? core stack listener shouldn't fire the ECR repo event - AbstractCompute.Builder existingComputeBuilder = requestedService - .getCompute().builder(); - newServiceConfigBuilder = newServiceConfigBuilder - .compute(existingComputeBuilder - .containerRepo(changedServiceConfig.getCompute().getContainerRepo()) - .build()); - } - } - // change s3 bucket name if passed (and if s3 already exists in service config) - if (requestedService.getS3() != null && changedServiceConfig.getS3() != null) { - String existingBucketName = requestedService.getS3().getBucketName(); - String newBucketName = changedServiceConfig.getS3().getBucketName(); - if (!Utils.nullableEquals(existingBucketName, newBucketName)) { - newServiceConfigBuilder = newServiceConfigBuilder.s3(changedServiceConfig.getS3()); - } - } - ServiceConfig newServiceConfig = newServiceConfigBuilder.build(); - if (!newServiceConfig.equals(requestedService)) { - LOGGER.info("Updating serviceConfig from {} to {}", - requestedService, newServiceConfig); - update = true; - dal.setServiceConfig(newServiceConfig); - } - } else { - LOGGER.error("Can't find app config service {}", changedServiceName); - } - } - } - // If there are provisioned tenants, and we just ran an update to the infrastructure - // we need to update the tenant environments to reflect any changes - if (update) { - List> provisionedTenants = getProvisionedTenants(context); - if (!provisionedTenants.isEmpty()) { - LOGGER.info("Updated app config with provisioned tenants"); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, "saas-boost", - AppConfigEvent.APP_CONFIG_UPDATE_COMPLETED.detailType(), - Collections.EMPTY_MAP); - } - } else { - LOGGER.info("No app config changes to process"); - } - } else { - LOGGER.error("Can't parse event detail as AppConfig {}", json); - } - } else { - LOGGER.error("Can't serialize detail to JSON {}", event.get("detail")); - } - } - - protected void handleAppConfigResourcesFileEvent(Map event, Context context) { - Utils.logRequestEvent(event); + interface SettingsServiceDependencyFactory { - // A database bootstrap file was uploaded for one of the app config services. - // We'll update the service config so Onboarding will know to run the file as - // part of provisioning RDS. - Map detail = (Map) event.get("detail"); - String bucket = (String) ((Map) detail.get("bucket")).get("name"); - String key = (String) ((Map) detail.get("object")).get("key"); - LOGGER.info("Processing resources bucket PUT {}, {}", bucket, key); - - // key will be services/${service_name}/bootstrap.sql - String serviceName = key.substring("services/".length(), (key.length() - "/bootstrap.sql".length())); - AppConfig appConfig = dal.getAppConfig(); - for (Map.Entry serviceConfig : appConfig.getServices().entrySet()) { - if (serviceName.equals(serviceConfig.getKey())) { - ServiceConfig service = serviceConfig.getValue(); - LOGGER.info("Saving bootstrap.sql file for {}", service.getName()); - service.getDatabase().setBootstrapFilename(key); - dal.setServiceConfig(service); - break; - } - } - } - - protected List> getProvisionedTenants(Context context) { - // Fetch all of the provisioned tenants - LOGGER.info("Calling tenant service to fetch all provisioned tenants"); - String getTenantsResponseBody = ApiGatewayHelper.signAndExecuteApiRequest( - ApiGatewayHelper.getApiRequest( - API_GATEWAY_HOST, - API_GATEWAY_STAGE, - ApiRequest.builder() - .resource("tenants?status=provisioned") - .method("GET") - .build() - ), - API_TRUST_ROLE, - context.getAwsRequestId() - ); - List> tenants = Utils.fromJson(getTenantsResponseBody, ArrayList.class); - if (tenants == null) { - tenants = new ArrayList<>(); - } - return tenants; + SettingsDataAccessLayer dal(); } - protected static boolean validateAppConfigUpdate(AppConfig currentAppConfig, AppConfig updatedAppConfig, - boolean provisionedTenants) { - boolean domainNameValid = true; - if (AppConfigHelper.isDomainChanged(currentAppConfig, updatedAppConfig) && provisionedTenants) { - LOGGER.error("Can't change domain name after onboarding tenants"); - domainNameValid = false; - } - - boolean serviceConfigValid = true; - if (AppConfigHelper.isServicesChanged(currentAppConfig, updatedAppConfig)) { - if (provisionedTenants) { - Set removedServices = AppConfigHelper.removedServices(currentAppConfig, updatedAppConfig); - if (!removedServices.isEmpty()) { - LOGGER.error("Can't remove existing application services after onboarding tenants"); - serviceConfigValid = false; - } - } - } + private static final class DefaultDependencyFactory implements SettingsServiceDependencyFactory { - return domainNameValid && serviceConfigValid; - } - - protected void generateDatabaseBootstrapFileUrl(AppConfig appConfig) { - // Create the pre-signed S3 URLs for the bootstrap SQL files. We won't save these to the - // database record because the user might not upload any SQL files. If they do, we'll - // process those uploads async and persist the relevant data to the database. - for (Map.Entry serviceConfig : appConfig.getServices().entrySet()) { - String serviceName = serviceConfig.getKey(); - ServiceConfig service = serviceConfig.getValue(); - if (service.hasDatabase() && Utils.isBlank(service.getDatabase().getBootstrapFilename())) { - try { - // Create a presigned S3 URL to upload the database bootstrap file to - final String key = "services/" + serviceName + "/bootstrap.sql"; - final Duration expires = Duration.ofMinutes(15); // UI times out in 10 min - PresignedPutObjectRequest presignedObject = presigner.presignPutObject(request -> request - .signatureDuration(expires) - .putObjectRequest(PutObjectRequest.builder() - .bucket(RESOURCES_BUCKET) - .key(key) - .build() - ).build() - ); - service.getDatabase().setBootstrapFilename(presignedObject.url().toString()); - } catch (S3Exception s3Error) { - LOGGER.error("s3 presign url failed", s3Error); - LOGGER.error(Utils.getFullStackTrace(s3Error)); - throw s3Error; - } - } + @Override + public SettingsDataAccessLayer dal() { + return new SettingsDataAccessLayer(Utils.sdkClient(SsmClient.builder(), SsmClient.SERVICE_NAME)); } } } \ No newline at end of file diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SettingsServiceDAL.java b/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SettingsServiceDAL.java deleted file mode 100644 index f38d0dbd..00000000 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SettingsServiceDAL.java +++ /dev/null @@ -1,585 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.services.acm.AcmClient; -import software.amazon.awssdk.services.acm.model.CertificateStatus; -import software.amazon.awssdk.services.acm.model.CertificateSummary; -import software.amazon.awssdk.services.acm.model.InvalidArgsException; -import software.amazon.awssdk.services.acm.model.ListCertificatesRequest; -import software.amazon.awssdk.services.acm.model.ListCertificatesResponse; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; -import software.amazon.awssdk.services.dynamodb.model.QueryResponse; -import software.amazon.awssdk.services.route53.Route53Client; -import software.amazon.awssdk.services.route53.model.HostedZone; -import software.amazon.awssdk.services.route53.model.ListHostedZonesRequest; -import software.amazon.awssdk.services.route53.model.ListHostedZonesResponse; -import software.amazon.awssdk.services.ssm.SsmClient; -import software.amazon.awssdk.services.ssm.model.*; - -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class SettingsServiceDAL { - - private static final Logger LOGGER = LoggerFactory.getLogger(SettingsServiceDAL.class); - private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV"); - private static final String OPTIONS_TABLE = System.getenv("OPTIONS_TABLE"); - private static final String AWS_REGION = System.getenv("AWS_REGION"); - - // Package private for testing - static final String SAAS_BOOST_PREFIX = "saas-boost"; - static final String APP_BASE_PATH = "app/"; - static final String PARAMETER_STORE_PREFIX = "/" + SAAS_BOOST_PREFIX + "/" + SAAS_BOOST_ENV + "/"; - // e.g. /saas-boost/production/SAAS_BOOST_BUCKET - static final Pattern SAAS_BOOST_PARAMETER_PATTERN = Pattern.compile("^" + PARAMETER_STORE_PREFIX + "(.+)$"); - // e.g. /saas-boost/test/app/APP_NAME or /saas-boost/test/app/myService/SERVICE_JSON - static final Pattern SAAS_BOOST_APP_PATTERN = Pattern.compile("^" + PARAMETER_STORE_PREFIX + APP_BASE_PATH + "(.+)$"); - - private final ParameterStoreFacade parameterStore; - private AcmClient acm; - private DynamoDbClient ddb; - private Route53Client route53; - - public SettingsServiceDAL() { - final long startTimeMillis = System.currentTimeMillis(); - if (Utils.isBlank(AWS_REGION)) { - throw new IllegalStateException("Missing environment variable AWS_REGION"); - } - if (Utils.isBlank(SAAS_BOOST_ENV)) { - throw new IllegalStateException("Missing environment variable SAAS_BOOST_ENV"); - } - SsmClient ssm = Utils.sdkClient(SsmClient.builder(), SsmClient.SERVICE_NAME); - // Warm up SSM for cold start hack - ssm.getParametersByPath(request -> request.path("/" + SAAS_BOOST_PREFIX + "/JUNK")); - parameterStore = new ParameterStoreFacade(ssm); - - if (Utils.isNotBlank(OPTIONS_TABLE)) { - this.ddb = Utils.sdkClient(DynamoDbClient.builder(), DynamoDbClient.SERVICE_NAME); - // Cold start performance hack -- take the TLS hit for the client in the constructor - this.ddb.describeTable(request -> request.tableName(OPTIONS_TABLE)); - } - this.acm = Utils.sdkClient(AcmClient.builder(), AcmClient.SERVICE_NAME); - this.route53 = Utils.sdkClient(Route53Client.builder(), Route53Client.SERVICE_NAME); - LOGGER.info("Constructor init: {}", System.currentTimeMillis() - startTimeMillis); - } - - public List getAllSettings() { - return getAllParametersUnder(PARAMETER_STORE_PREFIX, false) - .stream() - .map(SettingsServiceDAL::fromParameterStore) - .collect(Collectors.toList()); - } - - public List getAppConfigSettings() { - return getAllParametersUnder(PARAMETER_STORE_PREFIX + APP_BASE_PATH, true) - .stream() - .map(SettingsServiceDAL::fromAppParameterStore) - .collect(Collectors.toList()); - } - - public List getAllParametersUnder(String parameterStorePathPrefix, boolean recursive) { - final long startTimeMillis = System.currentTimeMillis(); - boolean decrypt = false; - List parameters = parameterStore.getParametersByPath(parameterStorePathPrefix, recursive, decrypt); - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("SettingsServiceDAL::getAllSettingsUnder {} Loaded {} parameters", - parameterStorePathPrefix, parameters.size()); - LOGGER.info("SettingsServiceDAL::getAllSettingsUnder exec " + totalTimeMillis); - return parameters; - } - - public List getNamedSettings(List namedSettings) { - LOGGER.info("getNamedSettings"); - long startTime = System.currentTimeMillis(); - List parameterNames = namedSettings - .stream() - .map(settingName -> toParameterStore(Setting.builder().name(settingName).build()).name()) - .collect(Collectors.toList()); - List settings = parameterStore.getParameters(parameterNames) - .stream() - .map(SettingsServiceDAL::fromParameterStore) - .collect(Collectors.toList()); - long endTime = System.currentTimeMillis(); - LOGGER.info("getNamedSettings exec: {} ms", endTime - startTime); - return settings; - } - - public Setting getSetting(String settingName) { - return getSetting(settingName, false); - } - - public Setting getSetting(String settingName, boolean decrypt) { - return fromParameterStore(parameterStore.getParameter( - toParameterStore(Setting.builder().name(settingName).build()).name(), decrypt)); - } - - public Setting getSecret(String settingName) { - if (settingName.contains("BILLING_API_KEY")) { - settingName = APP_BASE_PATH + settingName; - } - return getSetting(settingName, true); - } - - public String getParameterStoreReference(String settingName) { - Setting setting = getSetting(settingName); - return PARAMETER_STORE_PREFIX + setting.getName() + ":" + setting.getVersion(); - } - - public Setting updateSetting(Setting setting) { - Setting updated = fromParameterStore(parameterStore.putParameter(toParameterStore(setting))); - if (updated.isSecure()) { - // we don't want to return the unencrypted value, so replace this - // setting with the encrypted representation we just placed in ParameterStore - updated = getSetting(updated.getName()); - } - return updated; - } - - private void deleteSetting(Setting setting) { - parameterStore.deleteParameter(toParameterStore(setting)); - } - - public List> rdsOptions() { - List> orderableOptionsByRegion = new ArrayList<>(); - QueryResponse response = ddb.query(request -> request - .tableName(OPTIONS_TABLE) - .keyConditionExpression("#region = :region") - .expressionAttributeNames(Stream - .of(new AbstractMap.SimpleEntry<>("#region", "region")) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) - ) - .expressionAttributeValues(Stream - .of(new AbstractMap.SimpleEntry<>(":region", AttributeValue.builder().s(AWS_REGION).build())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) - ) - ); - response.items().forEach(item -> - orderableOptionsByRegion.add(fromAttributeValueMap(item)) - ); - return orderableOptionsByRegion; - } - - public List acmCertificateOptions() { - List certificateSummaries = new ArrayList<>(); - String nextToken = null; - do { - try { - // only list certificates that aren't expired, invalid, revoked, or otherwise unusable - ListCertificatesResponse response = acm.listCertificates(ListCertificatesRequest.builder() - .certificateStatuses(List.of(CertificateStatus.PENDING_VALIDATION, CertificateStatus.ISSUED)) - .nextToken(nextToken) - .build()); - LOGGER.info("ACM PENDING_VALIDATION and ISSUED certs: {}", response); - // documentation says the SDK will never return a null collection, but just in case - if (response.certificateSummaryList() != null) { - certificateSummaries.addAll(response.certificateSummaryList()); - } - nextToken = response.nextToken(); - } catch (InvalidArgsException iae) { - LOGGER.error("Error retrieving certificates", iae); - } - } while (nextToken != null); - return certificateSummaries; - } - - public List hostedZoneOptions() { - List allHostedZones = new ArrayList<>(); - String marker = null; - do { - ListHostedZonesResponse response = route53.listHostedZones(ListHostedZonesRequest.builder() - .marker(marker) - .build()); - LOGGER.info("Listed hostedZones: {}", response); - if (response.hasHostedZones() && response.hostedZones() != null) { - // we only want to list public zones, since we attaching them to an internet-facing - // ApplicationLoadBalancer for the tenant - for (HostedZone zone : response.hostedZones()) { - if (zone.config() != null && !zone.config().privateZone()) { - allHostedZones.add(zone); - } - } - } - marker = response.marker(); - } while (marker != null); - return allHostedZones; - } - - private static final Comparator> INSTANCE_TYPE_COMPARATOR = ((instance1, instance2) -> { - // T's before M's before R's - int compare = 0; - char type1 = instance1.get("instance").charAt(0); - char type2 = instance2.get("instance").charAt(0); - if (type1 != type2) { - if ('T' == type1) { - compare = -1; - } else if ('T' == type2) { - compare = 1; - } else if ('M' == type1) { - compare = -1; - } else if ('M' == type2) { - compare = 1; - } - } - return compare; - }); - - private static final Comparator> INSTANCE_GENERATION_COMPARATOR = ((instance1, instance2) -> { - Integer gen1 = Integer.valueOf(instance1.get("instance").substring(1, 2)); - Integer gen2 = Integer.valueOf(instance2.get("instance").substring(1, 2)); - return gen1.compareTo(gen2); - }); - - private static final Comparator> INSTANCE_SIZE_COMPARATOR = ((instance1, instance2) -> { - String size1 = instance1.get("instance").substring(3); - String size2 = instance2.get("instance").substring(3); - List sizes = Arrays.asList( - "MICRO", - "SMALL", - "MEDIUM", - "LARGE", - "XL", - "2XL", - "4XL", - "12XL", - "24XL" - ); - return Integer.compare(sizes.indexOf(size1), sizes.indexOf(size2)); - }); - - public static final Comparator> RDS_INSTANCE_COMPARATOR = INSTANCE_TYPE_COMPARATOR - .thenComparing(INSTANCE_GENERATION_COMPARATOR) - .thenComparing(INSTANCE_SIZE_COMPARATOR); - - public static Map fromAttributeValueMap(Map item) { - Map option = new LinkedHashMap<>(); - option.put("engine", item.get("engine").s()); - option.put("region", item.get("region").s()); - Map optionAttributes = item.get("options").m(); - option.put("name", optionAttributes.get("name").s()); - option.put("description", optionAttributes.get("description").s()); - - List> versions = new ArrayList<>(); - for (AttributeValue versionAttribute : optionAttributes.get("versions").l()) { - // build the version entry - Map versionAttributeMap = versionAttribute.m(); - Map version = new LinkedHashMap<>(); // use a linked map so we can sort - version.put("description", versionAttributeMap.get("description").s()); - version.put("family", versionAttributeMap.get("family").s()); - version.put("version", versionAttributeMap.get("version").s()); - - List> instances = new ArrayList<>(); - Map instancesAttributeMap = versionAttributeMap.get("instances").m(); - for (Map.Entry instanceAttribute : instancesAttributeMap.entrySet()) { - Map instance = new LinkedHashMap<>(); // use a linked map so we can sort - instance.put("instance", instanceAttribute.getKey()); - instance.put("class", instanceAttribute.getValue().m().get("class").s()); - instance.put("description", instanceAttribute.getValue().m().get("description").s()); - instances.add(instance); - } - Collections.sort(instances, RDS_INSTANCE_COMPARATOR); - version.put("instances", instances); - versions.add(version); - } - option.put("versions", versions); - - return option; - } - - public AppConfig setAppConfig(AppConfig appConfig) { - final long startTimeMillis = System.currentTimeMillis(); - LOGGER.info("SettingsServiceDAL::setAppConfig"); - // updateSettingsAndServices sends PUTs to ParameterStore - List updatedAppConfigSettings = updateSettingsAndSecrets(appConfigToSettings(appConfig)); - appConfig = appConfigFromSettings(updatedAppConfigSettings); - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("SettingsServiceDAL::setAppConfig exec " + totalTimeMillis); - return appConfig; - } - - public ServiceConfig setServiceConfig(ServiceConfig serviceConfig) { - final long startTimeMillis = System.currentTimeMillis(); - LOGGER.info("SettingsServiceDAL::setServiceConfig"); - List updatedServiceConfigSettings = updateSettingsAndSecrets(serviceConfigToSettings(serviceConfig)); - serviceConfig = appConfigFromSettings(updatedServiceConfigSettings).getServices().get(serviceConfig.getName()); - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("SettingsServiceDAL::setServiceConfig exec " + totalTimeMillis); - return serviceConfig; - } - - private List updateSettingsAndSecrets(List settingsToUpdate) { - List updatedSettings = new LinkedList<>(); - for (Setting setting : settingsToUpdate) { - LOGGER.info("Updating setting {} to {}", setting.getName(), setting.getValue()); - if (setting.isSecure()) { - Setting existing = getSetting(setting.getName()); - if (existing != null) { - LOGGER.info("Existing Secret {} {}", setting.getName(), existing.getValue()); - } else { - LOGGER.info("Secret {} doesn't exist yet", setting.getName()); - } - // If we were passed the encrypted string for a secret (from the UI), - // don't overwrite the secret with that gibberish... - if (existing != null && existing.getValue().equals(setting.getValue())) { - // Nothing has changed, don't overwrite the value in Parameter Store - LOGGER.info("Skipping update of secret because encrypted values are the same"); - updatedSettings.add(existing); - continue; - } - } - LOGGER.info("Calling put parameter {}", setting.getName()); - updatedSettings.add(updateSetting(setting)); - } - return updatedSettings; - } - - public AppConfig getAppConfig() { - final long startTimeMillis = System.currentTimeMillis(); - LOGGER.info("SettingsServiceDAL::getAppConfig"); - AppConfig appConfig = appConfigFromSettings(getAppConfigSettings()); - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("SettingsServiceDAL::getAppConfig exec " + totalTimeMillis); - return appConfig; - } - - private AppConfig toAppConfig(Map appSettings) { - AppConfig.Builder appConfigBuilder = AppConfig.builder() - .name(appSettings.get(APP_BASE_PATH + "APP_NAME")) - .domainName(appSettings.get(APP_BASE_PATH + "DOMAIN_NAME")) - .hostedZone(appSettings.get(APP_BASE_PATH + "HOSTED_ZONE")) - .sslCertificate(appSettings.get(APP_BASE_PATH + "SSL_CERT_ARN")); - - // TODO we shouldn't assume Settings passed to this function are encrypted or decrypted - // but right now we are assuming they're encrypted, because they always are - BillingProvider billingProvider = null; - Setting billingApiKey = getSetting(APP_BASE_PATH + "BILLING_API_KEY", true); - if (billingApiKey != null && Utils.isNotBlank(billingApiKey.getValue())) { - billingProvider = BillingProvider.builder() - .apiKey(appSettings.get(APP_BASE_PATH + "BILLING_API_KEY")) - .build(); - } - appConfigBuilder.billing(billingProvider); - - for (Map.Entry appSetting : appSettings.entrySet()) { - // every key that contains a "/" is necessarily nested under app - // e.g. /app/service_001/DB_PASSWORD - // /app/service_001/SERVICE_JSON - if (appSetting.getKey().contains("/") && appSetting.getKey().endsWith("SERVICE_JSON")) { - ServiceConfig existingServiceConfig = Utils.fromJson(appSetting.getValue(), ServiceConfig.class); - - // if this serviceConfig has a database, override the password with the encrypted version - ServiceConfig.Builder editedServiceConfigBuilder = ServiceConfig.builder(existingServiceConfig); - if (existingServiceConfig.hasDatabase()) { - Database.Builder editedDatabaseBuilder = Database.builder(existingServiceConfig.getDatabase()); - Setting dbMasterPasswordSetting = getSetting(APP_BASE_PATH + existingServiceConfig.getName() + "/DB_PASSWORD", false); - if (dbMasterPasswordSetting != null) { - editedDatabaseBuilder.password(dbMasterPasswordSetting.getValue()); - } - editedServiceConfigBuilder.database(editedDatabaseBuilder.build()); - } - appConfigBuilder.serviceConfig(editedServiceConfigBuilder.build()); - } - } - - return appConfigBuilder.build(); - } - - public void deleteAppConfig() { - final long startTimeMillis = System.currentTimeMillis(); - LOGGER.info("SettingsServiceDAL::deleteAppConfig"); - // NOTE: Could also implement this like deleteTenantSettings by combining SettingsService::REQUIRED_PARAMS - // and SettingsService::READ_WRITE_PARAMS and building the Parameter Store path by hand to avoid the call(s) - // to getParameters before the call to deleteParameters - AppConfig appConfig = getAppConfig(); - for (String serviceName : appConfig.getServices().keySet()) { - deleteServiceConfig(appConfig, serviceName); - } - List parametersToDelete = appConfigToSettings(appConfig).stream() - .map(s -> toParameterStore(s).name()) - .collect(Collectors.toList()); - parameterStore.deleteParameters(parametersToDelete); - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("SettingsServiceDAL::deleteAppConfig exec " + totalTimeMillis); - } - - public void deleteServiceConfig(AppConfig appConfig, String serviceName) { - parameterStore.deleteParameters(serviceConfigToSettings(appConfig.getServices().get(serviceName)) - .stream() - .map(s -> toParameterStore(s).name()) - .collect(Collectors.toList())); - } - - public static Setting fromParameterStore(Parameter parameter) { - Setting setting = null; - if (parameter != null) { - String parameterStoreName = parameter.name(); - if (Utils.isEmpty(parameterStoreName)) { - throw new RuntimeException("Can't get Setting name for blank Parameter Store name [" + parameter.toString() + "]"); - } - String settingName = null; - Matcher regex = SAAS_BOOST_PARAMETER_PATTERN.matcher(parameterStoreName); - if (regex.matches()) { - settingName = regex.group(1); - } - if (settingName == null) { - throw new RuntimeException("Parameter Store Parameter " + parameter.name() + " does not match SaaS Boost pattern"); - } - - setting = Setting.builder() - .name(settingName) // name now might be /SETTING - .value(!"N/A".equals(parameter.value()) ? parameter.value() : "") - .readOnly(!SettingsService.READ_WRITE_PARAMS.contains(settingName)) - .secure(ParameterType.SECURE_STRING == parameter.type()) - .version(parameter.version()) - .build(); - } - return setting; - } - - public static Parameter toParameterStore(Setting setting) { - if (setting == null || !Setting.isValidSettingName(setting.getName())) { - throw new RuntimeException("Can't create Parameter Store parameter with invalid Setting name"); - } - String parameterName = PARAMETER_STORE_PREFIX + setting.getName(); - String parameterValue = (Utils.isEmpty(setting.getValue())) ? "N/A" : setting.getValue(); - Parameter parameter = Parameter.builder() - .type(setting.isSecure() ? ParameterType.SECURE_STRING : ParameterType.STRING) - .name(parameterName) - .value(parameterValue) - .build(); - return parameter; - } - - public static Setting fromAppParameterStore(Parameter parameter) { - Setting setting = null; - if (parameter != null) { - String parameterStoreName = parameter.name(); - if (Utils.isEmpty(parameterStoreName)) { - throw new RuntimeException("Can't get Setting name for blank Parameter Store name"); - } - String settingName = null; - Matcher regex = SAAS_BOOST_APP_PATTERN.matcher(parameterStoreName); - if (regex.matches()) { - settingName = regex.group(1); - } - if (settingName == null) { - throw new RuntimeException("Parameter Store Parameter " + parameterStoreName + " does not match SaaS Boost app pattern " + SAAS_BOOST_APP_PATTERN); - } - setting = Setting.builder() - .name(APP_BASE_PATH + settingName) - .value(!"N/A".equals(parameter.value()) ? parameter.value() : "") - .readOnly(false) - .secure(ParameterType.SECURE_STRING == parameter.type()) - .version(parameter.version()) - .build(); - } - return setting; - } - - public List appConfigToSettings(AppConfig appConfig) { - List settings = new ArrayList<>(); - - settings.add(Setting.builder() - .name(APP_BASE_PATH + "APP_NAME") - .value(appConfig.getName()) - .readOnly(false) - .build()); - settings.add(Setting.builder() - .name(APP_BASE_PATH + "DOMAIN_NAME") - .value(appConfig.getDomainName()) - .readOnly(false) - .build()); - settings.add(Setting.builder() - .name(APP_BASE_PATH + "HOSTED_ZONE") - .value(appConfig.getHostedZone()) - .readOnly(false) - .build()); - settings.add(Setting.builder() - .name(APP_BASE_PATH + "SSL_CERT_ARN") - .value(appConfig.getSslCertificate()) - .readOnly(false) - .build()); - - for (ServiceConfig serviceConfig : appConfig.getServices().values()) { - settings.addAll(serviceConfigToSettings(serviceConfig)); - } - - String billingApiKeySettingValue = null; - if (appConfig.getBilling() != null) { - billingApiKeySettingValue = appConfig.getBilling().getApiKey(); - } - settings.add(Setting.builder() - .name(APP_BASE_PATH + "BILLING_API_KEY") - .value(billingApiKeySettingValue) - .readOnly(false) - .secure(true) - .build()); - - return settings; - } - - public List serviceConfigToSettings(ServiceConfig serviceConfig) { - List settings = new ArrayList<>(); - // we're keeping the DB_PASSWORD separate so we have an accessible form *somewhere* - // but that means we need to create the DB_PASSWORD for each Service - - // editedServiceConfig so that we can replace the password in all databases in tiers to have empty passwords - // that way we aren't storing actual passwords. - ServiceConfig.Builder editedServiceConfigBuilder = ServiceConfig.builder(serviceConfig); - String dbPasswordSettingValue = null; - if (serviceConfig.hasDatabase()) { - dbPasswordSettingValue = serviceConfig.getDatabase().getPassword(); - - Setting dbPasswordSetting = Setting.builder() - .name(APP_BASE_PATH + serviceConfig.getName() + "/DB_PASSWORD") - .value(dbPasswordSettingValue) - .secure(true).readOnly(false).build(); - settings.add(dbPasswordSetting); - - // place the passwordParam so appConfig holders can find the password if they need it - // and override password - // passwordParam should be an arn of the form - // arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/saas-boost/${Environment}/DB_PASSWORD - editedServiceConfigBuilder.database(Database.builder(serviceConfig.getDatabase()) - .password("**encrypted**") - .passwordParam(toParameterStore(dbPasswordSetting).name()) - .build()); - } - - settings.add(Setting.builder() - .name(APP_BASE_PATH + serviceConfig.getName() + "/SERVICE_JSON") - .value(Utils.toJson(editedServiceConfigBuilder.build())) - .readOnly(false).build()); - - return settings; - } - - public AppConfig appConfigFromSettings(List appConfigSettings) { - // Get the secret value for the optional billing provider or you'll always - // be testing for empty against the encrypted hash of the "N/A" sentinel string - return toAppConfig( - appConfigSettings.stream() - .collect(Collectors.toMap(Setting::getName, Setting::getValue))); - } -} diff --git a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/AppConfig.java b/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/AppConfig.java deleted file mode 100644 index 960a0b2c..00000000 --- a/services/settings-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/appconfig/AppConfig.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost.appconfig; - -import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; - -import java.util.*; - -@JsonDeserialize(builder = AppConfig.Builder.class) -public class AppConfig { - private final String name; - private final String domainName; - private final String hostedZone; - private final String sslCertificate; - private final Map services; - private final BillingProvider billing; - - private AppConfig(Builder builder) { - this.name = builder.name; - this.domainName = builder.domainName; - this.hostedZone = builder.hostedZone; - this.sslCertificate = builder.sslCertificate; - this.services = builder.services; - this.billing = builder.billing; - } - - public static Builder builder() { - return new Builder(); - } - - public static Builder builder(AppConfig otherAppConfig) { - return new Builder() - .name(otherAppConfig.name) - .domainName(otherAppConfig.domainName) - .hostedZone(otherAppConfig.hostedZone) - .sslCertificate(otherAppConfig.sslCertificate) - .services(otherAppConfig.services) - .billing(otherAppConfig.billing); - } - - public String getName() { - return name; - } - - public String getDomainName() { - return domainName; - } - - public String getHostedZone() { - return hostedZone; - } - - public String getSslCertificate() { - return sslCertificate; - } - - public BillingProvider getBilling() { - return billing; - } - - public Map getServices() { - return services != null ? Map.copyOf(services) : null; - } - - @JsonIgnore - public boolean isEmpty() { - return (Utils.isBlank(name) && Utils.isBlank(domainName) && Utils.isBlank(hostedZone) - && Utils.isBlank(sslCertificate) - && (billing == null || !billing.hasApiKey()) - && (services == null || services.isEmpty())); - } - - @Override - public String toString() { - return Utils.toJson(this); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - // Same reference? - if (this == obj) { - return true; - } - // Same type? - if (getClass() != obj.getClass()) { - return false; - } - final AppConfig other = (AppConfig) obj; - return (Utils.nullableEquals(name, other.getName()) - && Utils.nullableEquals(domainName, other.getDomainName()) - && Utils.nullableEquals(hostedZone, other.getHostedZone()) - && Utils.nullableEquals(sslCertificate, other.getSslCertificate()) - && ((services == null && other.services == null) || (servicesEqual(services, other.services))) - && Utils.nullableEquals(billing, other.getBilling())); - } - - public static boolean servicesEqual(Map services, Map otherServices) { - boolean equal = false; - if (services != null && otherServices != null) { - if (services.size() == otherServices.size()) { - boolean entriesEqual = true; - for (Map.Entry entry : services.entrySet()) { - if (!otherServices.containsKey(entry.getKey())) { - entriesEqual = false; - break; - } else { - ServiceConfig service1 = entry.getValue(); - ServiceConfig service2 = otherServices.get(entry.getKey()); - if (service1 == null && service2 == null) { - continue; - } - if (service1 == null || !service1.equals(service2)) { - entriesEqual = false; - break; - } - } - } - equal = entriesEqual; - } - } - return equal; - } - - @Override - public int hashCode() { - return Objects.hash(name, domainName, hostedZone, sslCertificate, services, billing); - } - - @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property] - @JsonIgnoreProperties(value = {"serviceConfig"}) - public static final class Builder { - private String name; - private String domainName; - private String hostedZone; - private String sslCertificate; - private Map services; - private BillingProvider billing; - - private Builder() { - services = new HashMap<>(); - } - - public Builder name(String name) { - this.name = name; - return this; - } - - public Builder domainName(String domainName) { - this.domainName = domainName; - return this; - } - - public Builder hostedZone(String hostedZone) { - this.hostedZone = hostedZone; - return this; - } - - public Builder sslCertificate(String sslCertificate) { - this.sslCertificate = sslCertificate; - return this; - } - - public Builder services(Map services) { - this.services = services != null ? services : new HashMap<>(); - return this; - } - - public Builder serviceConfig(ServiceConfig serviceConfig) { - this.services.put(serviceConfig.getName(), serviceConfig); - return this; - } - - public Builder billing(BillingProvider billing) { - this.billing = billing; - return this; - } - - public AppConfig build() { - return new AppConfig(this); - } - } -} diff --git a/services/settings-service/src/main/resources/spotbugs-exclude.xml b/services/settings-service/src/main/resources/spotbugs-exclude.xml new file mode 100644 index 00000000..17059f38 --- /dev/null +++ b/services/settings-service/src/main/resources/spotbugs-exclude.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigHelperTest.java b/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigHelperTest.java deleted file mode 100644 index 8f388876..00000000 --- a/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigHelperTest.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.AppConfig; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.AppConfigHelper; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.BillingProvider; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.ServiceConfig; -import org.junit.Test; - -import java.util.HashMap; -import java.util.Map; - -import static org.junit.Assert.*; - -public class AppConfigHelperTest { - - @Test - public void testIsDomainChanged() { - AppConfig existing = AppConfig.builder().build(); - AppConfig altered = AppConfig.builder().build(); - assertFalse("Both null", AppConfigHelper.isDomainChanged(existing, altered)); - - existing = AppConfig.builder().domainName("").build(); - altered = AppConfig.builder().domainName("").build(); - assertFalse("Both empty", AppConfigHelper.isDomainChanged(existing, altered)); - - existing = AppConfig.builder().domainName("ABC").build(); - altered = AppConfig.builder().domainName("abc").build(); - assertFalse("Ignore case", AppConfigHelper.isDomainChanged(existing, altered)); - - existing = AppConfig.builder().build(); - altered = AppConfig.builder().domainName("abc").build(); - assertTrue("null != non-empty", AppConfigHelper.isDomainChanged(existing, altered)); - - existing = AppConfig.builder().domainName("abc").build(); - altered = AppConfig.builder().build(); - assertTrue("null != non-empty", AppConfigHelper.isDomainChanged(existing, altered)); - - existing = AppConfig.builder().domainName("abc").build(); - altered = AppConfig.builder().domainName("xzy").build(); - assertTrue("Different values", AppConfigHelper.isDomainChanged(existing, altered)); - } - - @Test - public void testIsBillingProviderChanged() { - AppConfig existing = AppConfig.builder().build(); - AppConfig altered = AppConfig.builder().build(); - assertFalse("Both null", AppConfigHelper.isBillingChanged(existing, altered)); - - existing = AppConfig.builder().billing(BillingProvider.builder().build()).build(); - altered = AppConfig.builder().billing(BillingProvider.builder().build()).build(); - assertFalse("Both null keys", AppConfigHelper.isBillingChanged(existing, altered)); - - String apiKey1 = "AQICAHhcs1hgJKpJfeso9W7CCTmyCVulso9PlceBD2lnnVksMwFVwWN3pbig0jooa4LJ2IbtAAAAzjCBywYJKoZIhvcNAQcGoIG9MIG6AgEAMIG0BgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDGHgQErKnkEmp2kVkQIBEICBhlZ2lux43UJUx2R0Q3DdK80od7FHeWpA5mCLr7uWipkaQ79lxsx2ffRbwAPRbcves2NEWznQJsCm2+bgJRpE1mPEJtSfXwGVCsbf1RUGIAiB0+k+NKCih8qAlBcBsA9iFvRm0kVqoo9acz3ay56pImzWrg8wrjkhGkspnXZhvK7BZg5/zvxZ != AQICAHhcs1hgJKpJfeso9W7CCTmyCVulso9PlceBD2lnnVksMwFVwWN3pbig0jooa4LJ2IbtAAAAzjCBywYJKoZIhvcNAQcGoIG9MIG6AgEAMIG0BgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDGHgQErKnkEmp2kVkQIBEICBhlZ2lux43UJUx2R0Q3DdK80od7FHeWpA5mCLr7uWipkaQ79lxsx2ffRbwAPRbcves2NEWznQJsCm2+bgJRpE1mPEJtSfXwGVCsbf1RUGIAiB0+k+NKCih8qAlBcBsA9iFvRm0kVqoo9acz3ay56pImzWrg8wrjkhGkspnXZhvK7BZg5/zvxZ"; - existing = AppConfig.builder().billing(BillingProvider.builder().build()).build(); - altered = AppConfig.builder().billing(BillingProvider.builder().apiKey(apiKey1).build()).build(); - assertTrue("One null, one not null", AppConfigHelper.isBillingChanged(existing, altered)); - assertTrue("First time set", AppConfigHelper.isBillingFirstTime(existing, altered)); - } - - @Test - public void testIsSslCertArnChanged() { - AppConfig existing = AppConfig.builder().build(); - AppConfig altered = AppConfig.builder().build(); - assertFalse("Both null", AppConfigHelper.isSslArnChanged(existing, altered)); - - existing = AppConfig.builder().sslCertificate("").build(); - altered = AppConfig.builder().sslCertificate("").build(); - assertFalse("Both empty", AppConfigHelper.isSslArnChanged(existing, altered)); - - existing = AppConfig.builder().sslCertificate("ABC").build(); - altered = AppConfig.builder().sslCertificate("abc").build(); - assertFalse("Ignore case", AppConfigHelper.isSslArnChanged(existing, altered)); - - existing = AppConfig.builder().build(); - altered = AppConfig.builder().sslCertificate("abc").build(); - assertTrue("null != non-empty", AppConfigHelper.isSslArnChanged(existing, altered)); - - existing = AppConfig.builder().sslCertificate("abc").build(); - altered = AppConfig.builder().build(); - assertTrue("null != non-empty", AppConfigHelper.isSslArnChanged(existing, altered)); - - existing = AppConfig.builder().sslCertificate("abc").build(); - altered = AppConfig.builder().sslCertificate("xzy").build(); - assertTrue("Different values", AppConfigHelper.isSslArnChanged(existing, altered)); - } - - @Test - public void testIsServicesChanged() { - AppConfig existing = AppConfig.builder().build(); - AppConfig altered = AppConfig.builder().build(); - assertFalse(AppConfigHelper.isServicesChanged(existing, altered)); - - Map services1 = new HashMap<>(); - services1.put("foo", ServiceConfig.builder().build()); - Map services2 = new HashMap<>(); - services2.put("foo", ServiceConfig.builder().build()); - existing = AppConfig.builder().services(services1).build(); - altered = AppConfig.builder().services(services2).build(); - assertFalse(AppConfigHelper.isServicesChanged(existing, altered)); - - services2.put("bar", ServiceConfig.builder().build()); - existing = AppConfig.builder().services(services1).build(); - altered = AppConfig.builder().services(services2).build(); - assertTrue(AppConfigHelper.isServicesChanged(existing, altered)); - - services2.remove("bar"); - existing = AppConfig.builder().services(services1).build(); - altered = AppConfig.builder().services(services2).build(); - assertFalse(AppConfigHelper.isServicesChanged(existing, altered)); - - services1.clear(); - existing = AppConfig.builder().services(services1).build(); - altered = AppConfig.builder().services(services2).build(); - assertTrue(AppConfigHelper.isServicesChanged(existing, altered)); - } - - @Test - public void testRemovedServices() { - AppConfig existing = AppConfig.builder().build(); - AppConfig altered = AppConfig.builder().build(); - assertTrue(AppConfigHelper.removedServices(existing, altered).isEmpty()); - - Map services1 = new HashMap<>(); - services1.put("foo", ServiceConfig.builder().build()); - Map services2 = new HashMap<>(); - services2.put("FOO", ServiceConfig.builder().build()); - existing = AppConfig.builder().services(services1).build(); - altered = AppConfig.builder().services(services2).build(); - // foo | FOO - assertTrue(AppConfigHelper.removedServices(existing, altered).isEmpty()); - - // foo | FOO,bar - services2.put("bar", ServiceConfig.builder().build()); - existing = AppConfig.builder().services(services1).build(); - altered = AppConfig.builder().services(services2).build(); - assertTrue(AppConfigHelper.removedServices(existing, altered).isEmpty()); - - // foo | FOO - services2.remove("bar"); - existing = AppConfig.builder().services(services1).build(); - altered = AppConfig.builder().services(services2).build(); - assertTrue(AppConfigHelper.removedServices(existing, altered).isEmpty()); - - // foo | bar - services2.remove("FOO"); - services2.put("bar", ServiceConfig.builder().build()); - existing = AppConfig.builder().services(services1).build(); - altered = AppConfig.builder().services(services2).build(); - assertFalse(AppConfigHelper.removedServices(existing, altered).isEmpty()); - - // christmas,easter | bar,baz - services1.clear(); - services1.put("christmas", ServiceConfig.builder().build()); - services1.put("easter", ServiceConfig.builder().build()); - services2.clear(); - services2.put("bar", ServiceConfig.builder().build()); - services2.put("baz", ServiceConfig.builder().build()); - existing = AppConfig.builder().services(services1).build(); - altered = AppConfig.builder().services(services2).build(); - assertFalse(AppConfigHelper.removedServices(existing, altered).isEmpty()); - } -} diff --git a/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigTest.java b/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigTest.java deleted file mode 100644 index 01429a23..00000000 --- a/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/AppConfigTest.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.AppConfig; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.BillingProvider; -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.ServiceConfig; -import org.junit.Test; - -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; - -import static org.junit.Assert.*; - -public class AppConfigTest { - - @Test - public void testEquals() { - AppConfig config1 = AppConfig.builder().build(); - AppConfig config2 = null; - - assertFalse("NULL is not equal", config1.equals(config2)); - - config2 = config1; - assertTrue("Same instance", config1.equals(config2)); - - assertFalse("Different types are not equal", config1.equals(new HashMap<>())); - - config2 = AppConfig.builder().build(); - assertTrue("Empty config objects are equal", config1.equals(config2)); - - Map services1 = new HashMap<>(); - Map services2 = new HashMap<>(); - services1.put("foo", null); - services2.put("foo", null); - config1 = AppConfig.builder().services(services1).build(); - config2 = AppConfig.builder().services(services2).build(); - assertTrue("Both null services", config1.equals(config2)); - - services1.put("foo", ServiceConfig.builder().build()); - services2.put("foo", ServiceConfig.builder().build()); - config1 = AppConfig.builder().services(services1).build(); - config2 = AppConfig.builder().services(services2).build(); - assertTrue("Same services", config1.equals(config2)); - - services2.put("foo", null); - config1 = AppConfig.builder().services(services1).build(); - config2 = AppConfig.builder().services(services2).build(); - assertFalse("One service null", config1.equals(config2)); - - services1.put("foo", null); - services2.put("foo", ServiceConfig.builder().build()); - config1 = AppConfig.builder().services(services1).build(); - config2 = AppConfig.builder().services(services2).build(); - assertFalse("One service null", config1.equals(config2)); - - services1.put("foo", ServiceConfig.builder().build()); - services2.remove("foo"); - services2.put("bar", ServiceConfig.builder().build()); - config1 = AppConfig.builder().services(services1).build(); - config2 = AppConfig.builder().services(services2).build(); - assertFalse("Different service names", config1.equals(config2)); - - services2.put("foo", ServiceConfig.builder().build()); - config1 = AppConfig.builder().services(services1).build(); - config2 = AppConfig.builder().services(services2).build(); - assertFalse("Different number of services", config1.equals(config2)); - - services1.clear(); - services2.clear(); - services1.put("foo", ServiceConfig.builder().name("foo").build()); - services2.put("foo", ServiceConfig.builder().name("bar").build()); - config1 = AppConfig.builder().services(services1).build(); - config2 = AppConfig.builder().services(services2).build(); - assertFalse("Different service configs", config1.equals(config2)); - - config1 = AppConfig.builder() - .name("foo") - .domainName("bar") - .sslCertificate("baz") - .services(Map.of("foo", ServiceConfig.builder().build())) - .billing(BillingProvider.builder().build()) - .build(); - config2 = AppConfig.builder() - .name("foo") - .domainName("bar") - .sslCertificate("baz") - .services(Map.of("foo", ServiceConfig.builder().build())) - .billing(BillingProvider.builder().build()) - .build(); - assertTrue("Same name", config1.equals(config2)); - } - - @Test - public void testDeserialize() { - try (InputStream is = getClass().getClassLoader().getResourceAsStream("appConfig.json")) { - AppConfig appConfig = Utils.MAPPER.readValue(is, AppConfig.class); - //System.out.println(appConfig.toString()); - } catch (IOException ioe) { - throw new RuntimeException(ioe); - } - } - - @Test - public void testIsEmpty() { - AppConfig config = AppConfig.builder().build(); - assertTrue(config.isEmpty()); - - AppConfig config2 = AppConfig.builder().name("test").build(); - assertFalse(config2.isEmpty()); - - AppConfig config3 = AppConfig.builder().domainName("example.com").build(); - assertFalse(config3.isEmpty()); - - AppConfig config4 = AppConfig.builder().hostedZone("123456").build(); - assertFalse(config4.isEmpty()); - - AppConfig config5 = AppConfig.builder().sslCertificate("arn:aws:acm:xxxxx").build(); - assertFalse(config5.isEmpty()); - - AppConfig config6 = AppConfig.builder().services(Map.of("foo", ServiceConfig.builder().build())).build(); - assertFalse(config6.isEmpty()); - - AppConfig config7 = AppConfig.builder().billing(BillingProvider.builder().build()).build(); - assertTrue(config7.isEmpty()); - - AppConfig config8 = AppConfig.builder().billing(BillingProvider.builder().apiKey("test").build()).build(); - assertFalse(config8.isEmpty()); - } - -} diff --git a/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/S3StorageTest.java b/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/S3StorageTest.java deleted file mode 100644 index dd870ae1..00000000 --- a/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/S3StorageTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.amazon.aws.partners.saasfactory.saasboost; - -import com.amazon.aws.partners.saasfactory.saasboost.appconfig.S3Storage; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class S3StorageTest { - @Test - public void basic() { - assertEquals(new S3Storage(S3Storage.builder()), Utils.fromJson("{}", S3Storage.class)); - } -} diff --git a/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SettingsDataAccessLayerTest.java b/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SettingsDataAccessLayerTest.java new file mode 100644 index 00000000..c4ee48d3 --- /dev/null +++ b/services/settings-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SettingsDataAccessLayerTest.java @@ -0,0 +1,215 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.services.ssm.model.Parameter; +import software.amazon.awssdk.services.ssm.model.ParameterType; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +// Note that these tests will only work if you run them from Maven or if you add +// the AWS_REGION and SAAS_BOOST_ENV environment variables to your IDE's configuration settings +public class SettingsDataAccessLayerTest { + + private static String env; + + public SettingsDataAccessLayerTest() { + if (System.getenv("AWS_REGION") == null || System.getenv("SAAS_BOOST_ENV") == null) { + throw new IllegalStateException("Missing required environment variables for tests!"); + } + } + + @BeforeAll + public static void setup() { + env = System.getenv("SAAS_BOOST_ENV"); + } + + @Test + public void testFromParameterStore() { + String settingName = "SAAS_BOOST_BUCKET"; + String parameterName = SettingsDataAccessLayer.PARAMETER_STORE_PREFIX + settingName; + String parameterValue = "sb-" + env + "-artifacts-test"; + + assertNull(SettingsDataAccessLayer.fromParameterStore(null), "null parameter returns null setting"); + assertThrows(RuntimeException.class, + () -> SettingsDataAccessLayer.fromParameterStore(Parameter.builder().build()), + "null parameter name throws RuntimeException"); + assertThrows(RuntimeException.class, + () -> SettingsDataAccessLayer.fromParameterStore(Parameter.builder().name("").build()), + "Empty parameter name throws RuntimeException"); + assertThrows(RuntimeException.class, + () -> SettingsDataAccessLayer.fromParameterStore(Parameter.builder().name(" ").build()), + "Blank parameter name is invalid pattern throws RuntimeException"); + assertThrows(RuntimeException.class, + () -> SettingsDataAccessLayer.fromParameterStore(Parameter.builder().name("foobar").build()), + "Invalid pattern parameter name throws RuntimeException"); + + Parameter validParam = Parameter.builder() + .name(parameterName) + .value(parameterValue) + .type(ParameterType.STRING) + .version(null) + .build(); + Setting expectedValidSetting = Setting.builder() + .name(settingName) + .value(parameterValue) + .secure(false) + .version(null) + .description(null) + .build(); + assertEquals(expectedValidSetting, SettingsDataAccessLayer.fromParameterStore(validParam), "Valid " + parameterName + " param equals " + settingName + " setting"); + + String readWriteParameterName = SettingsDataAccessLayer.PARAMETER_STORE_PREFIX + "APP_NAME"; + Parameter readWriteParameter = Parameter.builder() + .name(readWriteParameterName) + .value("foobar") + .type(ParameterType.STRING) + .version(null) + .build(); + Setting expectedReadWriteSetting = Setting.builder() + .name("APP_NAME") + .value("foobar") + .secure(false) + .version(null) + .description(null) + .build(); + assertEquals(expectedReadWriteSetting, SettingsDataAccessLayer.fromParameterStore(readWriteParameter), "Read/Write param " + readWriteParameterName + " equals APP_NAME setting"); + + Parameter emptyParameter = Parameter.builder() + .name(parameterName) + .value("N/A") + .type(ParameterType.STRING) + .version(null) + .build(); + Setting expectedEmptySetting = Setting.builder() + .name(settingName) + .value("") + .secure(false) + .version(null) + .description(null) + .build(); + assertEquals(expectedEmptySetting, SettingsDataAccessLayer.fromParameterStore(emptyParameter), "Empty " + parameterName + " param equals blank setting"); + + Parameter secretParameter = Parameter.builder() + .name(parameterName) + .value(parameterValue) + .type(ParameterType.SECURE_STRING) + .version(null) + .build(); + Setting expectedSecretSetting = Setting.builder() + .name(settingName) + .value(parameterValue) + .secure(true) + .version(null) + .description(null) + .build(); + assertEquals(expectedSecretSetting, SettingsDataAccessLayer.fromParameterStore(secretParameter), "Valid secret param equals secure setting"); + } + + @Test + public void testToParameterStore() { + String settingName = "SAAS_BOOST_BUCKET"; + String parameterName = SettingsDataAccessLayer.PARAMETER_STORE_PREFIX + settingName; + String parameterValue = "sb-" + env + "-artifacts-test"; + + assertThrows(RuntimeException.class, () -> SettingsDataAccessLayer.toParameterStore(null), + "null setting throws RuntimeException"); + assertThrows(RuntimeException.class, () -> SettingsDataAccessLayer.toParameterStore(Setting.builder().build()), + "null setting name throws RuntimeException"); + assertThrows(RuntimeException.class, + () -> SettingsDataAccessLayer.toParameterStore(Setting.builder().name("").build()), + "Empty setting name throws RuntimeException"); + assertThrows(RuntimeException.class, + () -> SettingsDataAccessLayer.toParameterStore(Setting.builder().name(" ").build()), + "Blank setting name throws RuntimeException"); + + Parameter expectedEmptyParameter = Parameter.builder() + .name(parameterName) + .type(ParameterType.STRING) + .value("N/A") + .build(); + Setting settingNullValue = Setting.builder() + .name(settingName) + .value(null) + .description(null) + .version(null) + .secure(false) + .readOnly(false) + .build(); + assertEquals(expectedEmptyParameter, SettingsDataAccessLayer.toParameterStore(settingNullValue), "null setting value equals N/A parameter value"); + + Setting settingEmptyValue = Setting.builder() + .name(settingName) + .value("") + .description(null) + .version(null) + .secure(false) + .readOnly(false) + .build(); + assertEquals(expectedEmptyParameter, SettingsDataAccessLayer.toParameterStore(settingEmptyValue), "Empty setting value equals N/A parameter value"); + + Parameter expectedBlankParameter = Parameter.builder() + .name(parameterName) + .type(ParameterType.STRING) + .value(" ") + .build(); + Setting settingBlankValue = Setting.builder() + .name(settingName) + .value(" ") + .description(null) + .version(null) + .secure(false) + .readOnly(false) + .build(); + assertEquals(expectedBlankParameter, SettingsDataAccessLayer.toParameterStore(settingBlankValue), "Blank setting value equals N/A parameter value"); + + Parameter expectedValueParameter = Parameter.builder() + .name(parameterName) + .type(ParameterType.STRING) + .value(parameterValue) + .build(); + Setting settingWithValue = Setting.builder() + .name(settingName) + .value(parameterValue) + .description(null) + .version(null) + .secure(false) + .readOnly(false) + .build(); + assertEquals(expectedValueParameter, SettingsDataAccessLayer.toParameterStore(settingWithValue), "Setting value equals parameter value"); + + Parameter expectedSecretParameter = Parameter.builder() + .name(parameterName) + .type(ParameterType.SECURE_STRING) + .value(parameterValue) + .build(); + Setting settingSecretValue = Setting.builder() + .name(settingName) + .value(parameterValue) + .description(null) + .version(null) + .secure(true) + .readOnly(false) + .build(); + assertEquals(expectedSecretParameter, SettingsDataAccessLayer.toParameterStore(settingSecretValue), "Setting secret value equals secure parameter"); + } + +} diff --git a/services/system-user-service/pom.xml b/services/system-user-service/pom.xml index da5f2530..af63df2f 100644 --- a/services/system-user-service/pom.xml +++ b/services/system-user-service/pom.xml @@ -33,6 +33,7 @@ limitations under the License. + ${project.basedir}/../.. 0 @@ -60,12 +61,14 @@ limitations under the License. - junit - junit + org.jboss.logging + jboss-logging + 3.3.2.Final + test com.amazon.aws.partners.saasfactory.saasboost - Utils + KeycloakHelper 1.0.0 provided @@ -85,10 +88,5 @@ limitations under the License. - - org.keycloak - keycloak-admin-client - 19.0.3 - diff --git a/services/system-user-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoUserDataAccessLayer.java b/services/system-user-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoUserDataAccessLayer.java index 392e16b2..8ab2f577 100644 --- a/services/system-user-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoUserDataAccessLayer.java +++ b/services/system-user-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoUserDataAccessLayer.java @@ -148,11 +148,15 @@ public SystemUser insertUser(Map event, SystemUser user) { LOGGER.info("UserServiceDAL::insertUser"); SystemUser inserted; try { - AdminCreateUserResponse createUserResponse = cognito.adminCreateUser(AdminCreateUserRequest.builder() + AdminCreateUserResponse createUserResponse = cognito.adminCreateUser(request -> request .userPoolId(COGNITO_USER_POOL) .username(user.getUsername()) .userAttributes(toAttributeTypeCollection(user)) - .build() + ); + cognito.adminAddUserToGroup(request -> request + .userPoolId(COGNITO_USER_POOL) + .username(user.getUsername()) + .groupName("admin") ); inserted = fromUserType(createUserResponse.user()); } catch (SdkServiceException cognitoError) { @@ -170,6 +174,7 @@ public void deleteUser(Map event, String username) { .userPoolId(COGNITO_USER_POOL) .username(username) ); + // admin-delete-user also removes the user from any groups } catch (SdkServiceException cognitoError) { LOGGER.error("cognito-idp:AdminCreateUser", cognitoError); LOGGER.error(Utils.getFullStackTrace(cognitoError)); diff --git a/services/system-user-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SystemUserDataAccessLayer.java b/services/system-user-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SystemUserDataAccessLayer.java index 0e4d1ec7..043c994a 100644 --- a/services/system-user-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SystemUserDataAccessLayer.java +++ b/services/system-user-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SystemUserDataAccessLayer.java @@ -1,3 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.amazon.aws.partners.saasfactory.saasboost; import java.util.List; diff --git a/services/system-user-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SystemUserDataAccessLayerFactory.java b/services/system-user-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SystemUserDataAccessLayerFactory.java index 1a850143..6b1565fd 100644 --- a/services/system-user-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SystemUserDataAccessLayerFactory.java +++ b/services/system-user-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SystemUserDataAccessLayerFactory.java @@ -1,3 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.amazon.aws.partners.saasfactory.saasboost; import com.amazon.aws.partners.saasfactory.saasboost.keycloak.KeycloakUserDataAccessLayer; diff --git a/services/system-user-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakUserDataAccessLayer.java b/services/system-user-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakUserDataAccessLayer.java index e02c9ba9..a1e90977 100644 --- a/services/system-user-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakUserDataAccessLayer.java +++ b/services/system-user-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakUserDataAccessLayer.java @@ -19,7 +19,7 @@ import com.amazon.aws.partners.saasfactory.saasboost.SystemUser; import com.amazon.aws.partners.saasfactory.saasboost.SystemUserDataAccessLayer; import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,34 +35,37 @@ public class KeycloakUserDataAccessLayer implements SystemUserDataAccessLayer { private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakUserDataAccessLayer.class); - - private final KeycloakApi keycloak; + private static final String KEYCLOAK_HOST = System.getenv("KEYCLOAK_HOST"); + private static final RealmRepresentation KEYCLOAK_REALM = KeycloakUtils.asRealm( + System.getenv("KEYCLOAK_REALM")); public KeycloakUserDataAccessLayer() { - this(new KeycloakApi()); - } - - public KeycloakUserDataAccessLayer(KeycloakApi keycloak) { - this.keycloak = keycloak; + if (Utils.isEmpty(KEYCLOAK_HOST)) { + throw new IllegalStateException("Missing required environment variable KEYCLOAK_HOST"); + } + if (Utils.isEmpty(KEYCLOAK_REALM.getRealm())) { + throw new IllegalStateException("Missing required environment variable KEYCLOAK_REALM"); + } } @Override public List getUsers(Map event) { - return keycloak.listUsers(event).stream() + return keycloakApi(event) + .getUsers(KEYCLOAK_REALM) + .stream() .map(KeycloakUserDataAccessLayer::toSystemUser) .collect(Collectors.toList()); } @Override public SystemUser getUser(Map event, String username) { - return toSystemUser(keycloak.getUser(event, username)); + return toSystemUser(keycloakApi(event).getUser(KEYCLOAK_REALM, username)); } @Override public SystemUser updateUser(Map event, SystemUser user) { - UserRepresentation currentUser = keycloak.getUser(event, user.getUsername()); - UserRepresentation updatedUser = updateUserRepresentation(user, currentUser); - return toSystemUser(keycloak.putUser(event, updatedUser)); + UserRepresentation keycloakUser = keycloakApi(event).updateUser(KEYCLOAK_REALM, toKeycloakUser(user)); + return toSystemUser(keycloakUser); } @Override @@ -76,43 +79,41 @@ public SystemUser disableUser(Map event, String username) { } private SystemUser enableDisable(Map event, String username, boolean enable) { - UserRepresentation currentUser = keycloak.getUser(event, username); - currentUser.setEnabled(enable); - return toSystemUser(keycloak.putUser(event, currentUser)); + KeycloakAdminApi keycloak = keycloakApi(event); + UserRepresentation keycloakUser = keycloak.getUser(KEYCLOAK_REALM, username); + keycloakUser.setEnabled(enable); + return toSystemUser(keycloak.updateUser(KEYCLOAK_REALM, keycloakUser)); } @Override public SystemUser insertUser(Map event, SystemUser user) { // Create new users with a temp password that must be changed on first sign in - CredentialRepresentation tempPassword = new CredentialRepresentation(); - tempPassword.setType("password"); - tempPassword.setTemporary(Boolean.TRUE); - tempPassword.setValue(Utils.randomString(12)); - UserRepresentation keycloakUser = toKeycloakUser(user); - keycloakUser.setCredentials(List.of(tempPassword)); + keycloakUser.setCredentials(List.of(KeycloakUtils.temporaryPassword())); keycloakUser.setRequiredActions(List.of("UPDATE_PASSWORD")); keycloakUser.setEnabled(Boolean.TRUE); - keycloakUser.setGroups(List.of(keycloak.getAdminGroupPath(event))); - UserRepresentation createdUser = keycloak.createUser(event, keycloakUser); - return toSystemUser(createdUser); + KeycloakAdminApi keycloak = keycloakApi(event); + keycloakUser.setGroups(List.of(keycloak.getGroup(KEYCLOAK_REALM, "admin").getPath())); + + keycloakUser = keycloak.createUser(KEYCLOAK_REALM, keycloakUser); + return toSystemUser(keycloakUser); } @Override public void deleteUser(Map event, String username) { - keycloak.deleteUser(event, username); + KeycloakAdminApi keycloak = keycloakApi(event); + keycloak.deleteUser(KEYCLOAK_REALM, keycloak.getUser(KEYCLOAK_REALM, username)); } // VisibleForTesting - static SystemUser toSystemUser(UserRepresentation keycloakUser) { + protected static SystemUser toSystemUser(UserRepresentation keycloakUser) { SystemUser user = null; if (keycloakUser != null) { user = new SystemUser(); user.setId(keycloakUser.getId()); user.setCreated(LocalDateTime.ofInstant( - Instant.ofEpochMilli(keycloakUser.getCreatedTimestamp()), - ZoneId.of("UTC"))); + Instant.ofEpochMilli(keycloakUser.getCreatedTimestamp()), ZoneId.of("UTC"))); // Keycloak doesn't track when a user was last modified user.setModified(null); user.setActive(keycloakUser.isEnabled()); @@ -129,17 +130,10 @@ static SystemUser toSystemUser(UserRepresentation keycloakUser) { } // VisibleForTesting - static UserRepresentation toKeycloakUser(SystemUser user) { + protected static UserRepresentation toKeycloakUser(SystemUser user) { UserRepresentation keycloakUser = null; if (user != null) { - keycloakUser = updateUserRepresentation(user, new UserRepresentation()); - } - return keycloakUser; - } - - // VisibleForTesting - static UserRepresentation updateUserRepresentation(SystemUser user, UserRepresentation keycloakUser) { - if (user != null) { + keycloakUser = new UserRepresentation(); keycloakUser.setUsername(user.getUsername()); keycloakUser.setFirstName(user.getFirstName()); keycloakUser.setLastName(user.getLastName()); @@ -158,4 +152,15 @@ static UserRepresentation updateUserRepresentation(SystemUser user, UserRepresen } return keycloakUser; } + + protected KeycloakAdminApi keycloakApi(Map event) { + return new KeycloakAdminApi(KEYCLOAK_HOST, bearerToken(event)); + } + + protected String bearerToken(Map event) { + // If we got here, the API Gateway already verified the incoming JWT + // with the issuer, so we can safely reuse it without reverification + Map requestHeaders = (Map) event.get("headers"); + return requestHeaders.get("Authorization"); + } } diff --git a/services/system-user-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakApiTest.java b/services/system-user-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakApiTest.java index 76ba1684..494d8c92 100644 --- a/services/system-user-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakApiTest.java +++ b/services/system-user-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakApiTest.java @@ -17,8 +17,8 @@ package com.amazon.aws.partners.saasfactory.saasboost.keycloak; import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.keycloak.representations.idm.UserRepresentation; import org.mockito.ArgumentCaptor; @@ -43,9 +43,7 @@ import java.util.concurrent.Flow.Subscription; import static com.amazon.aws.partners.saasfactory.saasboost.keycloak.KeycloakTestUtils.*; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doReturn; @@ -62,7 +60,7 @@ public final class KeycloakApiTest { private ArgumentCaptor requestCaptor; private KeycloakApi api; - @Before + @BeforeEach public void setup() { mockClient = mock(HttpClient.class); requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); @@ -81,18 +79,18 @@ public void listUsers_basic() throws IOException, InterruptedException { assertUserListsEqual(expectedUsers, actualUsers); } - @Test(expected = RuntimeException.class) + @Test public void listUsers_wrongStatusCode() throws IOException, InterruptedException { doReturn(mockResponse(HttpURLConnection.HTTP_BAD_GATEWAY, null)) .when(mockClient).send(any(HttpRequest.class), any(BodyHandler.class)); - api.listUsers(TEST_EVENT); + assertThrows(RuntimeException.class, () -> api.listUsers(TEST_EVENT)); } - @Test(expected = RuntimeException.class) + @Test public void listUsers_invalidResponse() throws IOException, InterruptedException { doReturn(mockResponse(HttpURLConnection.HTTP_OK, Utils.toJson(null))) .when(mockClient).send(any(HttpRequest.class), any(BodyHandler.class)); - api.listUsers(TEST_EVENT); + assertThrows(RuntimeException.class, () -> api.listUsers(TEST_EVENT)); } @Test @@ -119,18 +117,18 @@ public void getUser_UTF8Name() throws IOException, InterruptedException { assertUsersEqual(expected, actual); } - @Test(expected = RuntimeException.class) + @Test public void getUser_wrongStatusCode() throws IOException, InterruptedException { doReturn(mockResponse(HttpURLConnection.HTTP_BAD_GATEWAY, null)) .when(mockClient).send(any(HttpRequest.class), any(BodyHandler.class)); - api.getUser(TEST_EVENT, "anyUser"); + assertThrows(RuntimeException.class, () -> api.getUser(TEST_EVENT, "anyUser")); } - @Test(expected = RuntimeException.class) + @Test public void getUser_invalidResponse() throws IOException, InterruptedException { doReturn(mockResponse(HttpURLConnection.HTTP_OK, Utils.toJson(null))) .when(mockClient).send(any(HttpRequest.class), any(BodyHandler.class)); - api.getUser(TEST_EVENT, "anyUser"); + assertThrows(RuntimeException.class, () -> api.getUser(TEST_EVENT, "anyUser")); } @Test @@ -146,11 +144,11 @@ public void createUser_basic() throws IOException, InterruptedException { assertUsersEqual(expected, actual); } - @Test(expected = RuntimeException.class) + @Test public void createUser_wrongStatusCode() throws IOException, InterruptedException { doReturn(mockResponse(HttpURLConnection.HTTP_BAD_GATEWAY, null)) .when(mockClient).send(any(HttpRequest.class), any(BodyHandler.class)); - api.createUser(TEST_EVENT, null); + assertThrows(RuntimeException.class, () -> api.createUser(TEST_EVENT, null)); } @Test @@ -163,11 +161,11 @@ public void putUser_basic() throws IOException, InterruptedException { assertUsersEqual(expected, actual); } - @Test(expected = RuntimeException.class) + @Test public void putUser_wrongStatusCode() throws IOException, InterruptedException { doReturn(mockResponse(HttpURLConnection.HTTP_BAD_GATEWAY, null)) .when(mockClient).send(any(HttpRequest.class), any(BodyHandler.class)); - api.putUser(TEST_EVENT, mockKeycloakUser("user")); + assertThrows(RuntimeException.class, () -> api.putUser(TEST_EVENT, mockKeycloakUser("user"))); } @Test @@ -183,11 +181,11 @@ public void deleteUser_basic() throws IOException, InterruptedException { assertUsersEqual(expected, actual); } - @Test(expected = RuntimeException.class) + @Test public void deleteUser_wrongStatusCode() throws IOException, InterruptedException { doReturn(mockResponse(HttpURLConnection.HTTP_BAD_GATEWAY, null)) .when(mockClient).send(any(HttpRequest.class), any(BodyHandler.class)); - api.deleteUser(TEST_EVENT, "anyUser"); + assertThrows(RuntimeException.class, () -> api.deleteUser(TEST_EVENT, "anyUser")); } @Test @@ -199,21 +197,21 @@ public void getAdminGroupPath_basic() throws IOException, InterruptedException { .when(mockClient).send(requestCaptor.capture(), any(BodyHandler.class)); String actualPath = api.getAdminGroupPath(TEST_EVENT); assertRequest(requestCaptor.getValue(), "GET", endpoint("/groups?search=" + adminGroupName), null); - assertEquals("Path should match", expectedPath, actualPath); + assertEquals(expectedPath, actualPath, "Path should match"); } - @Test(expected = RuntimeException.class) + @Test public void getAdminGroupPath_wrongStatusCode() throws IOException, InterruptedException { doReturn(mockResponse(HttpURLConnection.HTTP_BAD_GATEWAY, null)) .when(mockClient).send(any(HttpRequest.class), any(BodyHandler.class)); - api.getAdminGroupPath(TEST_EVENT); + assertThrows(RuntimeException.class, () -> api.getAdminGroupPath(TEST_EVENT)); } - @Test(expected = RuntimeException.class) + @Test public void getAdminGroupPath_invalidResponse() throws IOException, InterruptedException { doReturn(mockResponse(HttpURLConnection.HTTP_OK, Utils.toJson(null))) .when(mockClient).send(any(HttpRequest.class), any(BodyHandler.class)); - api.getAdminGroupPath(TEST_EVENT); + assertThrows(RuntimeException.class, () -> api.getAdminGroupPath(TEST_EVENT)); } @Test @@ -229,15 +227,15 @@ private void assertRequest(HttpRequest request, String method, String endpoint, assertNotNull(headers); List authHeaders = headers.allValues("Authorization"); assertNotNull(authHeaders); - assertEquals("Request should only have 1 Authorization header", 1, authHeaders.size()); - assertEquals("Request Authorization header should match event", EXAMPLE_AUTH_HEADER, authHeaders.get(0)); - assertEquals("Request URI should match", endpoint, request.uri().toString()); - assertEquals("Request method should match", method, request.method()); + assertEquals(1, authHeaders.size(), "Request should only have 1 Authorization header"); + assertEquals(EXAMPLE_AUTH_HEADER, authHeaders.get(0), "Request Authorization header should match event"); + assertEquals(endpoint, request.uri().toString(), "Request URI should match"); + assertEquals(method, request.method(), "Request method should match"); if (body != null) { request.bodyPublisher().ifPresentOrElse( publisher -> { - assertEquals("Request body content length should match", - (long) body.length(), publisher.contentLength()); + assertEquals((long) body.length(), publisher.contentLength(), + "Request body content length should match"); StringBodySubscriber bodySubscriber = new StringBodySubscriber(); publisher.subscribe(bodySubscriber); try { diff --git a/services/system-user-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakTestUtils.java b/services/system-user-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakTestUtils.java index 20571f35..80761b06 100644 --- a/services/system-user-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakTestUtils.java +++ b/services/system-user-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakTestUtils.java @@ -24,15 +24,15 @@ import java.util.Map; import java.util.Set; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.*; public final class KeycloakTestUtils { public static void assertUserListsEqual(List expected, List actual) { if ((expected == null && actual != null) || (expected != null && actual == null)) { - assertEquals("User lists do not match", expected, actual); + assertEquals(expected, actual, "User lists do not match"); } if (expected != null && actual != null) { - assertEquals("User list sizes must match", expected.size(), actual.size()); + assertEquals(expected.size(), actual.size(), "User list sizes must match"); for (int i = 0; i < expected.size(); i++) { UserRepresentation expectedUser = expected.get(i); UserRepresentation actualUser = actual.get(i); @@ -43,15 +43,15 @@ public static void assertUserListsEqual(List expected, List< public static void assertUsersEqual(UserRepresentation expected, UserRepresentation actual) { // the keycloak UserRepresentation class doesn't implement .equals :( - assertEquals("Id should match", expected.getId(), actual.getId()); - assertEquals("CreatedTimestamp should match", expected.getCreatedTimestamp(), actual.getCreatedTimestamp()); - assertEquals("Username should match", expected.getUsername(), actual.getUsername()); - assertEquals("Enabled should match", expected.isEnabled(), actual.isEnabled()); - assertEquals("Email should match", expected.getEmail(), actual.getEmail()); - assertEquals("EmailVerified should match", expected.isEmailVerified(), actual.isEmailVerified()); - assertEquals("FirstName should match", expected.getFirstName(), actual.getFirstName()); - assertEquals("LastName should match", expected.getLastName(), actual.getLastName()); - assertEquals("RequiredActions should match", expected.getRequiredActions(), actual.getRequiredActions()); + assertEquals(expected.getId(), actual.getId(), "Id should match"); + assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp(), "CreatedTimestamp should match"); + assertEquals(expected.getUsername(), actual.getUsername(), "Username should match"); + assertEquals(expected.isEnabled(), actual.isEnabled(), "Enabled should match"); + assertEquals(expected.getEmail(), actual.getEmail(), "Email should match"); + assertEquals(expected.isEmailVerified(), actual.isEmailVerified(), "EmailVerified should match"); + assertEquals(expected.getFirstName(), actual.getFirstName(), "FirstName should match"); + assertEquals(expected.getLastName(), actual.getLastName(), "LastName should match"); + assertEquals(expected.getRequiredActions(), actual.getRequiredActions(), "RequiredActions should match"); } public static UserRepresentation mockKeycloakUser(String username) { diff --git a/services/system-user-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakUserDataAccessLayerTest.java b/services/system-user-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakUserDataAccessLayerTest.java index cd348773..16ae9ca2 100644 --- a/services/system-user-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakUserDataAccessLayerTest.java +++ b/services/system-user-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakUserDataAccessLayerTest.java @@ -17,8 +17,7 @@ package com.amazon.aws.partners.saasfactory.saasboost.keycloak; import com.amazon.aws.partners.saasfactory.saasboost.SystemUser; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.keycloak.representations.idm.UserRepresentation; import org.mockito.ArgumentCaptor; @@ -29,74 +28,68 @@ import java.util.function.UnaryOperator; import static com.amazon.aws.partners.saasfactory.saasboost.keycloak.KeycloakTestUtils.*; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; public final class KeycloakUserDataAccessLayerTest { - private KeycloakApi mockApi; private KeycloakUserDataAccessLayer dal; private ArgumentCaptor userCaptor; - @Before - public void setup() { - mockApi = mock(KeycloakApi.class); - dal = new KeycloakUserDataAccessLayer(mockApi); - userCaptor = ArgumentCaptor.forClass(UserRepresentation.class); - } +// @Before +// public void setup() { +// dal = new KeycloakUserDataAccessLayer(); +// userCaptor = ArgumentCaptor.forClass(UserRepresentation.class); +// } - @Test - public void enableUserTest() { - final String username = "user"; - UserRepresentation existingUser = mockKeycloakUser(username); - existingUser.setEnabled(false); - doReturn(existingUser).when(mockApi).getUser(any(Map.class), any(String.class)); - doReturn(null).when(mockApi).putUser(any(Map.class), userCaptor.capture()); - dal.enableUser(Map.of(), username); - assertTrue("User should be active", userCaptor.getValue().isEnabled()); - } +// @Test +// public void enableUserTest() { +// final String username = "user"; +// UserRepresentation existingUser = mockKeycloakUser(username); +// existingUser.setEnabled(false); +// doReturn(existingUser).when(mockApi).getUser(any(Map.class), any(String.class)); +// doReturn(null).when(mockApi).putUser(any(Map.class), userCaptor.capture()); +// dal.enableUser(Map.of(), username); +// assertTrue("User should be active", userCaptor.getValue().isEnabled()); +// } - @Test - public void disableUserTest() { - final String username = "user"; - UserRepresentation existingUser = mockKeycloakUser(username); - existingUser.setEnabled(true); - doReturn(existingUser).when(mockApi).getUser(any(Map.class), any(String.class)); - doReturn(null).when(mockApi).putUser(any(Map.class), userCaptor.capture()); - dal.disableUser(Map.of(), username); - assertFalse("User should not be active", userCaptor.getValue().isEnabled()); - } +// @Test +// public void disableUserTest() { +// final String username = "user"; +// UserRepresentation existingUser = mockKeycloakUser(username); +// existingUser.setEnabled(true); +// doReturn(existingUser).when(mockApi).getUser(any(Map.class), any(String.class)); +// doReturn(null).when(mockApi).putUser(any(Map.class), userCaptor.capture()); +// dal.disableUser(Map.of(), username); +// assertFalse("User should not be active", userCaptor.getValue().isEnabled()); +// } - @Test - public void insertUserTest() { - final String username = "user"; - final String groupPath = "/testadmin"; - final SystemUser user = mockSystemUser(username); - doReturn(mockKeycloakUser(username)).when(mockApi).createUser(any(Map.class), userCaptor.capture()); - doReturn(groupPath).when(mockApi).getAdminGroupPath(any(Map.class)); - dal.insertUser(Map.of(), user); - UserRepresentation capturedUser = userCaptor.getValue(); - assertNotNull("Created user should have credentials", capturedUser.getCredentials()); - assertEquals("Created user should have exactly 1 credential", capturedUser.getCredentials().size(), 1); - assertEquals("Credential type should be password", capturedUser.getCredentials().get(0).getType(), "password"); - assertTrue("Credential should be temporary", capturedUser.getCredentials().get(0).isTemporary()); - assertEquals("Credential should be 12 characters long", - capturedUser.getCredentials().get(0).getValue().length(), 12); - assertNotNull("Created user should have required actions", capturedUser.getRequiredActions()); - assertEquals("Created user should have exactly 1 required action", - capturedUser.getRequiredActions().size(), 1); - assertEquals("Required action should be UPDATE_PASSWORD", - capturedUser.getRequiredActions().get(0), "UPDATE_PASSWORD"); - assertTrue("Created user should be enabled", capturedUser.isEnabled()); - assertNotNull("Created user should have groups", capturedUser.getGroups()); - assertEquals("Created user should have exactly 1 group", capturedUser.getGroups().size(), 1); - assertEquals("Created user should have admin group", capturedUser.getGroups().get(0), groupPath); - } +// @Test +// public void insertUserTest() { +// final String username = "user"; +// final String groupPath = "/testadmin"; +// final SystemUser user = mockSystemUser(username); +// doReturn(mockKeycloakUser(username)).when(mockApi).createUser(any(Map.class), userCaptor.capture()); +// doReturn(groupPath).when(mockApi).getAdminGroupPath(any(Map.class)); +// dal.insertUser(Map.of(), user); +// UserRepresentation capturedUser = userCaptor.getValue(); +// assertNotNull("Created user should have credentials", capturedUser.getCredentials()); +// assertEquals("Created user should have exactly 1 credential", capturedUser.getCredentials().size(), 1); +// assertEquals("Credential type should be password", capturedUser.getCredentials().get(0).getType(), "password"); +// assertTrue("Credential should be temporary", capturedUser.getCredentials().get(0).isTemporary()); +// assertEquals("Credential should be 12 characters long", +// capturedUser.getCredentials().get(0).getValue().length(), 12); +// assertNotNull("Created user should have required actions", capturedUser.getRequiredActions()); +// assertEquals("Created user should have exactly 1 required action", +// capturedUser.getRequiredActions().size(), 1); +// assertEquals("Required action should be UPDATE_PASSWORD", +// capturedUser.getRequiredActions().get(0), "UPDATE_PASSWORD"); +// assertTrue("Created user should be enabled", capturedUser.isEnabled()); +// assertNotNull("Created user should have groups", capturedUser.getGroups()); +// assertEquals("Created user should have exactly 1 group", capturedUser.getGroups().size(), 1); +// assertEquals("Created user should have admin group", capturedUser.getGroups().get(0), groupPath); +// } @Test public void toSystemUserTest() { @@ -104,44 +97,46 @@ public void toSystemUserTest() { final String requiredAction = "UPDATE_PASSWORD"; keycloakUser.setRequiredActions(List.of(requiredAction)); SystemUser sysUser = KeycloakUserDataAccessLayer.toSystemUser(keycloakUser); - assertEquals("Ids should match", keycloakUser.getId(), sysUser.getId()); - assertEquals("Created Long timestamp should match", keycloakUser.getCreatedTimestamp().longValue(), - sysUser.getCreated().toInstant(ZoneOffset.UTC).toEpochMilli()); - assertEquals("Modified should be null", null, sysUser.getModified()); - assertEquals("Active should match enabled", keycloakUser.isEnabled(), sysUser.getActive()); - assertEquals("Usernames should match", keycloakUser.getUsername(), sysUser.getUsername()); - assertEquals("FirstName should match", keycloakUser.getFirstName(), sysUser.getFirstName()); - assertEquals("LastName should match", keycloakUser.getLastName(), sysUser.getLastName()); - assertEquals("Email should match", keycloakUser.getEmail(), sysUser.getEmail()); - assertEquals("Email verified should match", keycloakUser.isEmailVerified(), sysUser.getEmailVerified()); - assertEquals("RequiredAction should match", requiredAction, sysUser.getStatus()); + assertEquals(keycloakUser.getId(), sysUser.getId(), "Ids should match"); + assertEquals(keycloakUser.getCreatedTimestamp().longValue(), + sysUser.getCreated().toInstant(ZoneOffset.UTC).toEpochMilli(), "Created Long timestamp should match"); + assertEquals(null, sysUser.getModified(), "Modified should be null"); + assertEquals(keycloakUser.isEnabled(), sysUser.getActive(), "Active should match enabled"); + assertEquals(keycloakUser.getUsername(), sysUser.getUsername(), "Usernames should match"); + assertEquals(keycloakUser.getFirstName(), sysUser.getFirstName(), "FirstName should match"); + assertEquals(keycloakUser.getLastName(), sysUser.getLastName(), "LastName should match"); + assertEquals(keycloakUser.getEmail(), sysUser.getEmail(), "Email should match"); + assertEquals(keycloakUser.isEmailVerified(), sysUser.getEmailVerified(), "Email verified should match"); + assertEquals(requiredAction, sysUser.getStatus(), "RequiredAction should match"); } - @Test - public void updateUserRepresentationTest() { - final String username = "user"; - UserRepresentation existingUser = mockKeycloakUser(username); - UserRepresentation editedUser = mockKeycloakUser(username); - SystemUser edits = new SystemUser(); - UnaryOperator alteration = (str) -> "different" + str; - edits.setId(alteration.apply(editedUser.getId())); - edits.setUsername(alteration.apply(editedUser.getUsername())); - edits.setFirstName(alteration.apply(editedUser.getFirstName())); - edits.setLastName(alteration.apply(editedUser.getLastName())); - edits.setActive(!editedUser.isEnabled()); - edits.setEmail(alteration.apply(editedUser.getEmail())); - edits.setEmailVerified(!editedUser.isEmailVerified()); - edits.setCreated(LocalDateTime.now()); - editedUser = KeycloakUserDataAccessLayer.updateUserRepresentation(edits, editedUser); - assertEquals("Id should not have changed", existingUser.getId(), editedUser.getId()); - assertNotEquals("Username should have changed", existingUser.getUsername(), editedUser.getUsername()); - assertNotEquals("FirstName should have changed", existingUser.getFirstName(), editedUser.getFirstName()); - assertNotEquals("LastName should have changed", existingUser.getLastName(), editedUser.getLastName()); - assertNotEquals("Active should have changed", existingUser.isEnabled(), editedUser.isEnabled()); - assertNotEquals("Email should have changed", existingUser.getEmail(), editedUser.getEmail()); - assertNotEquals("EmailVerified should have changed", - existingUser.isEmailVerified(), editedUser.isEmailVerified()); - assertNotEquals("CreatedTimestamp should have changed", - existingUser.getCreatedTimestamp(), editedUser.getCreatedTimestamp()); - } +// @Test +// public void updateUserTest() { +// final String username = "user"; +// UserRepresentation existingUser = mockKeycloakUser(username); +// UserRepresentation editedUser = mockKeycloakUser(username); +// SystemUser edits = new SystemUser(); +// +// UnaryOperator alteration = (str) -> "different" + str; +// edits.setId(alteration.apply(existingUser.getId())); +// edits.setUsername(alteration.apply(existingUser.getUsername())); +// edits.setFirstName(alteration.apply(existingUser.getFirstName())); +// edits.setLastName(alteration.apply(existingUser.getLastName())); +// edits.setActive(!existingUser.isEnabled()); +// edits.setEmail(alteration.apply(existingUser.getEmail())); +// edits.setEmailVerified(!existingUser.isEmailVerified()); +// edits.setCreated(LocalDateTime.now()); +// +// editedUser = KeycloakUserDataAccessLayer.updateUserRepresentation(edits, editedUser); +// assertEquals("Id should not have changed", existingUser.getId(), editedUser.getId()); +// assertNotEquals("Username should have changed", existingUser.getUsername(), editedUser.getUsername()); +// assertNotEquals("FirstName should have changed", existingUser.getFirstName(), editedUser.getFirstName()); +// assertNotEquals("LastName should have changed", existingUser.getLastName(), editedUser.getLastName()); +// assertNotEquals("Active should have changed", existingUser.isEnabled(), editedUser.isEnabled()); +// assertNotEquals("Email should have changed", existingUser.getEmail(), editedUser.getEmail()); +// assertNotEquals("EmailVerified should have changed", +// existingUser.isEmailVerified(), editedUser.isEmailVerified()); +// assertNotEquals("CreatedTimestamp should have changed", +// existingUser.getCreatedTimestamp(), editedUser.getCreatedTimestamp()); +// } } diff --git a/services/tenant-service/pom.xml b/services/tenant-service/pom.xml index 02053880..562eae7a 100644 --- a/services/tenant-service/pom.xml +++ b/services/tenant-service/pom.xml @@ -33,7 +33,8 @@ limitations under the License. - 35 + ${project.basedir}/../.. + 0 @@ -46,6 +47,17 @@ limitations under the License. org.apache.maven.plugins maven-surefire-plugin + + + us-east-1 + test + test + + + + + org.jacoco + jacoco-maven-plugin org.apache.maven.plugins @@ -55,17 +67,26 @@ limitations under the License. io.github.git-commit-id git-commit-id-maven-plugin + + com.github.spotbugs + spotbugs-maven-plugin + + Max + medium + + + software.amazon.lambda.snapstart + aws-lambda-snapstart-java-rules + 0.1.0 + + + ${project.basedir}/src/main/resources/spotbugs-exclude.xml + + - - com.amazon.aws.partners.saasfactory.saasboost - Utils - 1.0.0 - - provided - software.amazon.awssdk dynamodb diff --git a/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Tenant.java b/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Tenant.java index 196597d1..6d0a3529 100644 --- a/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Tenant.java +++ b/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Tenant.java @@ -25,6 +25,8 @@ @JsonIgnoreProperties(ignoreUnknown = true, value = {"hasBilling"}) public class Tenant { + protected static final List PROVISIONED_STATES = List.of("provisioned", "updating", "updated", + "deploying", "deployed", "completed"); private UUID id; private LocalDateTime created; private LocalDateTime modified; @@ -34,16 +36,16 @@ public class Tenant { private String name; private String subdomain; private String hostname; - private String billingPlan; private Map attributes = new HashMap<>(); private Map resources = new HashMap<>(); + private Set adminUsers = new HashSet<>(); public Tenant() { } @JsonProperty(access = JsonProperty.Access.READ_ONLY) public boolean isProvisioned() { - return onboardingStatus != null && !Arrays.asList("failed", "deleting", "deleted").contains(onboardingStatus); + return onboardingStatus != null && PROVISIONED_STATES.contains(onboardingStatus); } public UUID getId() { @@ -118,16 +120,8 @@ public void setHostname(String hostname) { this.hostname = hostname; } - public String getBillingPlan() { - return billingPlan; - } - - public void setBillingPlan(String billingPlan) { - this.billingPlan = billingPlan; - } - public Map getAttributes() { - return attributes; + return Map.copyOf(attributes); } public void setAttributes(Map attributes) { @@ -135,13 +129,23 @@ public void setAttributes(Map attributes) { } public Map getResources() { - return resources; + return Map.copyOf(resources); } public void setResources(Map resources) { this.resources = resources != null ? resources : new HashMap<>(); } + public Set getAdminUsers() { + return Set.copyOf(adminUsers); + } + + public void setAdminUsers(Collection adminUsers) { + if (adminUsers != null) { + this.adminUsers.addAll(adminUsers); + } + } + public boolean equals(Object obj) { if (obj == null) { return false; @@ -179,28 +183,31 @@ public boolean equals(Object obj) { } } } + boolean adminUsersEqual = adminUsers != null && adminUsers.equals(other.adminUsers); + return ( - ((id == null && other.id == null) || (id != null && id.equals(other.id))) - && ((created == null && other.created == null) || (created != null && created.equals(other.created))) - && ((modified == null && other.modified == null) || (modified != null && modified.equals(other.modified))) + Objects.equals(id, other.id) + && Objects.equals(created, other.created) + && Objects.equals(modified, other.modified) && (active == other.active) - && ((tier == null && other.tier == null) || (tier != null && tier.equals(other.tier))) - && ((onboardingStatus == null && other.onboardingStatus == null) || (onboardingStatus != null && onboardingStatus.equals(other.onboardingStatus))) - && ((name == null && other.name == null) || (name != null && name.equals(other.name))) - && ((subdomain == null && other.subdomain == null) || (subdomain != null && subdomain.equals(other.subdomain))) - && ((hostname == null && other.hostname == null) || (hostname != null && hostname.equals(other.hostname))) - && ((billingPlan == null && other.billingPlan == null) || (billingPlan != null && billingPlan.equals(other.billingPlan))) + && Objects.equals(tier, other.tier) + && Objects.equals(onboardingStatus, other.onboardingStatus) + && Objects.equals(name, other.name) + && Objects.equals(subdomain, other.subdomain) + && Objects.equals(hostname, other.hostname) && ((attributes == null && other.attributes == null) || attributesEqual) - && ((resources == null && other.resources == null) || resourcesEqual)); + && ((resources == null && other.resources == null) || resourcesEqual) + && ((adminUsers == null && other.adminUsers == null) || adminUsersEqual)); } @Override public int hashCode() { - return Objects.hash(id, created, modified, active, tier, onboardingStatus, name, subdomain, hostname, billingPlan) + return Objects.hash(id, created, modified, active, tier, onboardingStatus, name, subdomain, hostname) + Arrays.hashCode(attributes != null ? attributes.keySet().toArray(new String[0]) : null) + Arrays.hashCode(attributes != null ? attributes.values().toArray(new Object[0]) : null) + Arrays.hashCode(resources != null ? resources.keySet().toArray(new String[0]) : null) - + Arrays.hashCode(resources != null ? resources.values().toArray(new Resource[0]) : null); + + Arrays.hashCode(resources != null ? resources.values().toArray(new Resource[0]) : null) + + Arrays.hashCode(adminUsers != null ? adminUsers.toArray(new TenantAdminUser[0]) : null); } public static class Resource { @@ -256,9 +263,9 @@ public boolean equals(Object obj) { } final Resource other = (Resource) obj; return ( - ((name == null && other.name == null) || (name != null && name.equals(other.name))) - && ((arn == null && other.arn == null) || (arn != null && arn.equals(other.arn))) - && ((consoleUrl == null && other.consoleUrl == null) || (consoleUrl != null && consoleUrl.equals(other.consoleUrl)))); + Objects.equals(name, other.name) + && Objects.equals(arn, other.arn) + && Objects.equals(consoleUrl, other.consoleUrl)); } @Override diff --git a/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantAdminUser.java b/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantAdminUser.java new file mode 100644 index 00000000..695a7987 --- /dev/null +++ b/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantAdminUser.java @@ -0,0 +1,135 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; + +import java.util.Objects; + +@JsonDeserialize(builder = TenantAdminUser.Builder.class) +public class TenantAdminUser { + + private final String username; + private final String email; + private final String phoneNumber; + private final String givenName; + private final String familyName; + + private TenantAdminUser(Builder builder) { + this.username = builder.username; + this.email = builder.email; + this.phoneNumber = builder.phoneNumber; + this.givenName = builder.givenName; + this.familyName = builder.familyName; + } + + public String getUsername() { + return username; + } + + public String getEmail() { + return email; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public String getGivenName() { + return givenName; + } + + public String getFamilyName() { + return familyName; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + final TenantAdminUser other = (TenantAdminUser) obj; + + return ( + Objects.equals(username, other.username) + && Objects.equals(email, other.email) + && Objects.equals(phoneNumber, other.phoneNumber) + && Objects.equals(givenName, other.givenName) + && Objects.equals(familyName, other.familyName)); + } + + @Override + public int hashCode() { + return Objects.hash(username, email, phoneNumber, givenName, familyName); + } + + @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property] + public static final class Builder { + + private String username; + private String email; + private String phoneNumber; + private String givenName; + private String familyName; + + private Builder() { + } + + public Builder username(String username) { + this.username = username; + return this; + } + + public Builder email(String email) { + this.email = email; + return this; + } + + public Builder phoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + return this; + } + + public Builder givenName(String givenName) { + this.givenName = givenName; + return this; + } + + public Builder familyName(String familyName) { + this.familyName = familyName; + return this; + } + + public TenantAdminUser build() { + if (username == null || username.isBlank()) { + throw new IllegalStateException("Can't build TenantAdminUser without username"); + } + return new TenantAdminUser(this); + } + } +} diff --git a/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantServiceDAL.java b/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantDataAccessLayer.java similarity index 80% rename from services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantServiceDAL.java rename to services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantDataAccessLayer.java index 74c46a13..64ce9403 100644 --- a/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantServiceDAL.java +++ b/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantDataAccessLayer.java @@ -27,19 +27,17 @@ import java.util.*; import java.util.stream.Collectors; -public class TenantServiceDAL { +public class TenantDataAccessLayer { - private static final Logger LOGGER = LoggerFactory.getLogger(TenantServiceDAL.class); - private static final String TENANTS_TABLE = System.getenv("TENANTS_TABLE"); + private static final Logger LOGGER = LoggerFactory.getLogger(TenantDataAccessLayer.class); + private final String tenantsTable; private final DynamoDbClient ddb; - public TenantServiceDAL() { - if (Utils.isBlank(TENANTS_TABLE)) { - throw new IllegalStateException("Missing required environment variable TENANTS_TABLE"); - } - this.ddb = Utils.sdkClient(DynamoDbClient.builder(), DynamoDbClient.SERVICE_NAME); + public TenantDataAccessLayer(DynamoDbClient ddb, String tenantsTable) { + this.tenantsTable = tenantsTable; + this.ddb = ddb; // Cold start performance hack -- take the TLS hit for the client in the constructor - this.ddb.describeTable(request -> request.tableName(TENANTS_TABLE)); + this.ddb.describeTable(request -> request.tableName(tenantsTable)); } public List getOnboardedTenants() { @@ -51,7 +49,7 @@ public List getOnboardedTenants() { List tenants = new ArrayList<>(); try { ScanResponse response = ddb.scan(request -> request - .tableName(TENANTS_TABLE) + .tableName(tenantsTable) .filterExpression("attribute_exists(onboarding_status) " + "AND onboarding_status IN (:updating, :updated, :deploying, :deployed)") .expressionAttributeValues(Map.of( @@ -83,7 +81,7 @@ public List getProvisionedTenants() { List tenants = new ArrayList<>(); try { ScanResponse response = ddb.scan(ScanRequest.builder() - .tableName(TENANTS_TABLE) + .tableName(tenantsTable) .filterExpression("attribute_exists(onboarding_status) " + "AND onboarding_status <> :failed " + "AND onboarding_status <> :deleting " @@ -95,7 +93,7 @@ public List getProvisionedTenants() { )) .build() ); - LOGGER.info("TenantServiceDAL::getProvisionedTenants returning {} provisioned tenants", response.items().size()); + LOGGER.info("Returning {} provisioned tenants", response.items().size()); response.items().forEach(item -> tenants.add(fromAttributeValueMap(item)) ); @@ -113,7 +111,7 @@ public List getAllTenants() { LOGGER.info("TenantServiceDAL::getAllTenants"); List tenants = new ArrayList<>(); try { - ScanResponse response = ddb.scan(request -> request.tableName(TENANTS_TABLE)); + ScanResponse response = ddb.scan(request -> request.tableName(tenantsTable)); response.items().forEach(item -> tenants.add(fromAttributeValueMap(item)) ); @@ -137,7 +135,7 @@ public Tenant getTenant(String tenantId) { try { Map key = new HashMap<>(); key.put("id", AttributeValue.builder().s(tenantId).build()); - GetItemResponse response = ddb.getItem(request -> request.tableName(TENANTS_TABLE).key(key)); + GetItemResponse response = ddb.getItem(request -> request.tableName(tenantsTable).key(key)); item = response.item(); } catch (DynamoDbException e) { LOGGER.error("TenantServiceDAL::getTenant " + Utils.getFullStackTrace(e)); @@ -159,7 +157,7 @@ public Tenant updateTenant(Tenant tenant) { // object was persisted tenant.setModified(LocalDateTime.now()); Map item = toAttributeValueMap(tenant); - ddb.putItem(request -> request.tableName(TENANTS_TABLE).item(item)); + ddb.putItem(request -> request.tableName(tenantsTable).item(item)); } catch (DynamoDbException e) { LOGGER.error("TenantServiceDAL::updateTenant " + Utils.getFullStackTrace(e)); throw new RuntimeException(e); @@ -177,7 +175,7 @@ public Tenant updateTenantOnboardingStatus(UUID tenantId, String onboardingStatu String modified = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); key.put("id", AttributeValue.builder().s(tenantId.toString()).build()); UpdateItemResponse response = ddb.updateItem(request -> request - .tableName(TENANTS_TABLE) + .tableName(tenantsTable) .key(key) .updateExpression("SET onboarding_status = :onboarding, modified = :modified") .expressionAttributeValues(Map.of( @@ -201,7 +199,7 @@ public Tenant updateTenantHostname(String tenantId, String hostname) { String modified = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); key.put("id", AttributeValue.builder().s(tenantId).build()); UpdateItemResponse response = ddb.updateItem(request -> request - .tableName(TENANTS_TABLE) + .tableName(tenantsTable) .key(key) .updateExpression("SET hostname = :hostname, modified = :modified") .expressionAttributeValues(Map.of( @@ -229,8 +227,8 @@ public Tenant updateTenantResources(String tenantId, Map tenantResource : resources.entrySet()) { - String resourceKey = tenantResource.getKey(); - Tenant.Resource resourceValue = tenantResource.getValue(); + final String resourceKey = tenantResource.getKey(); + final Tenant.Resource resourceValue = tenantResource.getValue(); updateExpression.append(", "); updateExpression.append(mapAttributeUpdateExpression("resources", resourceKey, resourceKey)); @@ -252,7 +250,7 @@ public Tenant updateTenantResources(String tenantId, Map key = new HashMap<>(); key.put("id", AttributeValue.builder().s(tenantId).build()); UpdateItemResponse response = ddb.updateItem(request -> request - .tableName(TENANTS_TABLE) + .tableName(tenantsTable) .key(key) .updateExpression("SET active = :active, modified = :modified") .expressionAttributeValues(Map.of( @@ -320,8 +318,11 @@ public Tenant insertTenant(Tenant tenant) { LocalDateTime now = LocalDateTime.now(); tenant.setCreated(now); tenant.setModified(now); + if (Utils.isEmpty(tenant.getOnboardingStatus())) { + tenant.setOnboardingStatus("unknown"); + } try { - ddb.putItem(request -> request.tableName(TENANTS_TABLE).item(toAttributeValueMap(tenant))); + ddb.putItem(request -> request.tableName(tenantsTable).item(toAttributeValueMap(tenant))); } catch (DynamoDbException e) { LOGGER.error("TenantServiceDAL::insertTenant " + Utils.getFullStackTrace(e)); throw e; @@ -341,7 +342,7 @@ public void deleteTenant(String tenantId) { try { Map key = new HashMap<>(); key.put("id", AttributeValue.builder().s(tenantId).build()); - ddb.deleteItem(request -> request.tableName(TENANTS_TABLE).key(key)); + ddb.deleteItem(request -> request.tableName(tenantsTable).key(key)); } catch (DynamoDbException e) { LOGGER.error("TenantServiceDAL::deleteTenant " + Utils.getFullStackTrace(e)); throw new RuntimeException(e); @@ -350,14 +351,16 @@ public void deleteTenant(String tenantId) { LOGGER.info("TenantServiceDAL::deleteTenant exec " + totalTimeMillis); } - public static Map toAttributeValueMap(Tenant tenant) { + protected static Map toAttributeValueMap(Tenant tenant) { Map item = new HashMap<>(); item.put("id", AttributeValue.builder().s(tenant.getId().toString()).build()); if (tenant.getCreated() != null) { - item.put("created", AttributeValue.builder().s(tenant.getCreated().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); + item.put("created", AttributeValue.builder().s( + tenant.getCreated().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); } if (tenant.getModified() != null) { - item.put("modified", AttributeValue.builder().s(tenant.getModified().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); + item.put("modified", AttributeValue.builder().s( + tenant.getModified().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); } if (tenant.getActive() != null) { item.put("active", AttributeValue.builder().bool(tenant.getActive()).build()); @@ -377,9 +380,6 @@ public static Map toAttributeValueMap(Tenant tenant) { if (Utils.isNotBlank(tenant.getTier())) { item.put("tier", AttributeValue.builder().s(tenant.getTier()).build()); } - if (Utils.isNotBlank(tenant.getBillingPlan())) { - item.put("billing_plan", AttributeValue.builder().s(tenant.getBillingPlan()).build()); - } if (tenant.getAttributes() != null) { item.put("attributes", AttributeValue.builder().m(tenant.getAttributes().entrySet() .stream() @@ -397,18 +397,53 @@ public static Map toAttributeValueMap(Tenant tenant) { entry -> entry.getKey(), entry -> AttributeValue.builder().m( Map.of( - "name", AttributeValue.builder().s(entry.getValue().getName()).build(), - "arn", AttributeValue.builder().s(entry.getValue().getArn()).build(), - "consoleUrl", AttributeValue.builder().s(entry.getValue().getConsoleUrl()).build() + "name", AttributeValue.builder().s( + entry.getValue().getName()).build(), + "arn", AttributeValue.builder().s( + entry.getValue().getArn()).build(), + "consoleUrl", AttributeValue.builder().s( + entry.getValue().getConsoleUrl()).build() )).build() )) ).build() ); } + if (tenant.getAdminUsers() != null) { + item.put("admin_users", AttributeValue.builder().l(tenant.getAdminUsers() + .stream().map(user -> AttributeValue.builder().m( + toAttributeValueMap(user) + ).build()) + .collect(Collectors.toSet()) + ).build()); + } + LOGGER.debug("DynamoDB Item"); + LOGGER.debug(Utils.toJson(item)); return item; } - public static Tenant fromAttributeValueMap(Map item) { + protected static Map toAttributeValueMap(TenantAdminUser adminUser) { + Map item = new LinkedHashMap<>(); + if (adminUser != null) { + if (Utils.isNotBlank(adminUser.getUsername())) { + item.put("username", AttributeValue.builder().s(adminUser.getUsername()).build()); + } + if (Utils.isNotBlank(adminUser.getEmail())) { + item.put("email", AttributeValue.builder().s(adminUser.getEmail()).build()); + } + if (Utils.isNotBlank(adminUser.getPhoneNumber())) { + item.put("phone_number", AttributeValue.builder().s(adminUser.getPhoneNumber()).build()); + } + if (Utils.isNotBlank(adminUser.getGivenName())) { + item.put("given_name", AttributeValue.builder().s(adminUser.getGivenName()).build()); + } + if (Utils.isNotBlank(adminUser.getFamilyName())) { + item.put("family_name", AttributeValue.builder().s(adminUser.getFamilyName()).build()); + } + } + return item; + } + + protected static Tenant fromAttributeValueMap(Map item) { Tenant tenant = null; if (item != null && !item.isEmpty()) { tenant = new Tenant(); @@ -422,7 +457,8 @@ public static Tenant fromAttributeValueMap(Map item) { } if (item.containsKey("created")) { try { - LocalDateTime created = LocalDateTime.parse(item.get("created").s(), DateTimeFormatter.ISO_DATE_TIME); + LocalDateTime created = LocalDateTime.parse(item.get("created").s(), + DateTimeFormatter.ISO_DATE_TIME); tenant.setCreated(created); } catch (DateTimeParseException e) { LOGGER.error("Failed to parse created date from database: " + item.get("created").s()); @@ -431,7 +467,8 @@ public static Tenant fromAttributeValueMap(Map item) { } if (item.containsKey("modified")) { try { - LocalDateTime modified = LocalDateTime.parse(item.get("modified").s(), DateTimeFormatter.ISO_DATE_TIME); + LocalDateTime modified = LocalDateTime.parse(item.get("modified").s(), + DateTimeFormatter.ISO_DATE_TIME); tenant.setModified(modified); } catch (DateTimeParseException e) { LOGGER.error("Failed to parse created date from database: " + item.get("modified").s()); @@ -456,9 +493,6 @@ public static Tenant fromAttributeValueMap(Map item) { if (item.containsKey("subdomain")) { tenant.setSubdomain(item.get("subdomain").s()); } - if (item.containsKey("billing_plan")) { - tenant.setBillingPlan(item.get("billing_plan").s()); - } if (item.containsKey("attributes")) { try { tenant.setAttributes(item.get("attributes").m().entrySet() @@ -489,10 +523,55 @@ public static Tenant fromAttributeValueMap(Map item) { LOGGER.error(Utils.getFullStackTrace(e)); } } + if (item.containsKey("admin_users")) { + try { + tenant.setAdminUsers(item.get("admin_users").l().stream() + .map(entry -> fromAdminUserAttributeValueMap(entry.m())) + .collect(Collectors.toSet()) + ); + } catch (Exception e) { + LOGGER.error("Failed to parse admin users from database: {}", item.get("admin_users").l()); + LOGGER.error(Utils.getFullStackTrace(e)); + } + } } return tenant; } + protected static TenantAdminUser fromAdminUserAttributeValueMap(Map item) { + TenantAdminUser adminUser = null; + if (item != null && !item.isEmpty()) { + String username = null; + String email = null; + String phoneNumber = null; + String givenName = null; + String familyName = null; + if (item.containsKey("username")) { + username = item.get("username").s(); + } + if (item.containsKey("email")) { + email = item.get("email").s(); + } + if (item.containsKey("phone_number")) { + phoneNumber = item.get("phone_number").s(); + } + if (item.containsKey("given_name")) { + givenName = item.get("given_name").s(); + } + if (item.containsKey("family_name")) { + familyName = item.get("family_name").s(); + } + adminUser = TenantAdminUser.builder() + .username(username) + .email(email) + .phoneNumber(phoneNumber) + .givenName(givenName) + .familyName(familyName) + .build(); + } + return adminUser; + } + protected static String mapAttributeExpressionName(String keyName) { if (Utils.isBlank(keyName)) { throw new IllegalArgumentException("Missing arguments"); diff --git a/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantEvent.java b/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantEvent.java index a12cc368..ef2788dc 100644 --- a/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantEvent.java +++ b/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantEvent.java @@ -21,13 +21,13 @@ // TODO Make a marker interface of SaaSBoostEvent? public enum TenantEvent { - TENANT_ONBOARDING_STATUS_CHANGED("Tenant Onboarding Status Changed"), - TENANT_RESOURCES_CHANGED("Tenant Resources Changed"), - TENANT_HOSTNAME_CHANGED("Tenant Hostname Changed"), + TENANT_ONBOARDING_STATUS_CHANGED("Tenant Onboarding Status Changed"), // Consume + TENANT_RESOURCES_CHANGED("Tenant Resources Changed"), // Consume + TENANT_HOSTNAME_CHANGED("Tenant Hostname Changed"), // Consume TENANT_TIER_CHANGED("Tenant Tier Changed"), - TENANT_ENABLED("Tenant Enabled"), - TENANT_DISABLED("Tenant Disabled"), - TENANT_DELETED("Tenant Deleted") + TENANT_ENABLED("Tenant Enabled"), // Produce + TENANT_DISABLED("Tenant Disabled"), // Produce + TENANT_DELETED("Tenant Deleted") // Produce ; private final String detailType; diff --git a/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantService.java b/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantService.java index 29e1c674..47ef14e3 100644 --- a/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantService.java +++ b/services/tenant-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TenantService.java @@ -17,50 +17,63 @@ package com.amazon.aws.partners.saasfactory.saasboost; import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.eventbridge.EventBridgeClient; +import java.net.HttpURLConnection; import java.util.*; import java.util.stream.Collectors; -public class TenantService implements RequestHandler, APIGatewayProxyResponseEvent> { +public class TenantService { private static final Logger LOGGER = LoggerFactory.getLogger(TenantService.class); private static final Map CORS = Map.of("Access-Control-Allow-Origin", "*"); + private static final String TENANTS_TABLE = System.getenv("TENANTS_TABLE"); private static final String SAAS_BOOST_EVENT_BUS = System.getenv("SAAS_BOOST_EVENT_BUS"); private static final String EVENT_SOURCE = "saas-boost"; - private final TenantServiceDAL dal; + private final TenantDataAccessLayer dal; private final EventBridgeClient eventBridge; public TenantService() { + this(new DefaultDependencyFactory()); + } + + // Facilitates testing by being able to mock out AWS SDK dependencies + public TenantService(TenantServiceDependencyFactory init) { if (Utils.isBlank(SAAS_BOOST_EVENT_BUS)) { - throw new IllegalStateException("Missing required environment variable TENANTS_TABLE"); + throw new IllegalStateException("Missing required environment variable SAAS_BOOST_EVENT_BUS"); + } + if (Utils.isBlank(TENANTS_TABLE)) { + throw new IllegalStateException("Missing environment variable TENANTS_TABLE"); } LOGGER.info("Version Info: {}", Utils.version(this.getClass())); - this.dal = new TenantServiceDAL(); - this.eventBridge = Utils.sdkClient(EventBridgeClient.builder(), EventBridgeClient.SERVICE_NAME); - } - - @Override - public APIGatewayProxyResponseEvent handleRequest(Map event, Context context) { - //logRequestEvent(event); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + this.dal = init.dal(); + this.eventBridge = init.eventBridge(); } - public APIGatewayProxyResponseEvent getTenants(Map event, Context context) { + /** + * Get tenants. Integration for GET /tenants endpoint. + * Can be filtered to search for only provisioned or onboarded tenants + * using GET /tenants?status={provisioned|onboarded} + * @param event API Gateway proxy request event + * @param context + * @return List of tier objects + */ + public APIGatewayProxyResponseEvent getTenants(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } final long startTimeMillis = System.currentTimeMillis(); LOGGER.info("TenantService::getTenants"); //Utils.logRequestEvent(event); List tenants = new ArrayList<>(); - Map queryParams = (Map) event.get("queryStringParameters"); + Map queryParams = event.getQueryStringParameters(); if (queryParams != null && queryParams.containsKey("status")) { if ("provisioned".equalsIgnoreCase(queryParams.get("status"))) { tenants.addAll(dal.getProvisionedTenants()); @@ -71,7 +84,7 @@ public APIGatewayProxyResponseEvent getTenants(Map event, Contex tenants.addAll(dal.getAllTenants()); } APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() - .withStatusCode(200) + .withStatusCode(HttpURLConnection.HTTP_OK) .withHeaders(CORS) .withBody(Utils.toJson(tenants)); long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; @@ -79,54 +92,69 @@ public APIGatewayProxyResponseEvent getTenants(Map event, Contex return response; } - public APIGatewayProxyResponseEvent getTenant(Map event, Context context) { + /** + * Get tenant by id. Integration for GET /tenants/{id} endpoint. + * @param event API Gateway proxy request event containing an id path parameter + * @param context + * @return Tenant object for id or HTTP 404 if not found + */ + public APIGatewayProxyResponseEvent getTenant(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } final long startTimeMillis = System.currentTimeMillis(); LOGGER.info("TenantService::getTenant"); //Utils.logRequestEvent(event); APIGatewayProxyResponseEvent response = null; - Map params = (Map) event.get("pathParameters"); + Map params = event.getPathParameters(); String tenantId = params.get("id"); Tenant tenant = dal.getTenant(tenantId); if (tenant != null) { response = new APIGatewayProxyResponseEvent() - .withStatusCode(200) + .withStatusCode(HttpURLConnection.HTTP_OK) .withHeaders(CORS) .withBody(Utils.toJson(tenant)); } else { - response = new APIGatewayProxyResponseEvent().withStatusCode(404).withHeaders(CORS); + response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_NOT_FOUND) + .withHeaders(CORS); } long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; LOGGER.info("TenantService::getTenant exec {}", totalTimeMillis); return response; } - public APIGatewayProxyResponseEvent updateTenant(Map event, Context context) { + /** + * Update a tenant by id. Integration for PUT /tenants/{id} endpoint. + * @param event API Gateway proxy request event containing an id path parameter + * @param context + * @return HTTP 200 if updated, HTTP 400 on failure + */ + public APIGatewayProxyResponseEvent updateTenant(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } final long startTimeMillis = System.currentTimeMillis(); LOGGER.info("TenantService::updateTenant"); Utils.logRequestEvent(event); APIGatewayProxyResponseEvent response = null; - Map params = (Map) event.get("pathParameters"); + Map params = event.getPathParameters(); String tenantId = params.get("id"); - Tenant tenant = Utils.fromJson((String) event.get("body"), Tenant.class); + Tenant tenant = Utils.fromJson(event.getBody(), Tenant.class); if (tenant == null) { response = new APIGatewayProxyResponseEvent() - .withStatusCode(400) - .withHeaders(CORS); + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) + .withHeaders(CORS) + .withBody("{\"message\": \"Invalid request body\"}"); } else { if (tenant.getId() == null || !tenant.getId().toString().equals(tenantId)) { LOGGER.error("Can't update tenant {} at resource {}", tenant.getId(), tenantId); response = new APIGatewayProxyResponseEvent() - .withStatusCode(400) + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) .withHeaders(CORS); } else { Tenant existing = dal.getTenant(tenantId); @@ -139,7 +167,7 @@ public APIGatewayProxyResponseEvent updateTenant(Map event, Cont } response = new APIGatewayProxyResponseEvent() - .withStatusCode(200) + .withStatusCode(HttpURLConnection.HTTP_OK) .withHeaders(CORS) .withBody(Utils.toJson(tenant)); } @@ -149,29 +177,32 @@ public APIGatewayProxyResponseEvent updateTenant(Map event, Cont return response; } - public APIGatewayProxyResponseEvent enableTenant(Map event, Context context) { + public APIGatewayProxyResponseEvent enableTenant(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } final long startTimeMillis = System.currentTimeMillis(); LOGGER.info("TenantService::enableTenant"); //Utils.logRequestEvent(event); - APIGatewayProxyResponseEvent response = null; - Map params = (Map) event.get("pathParameters"); + APIGatewayProxyResponseEvent response; + Map params = event.getPathParameters(); String tenantId = params.get("id"); LOGGER.info("TenantService::enableTenant " + tenantId); if (tenantId == null || tenantId.isEmpty()) { - response = new APIGatewayProxyResponseEvent().withStatusCode(400).withHeaders(CORS); + response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) + .withHeaders(CORS); } else { Tenant tenant = dal.enableTenant(tenantId); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, "Tenant Enabled", + Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, + TenantEvent.TENANT_ENABLED.detailType(), Map.of("tenantId", tenantId)); response = new APIGatewayProxyResponseEvent() - .withStatusCode(200) + .withStatusCode(HttpURLConnection.HTTP_OK) .withHeaders(CORS) .withBody(Utils.toJson(tenant)); } @@ -180,29 +211,32 @@ public APIGatewayProxyResponseEvent enableTenant(Map event, Cont return response; } - public APIGatewayProxyResponseEvent disableTenant(Map event, Context context) { + public APIGatewayProxyResponseEvent disableTenant(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } final long startTimeMillis = System.currentTimeMillis(); LOGGER.info("TenantService::disableTenant"); //Utils.logRequestEvent(event); - APIGatewayProxyResponseEvent response = null; - Map params = (Map) event.get("pathParameters"); + APIGatewayProxyResponseEvent response; + Map params = event.getPathParameters(); String tenantId = params.get("id"); LOGGER.info("TenantService::disableTenant " + tenantId); if (tenantId == null || tenantId.isEmpty()) { - response = new APIGatewayProxyResponseEvent().withStatusCode(400).withHeaders(CORS); + response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) + .withHeaders(CORS); } else { Tenant tenant = dal.disableTenant(tenantId); - Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, "Tenant Disabled", + Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, + TenantEvent.TENANT_DISABLED.detailType(), Map.of("tenantId", tenantId)); response = new APIGatewayProxyResponseEvent() - .withStatusCode(200) + .withStatusCode(HttpURLConnection.HTTP_OK) .withHeaders(CORS) .withBody(Utils.toJson(tenant)); } @@ -211,25 +245,27 @@ public APIGatewayProxyResponseEvent disableTenant(Map event, Con return response; } - public APIGatewayProxyResponseEvent insertTenant(Map event, Context context) { + /** + * Inserts a new tenant. Integration for POST /tenants endpoint + * @param event API Gateway proxy request event containing a Tenant object in the request body + * @param context + * @return Tenant object in a created state or HTTP 400 if the request does not contain a name and tier + */ + public APIGatewayProxyResponseEvent insertTenant(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } final long startTimeMillis = System.currentTimeMillis(); LOGGER.info("TenantService::insertTenant"); Utils.logRequestEvent(event); - APIGatewayProxyResponseEvent response = null; + APIGatewayProxyResponseEvent response; - Tenant tenant = null; - // Were we called from Step Functions or API Gateway? - if (event.containsKey("body")) { - tenant = Utils.fromJson((String) event.get("body"), Tenant.class); - } + Tenant tenant = Utils.fromJson(event.getBody(), Tenant.class); if (tenant == null) { response = new APIGatewayProxyResponseEvent() - .withStatusCode(400) + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) .withHeaders(CORS) .withBody("{\"message\": \"Invalid request body\"}"); } else { @@ -238,7 +274,7 @@ public APIGatewayProxyResponseEvent insertTenant(Map event, Cont LOGGER.info("TenantService::insertTenant tenant id {}", tenant.getId().toString()); response = new APIGatewayProxyResponseEvent() - .withStatusCode(200) + .withStatusCode(HttpURLConnection.HTTP_CREATED) .withHeaders(CORS) .withBody(Utils.toJson(tenant)); } @@ -247,29 +283,35 @@ public APIGatewayProxyResponseEvent insertTenant(Map event, Cont return response; } - public APIGatewayProxyResponseEvent deleteTenant(Map event, Context context) { + /** + * Delete a tenant by id. Integration for DELETE /tenant/{id} endpoint. + * @param event API Gateway proxy request event containing an id path parameter + * @param context + * @return HTTP 204 if deleted, HTTP 400 on failure + */ + public APIGatewayProxyResponseEvent deleteTenant(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } final long startTimeMillis = System.currentTimeMillis(); LOGGER.info("TenantService::deleteTenant"); //Utils.logRequestEvent(event); APIGatewayProxyResponseEvent response = null; - Map params = (Map) event.get("pathParameters"); + Map params = event.getPathParameters(); String tenantId = params.get("id"); - Tenant tenant = Utils.fromJson((String) event.get("body"), Tenant.class); + Tenant tenant = Utils.fromJson(event.getBody(), Tenant.class); if (tenant == null) { response = new APIGatewayProxyResponseEvent() - .withStatusCode(400) + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) .withHeaders(CORS) .withBody("{\"message\": \"Invalid request body\"}"); } else { if (tenant.getId() == null || !tenant.getId().toString().equals(tenantId)) { LOGGER.error("Can't delete tenant {} at resource {}", tenant.getId(), tenantId); response = new APIGatewayProxyResponseEvent() - .withStatusCode(400) + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) .withHeaders(CORS) .withBody("{\"message\": \"Invalid request for specified resource\"}"); } else { @@ -277,7 +319,7 @@ public APIGatewayProxyResponseEvent deleteTenant(Map event, Cont //dal.deleteTenant(tenantId); response = new APIGatewayProxyResponseEvent() .withHeaders(CORS) - .withStatusCode(200); + .withStatusCode(HttpURLConnection.HTTP_NO_CONTENT); Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, TenantEvent.TENANT_DELETED.detailType(), @@ -306,6 +348,8 @@ public void handleTenantEvent(Map event, Context context) { LOGGER.info("Handling Tenant Resources Changed"); handleTenantResourcesChanged(event, context); break; + default: + LOGGER.error("Unknown Tenant Event!"); } } else { LOGGER.error("Can't find tenant event for detail-type {}", event.get("detail-type")); @@ -317,6 +361,7 @@ public void handleTenantEvent(Map event, Context context) { } } + // Keep track of where the tenant is in the onboarding flow protected void handleTenantOnboardingStatusChanged(Map event, Context context) { //Utils.logRequestEvent(event); if (TenantEvent.validate(event, "onboardingStatus")) { @@ -353,6 +398,7 @@ protected void handleTenantHostnameChanged(Map event, Context co } } + // Provisioned infra resources for this tenant have changed protected void handleTenantResourcesChanged(Map event, Context context) { Utils.logRequestEvent(event); if (TenantEvent.validate(event, "resources")) { @@ -391,4 +437,24 @@ protected static Map fromTenantResourcesChangedEvent(Ma return null; } + interface TenantServiceDependencyFactory { + + TenantDataAccessLayer dal(); + + EventBridgeClient eventBridge(); + } + + private static final class DefaultDependencyFactory implements TenantServiceDependencyFactory { + + @Override + public TenantDataAccessLayer dal() { + return new TenantDataAccessLayer(Utils.sdkClient(DynamoDbClient.builder(), DynamoDbClient.SERVICE_NAME), + TENANTS_TABLE); + } + + @Override + public EventBridgeClient eventBridge() { + return Utils.sdkClient(EventBridgeClient.builder(), EventBridgeClient.SERVICE_NAME); + } + } } \ No newline at end of file diff --git a/services/tenant-service/src/main/resources/spotbugs-exclude.xml b/services/tenant-service/src/main/resources/spotbugs-exclude.xml new file mode 100644 index 00000000..1975662a --- /dev/null +++ b/services/tenant-service/src/main/resources/spotbugs-exclude.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/services/tenant-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TenantServiceDALTest.java b/services/tenant-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TenantDataAccessLayerTest.java similarity index 74% rename from services/tenant-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TenantServiceDALTest.java rename to services/tenant-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TenantDataAccessLayerTest.java index ed82069d..94f252b8 100644 --- a/services/tenant-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TenantServiceDALTest.java +++ b/services/tenant-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TenantDataAccessLayerTest.java @@ -17,29 +17,30 @@ package com.amazon.aws.partners.saasfactory.saasboost; import com.fasterxml.jackson.annotation.JsonProperty; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; -import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; -public class TenantServiceDALTest { +public class TenantDataAccessLayerTest { private static UUID tenantId; private static HashMap attributes; private static HashMap resources; + private static TenantAdminUser adminUser; + private static Set users; - @BeforeClass + @BeforeAll public static void setup() throws Exception { tenantId = UUID.fromString("d1c1e3cc-962f-4f03-b4a8-d8a7c1f986c3"); @@ -56,6 +57,16 @@ public static void setup() throws Exception { resources.put("PRIVATE_SUBNET_A", new Tenant.Resource("subnet-03a78eb00d87a0bbf", "arn:aws:ec2:us-east-1:111111111:subnet/subnet-03a78eb00d87a0bbf", "https://us-east-1.console.aws.amazon.com/vpc/home?region=us-east-1#SubnetDetails:subnetId=subnet-03a78eb00d87a0bbf")); + + adminUser = TenantAdminUser.builder() + .username("username") + .email("user@tenant.com") + .phoneNumber("800-555-1212") + .givenName("user") + .familyName("tenant one") + .build(); + users = Set.of(adminUser); + //users = new HashSet<>(); } @Test @@ -71,11 +82,11 @@ public void testToAttributeValueMap() { tenant.setTier("default"); tenant.setName("Test Tenant"); tenant.setOnboardingStatus("succeeded"); - tenant.setBillingPlan("Billing Plan"); tenant.setHostname("test-tenant.saas-example.com"); tenant.setSubdomain("test-tenant"); tenant.setAttributes(attributes); tenant.setResources(resources); + tenant.setAdminUsers(users); Map expected = new HashMap<>(); expected.put("id", AttributeValue.builder().s(tenantId.toString()).build()); @@ -87,7 +98,6 @@ public void testToAttributeValueMap() { expected.put("onboarding_status", AttributeValue.builder().s("succeeded").build()); expected.put("hostname", AttributeValue.builder().s("test-tenant.saas-example.com").build()); expected.put("subdomain", AttributeValue.builder().s("test-tenant").build()); - expected.put("billing_plan", AttributeValue.builder().s("Billing Plan").build()); expected.put("attributes", AttributeValue.builder().m(attributes.entrySet() .stream() .collect(Collectors.toMap( @@ -109,13 +119,25 @@ public void testToAttributeValueMap() { )).build() )) ).build()); - - Map actual = TenantServiceDAL.toAttributeValueMap(tenant); + expected.put("admin_users", AttributeValue.builder().l(users.stream() + .map(user -> AttributeValue.builder().m( + Map.of( + "username", AttributeValue.builder().s(user.getUsername()).build(), + "email", AttributeValue.builder().s(user.getEmail()).build(), + "phone_number", AttributeValue.builder().s(user.getPhoneNumber()).build(), + "given_name", AttributeValue.builder().s(user.getGivenName()).build(), + "family_name", AttributeValue.builder().s(user.getFamilyName()).build() + ) + ).build()) + .collect(Collectors.toSet())).build() + ); + + Map actual = TenantDataAccessLayer.toAttributeValueMap(tenant); // DynamoDB marshalling - assertEquals("Size unequal", expected.size(), actual.size()); + assertEquals(expected.size(), actual.size(), "Size unequal"); expected.keySet().stream().forEach(key -> { - assertEquals("Value mismatch for '" + key + "'", expected.get(key), actual.get(key)); + assertEquals(expected.get(key), actual.get(key), "Value mismatch for '" + key + "'"); }); // Ignore read only properties from JSON serialization @@ -139,28 +161,28 @@ public void testToAttributeValueMap() { .filter(key -> !ignoreProperties.contains(key)) .map(key -> Utils.toSnakeCase(key)) .forEach(key -> { - assertTrue("Class property '" + key + "' does not exist in DynamoDB attribute map", actual.containsKey(key)); + assertTrue(actual.containsKey(key), "Class property '" + key + "' does not exist in DynamoDB attribute map"); }); } @Test public void testMapAttributeExpressionName() { - assertThrows(IllegalArgumentException.class, () -> TenantServiceDAL.mapAttributeExpressionName(null)); - assertThrows(IllegalArgumentException.class, () -> TenantServiceDAL.mapAttributeExpressionName("")); - assertThrows(IllegalArgumentException.class, () -> TenantServiceDAL.mapAttributeExpressionName(" ")); + assertThrows(IllegalArgumentException.class, () -> TenantDataAccessLayer.mapAttributeExpressionName(null)); + assertThrows(IllegalArgumentException.class, () -> TenantDataAccessLayer.mapAttributeExpressionName("")); + assertThrows(IllegalArgumentException.class, () -> TenantDataAccessLayer.mapAttributeExpressionName(" ")); - assertEquals("#ECS_CLUSTER", TenantServiceDAL.mapAttributeExpressionName("ECS_CLUSTER")); + assertEquals(TenantDataAccessLayer.mapAttributeExpressionName("ECS_CLUSTER"), "#ECS_CLUSTER"); } @Test public void testMapAttributeExpressionValue() { - assertEquals(":PRIVATE_SUBNET_A", - TenantServiceDAL.mapAttributeExpressionValue("PRIVATE_SUBNET_A")); + assertEquals(TenantDataAccessLayer.mapAttributeExpressionValue("PRIVATE_SUBNET_A"), + ":PRIVATE_SUBNET_A"); } @Test public void testMapAttributeUpdateExpression() { - assertEquals("resources.#VPC = :VPC", - TenantServiceDAL.mapAttributeUpdateExpression("resources", "VPC", "VPC")); + assertEquals(TenantDataAccessLayer.mapAttributeUpdateExpression("resources", "VPC", "VPC"), + "resources.#VPC = :VPC"); } } diff --git a/services/tenant-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TenantServiceTest.java b/services/tenant-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TenantServiceTest.java index a98d32f5..a1d15f61 100644 --- a/services/tenant-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TenantServiceTest.java +++ b/services/tenant-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TenantServiceTest.java @@ -16,11 +16,12 @@ package com.amazon.aws.partners.saasfactory.saasboost; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + import java.util.*; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class TenantServiceTest { @@ -29,7 +30,7 @@ public class TenantServiceTest { private static Map event; private static Map eventDetail; - @BeforeClass + @BeforeAll public static void setup() throws Exception { tenantId = "d1c1e3cc-962f-4f03-b4a8-d8a7c1f986c3"; @@ -66,9 +67,9 @@ public void testFromTenantResourcesChangedEvent() { Map expected = resources; Map actual = TenantService.fromTenantResourcesChangedEvent(event); - assertEquals("Size unequal", expected.size(), actual.size()); + assertEquals(expected.size(), actual.size(), "Size unequal"); expected.keySet().stream().forEach((key) -> { - assertEquals("Value mismatch for '" + key + "'", expected.get(key), actual.get(key)); + assertEquals(expected.get(key), actual.get(key), "Value mismatch for '" + key + "'"); }); } diff --git a/services/tenant-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TenantTest.java b/services/tenant-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TenantTest.java index b20f2de2..03cac8e0 100644 --- a/services/tenant-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TenantTest.java +++ b/services/tenant-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TenantTest.java @@ -16,36 +16,35 @@ package com.amazon.aws.partners.saasfactory.saasboost; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.Collection; import java.util.UUID; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class TenantTest { @Test public void testIsProvisioned() { Tenant tenant = new Tenant(); - assertFalse("Null onboarding status tenants are not provisioned", tenant.isProvisioned()); + assertFalse(tenant.isProvisioned(), "Null onboarding status tenants are not provisioned"); - Collection provisionedStates = Arrays.asList("created", "validating", "validated", "provisioning", - "provisioned", "updating", "updated", "deploying", "deployed"); - Collection unProvisionedStates = Arrays.asList("failed", "deleting", "deleted"); + Collection unProvisionedStates = Arrays.asList("created", "validating", "validated", + "provisioning", "failed", "deleting", "deleted", "uknown"); - for (String onboardingStatus : provisionedStates) { + for (String onboardingStatus : Tenant.PROVISIONED_STATES) { tenant.setOnboardingStatus(onboardingStatus); - assertTrue(onboardingStatus + " tenants are provisioned", tenant.isProvisioned()); - assertTrue("Serialized tenant has provisioned property", - Utils.toJson(tenant).contains("\"provisioned\":true")); + assertTrue(tenant.isProvisioned(), onboardingStatus + " tenants are provisioned"); + assertTrue(Utils.toJson(tenant).contains("\"provisioned\":true"), + "Serialized tenant has provisioned property"); } for (String onboardingStatus : unProvisionedStates) { tenant.setOnboardingStatus(onboardingStatus); - assertFalse(onboardingStatus + " tenants are not provisioned", tenant.isProvisioned()); - assertTrue("Serialized tenant has provisioned property", - Utils.toJson(tenant).contains("\"provisioned\":false")); + assertFalse(tenant.isProvisioned(), onboardingStatus + " tenants are not provisioned"); + assertTrue(Utils.toJson(tenant).contains("\"provisioned\":false"), + "Serialized tenant has provisioned property"); } String json = "{\"id\":\"" + UUID.randomUUID() + "\"" @@ -53,8 +52,8 @@ public void testIsProvisioned() { + ", \"name\":\"Unit Test\"" + ", \"provisioned\":true" + "}"; - assertFalse("Deserialized tenant doesn't write provisioned", - Utils.fromJson(json, Tenant.class).isProvisioned()); + assertFalse(Utils.fromJson(json, Tenant.class).isProvisioned(), + "Deserialized tenant doesn't write provisioned"); json = "{\"id\":\"" + UUID.randomUUID() + "\"" + ", \"active\":true" @@ -62,8 +61,8 @@ public void testIsProvisioned() { + ", \"provisioned\":false" + ", \"onboardingStatus\": \"deployed\"" + "}"; - assertTrue("Deserialized tenant doesn't write provisioned", - Utils.fromJson(json, Tenant.class).isProvisioned()); + assertTrue(Utils.fromJson(json, Tenant.class).isProvisioned(), + "Deserialized tenant doesn't write provisioned"); } } diff --git a/services/tenant-service/src/test/resources/log4j2.xml b/services/tenant-service/src/test/resources/log4j2.xml deleted file mode 100644 index bbb786ea..00000000 --- a/services/tenant-service/src/test/resources/log4j2.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} %X{AWSRequestId} %-5p %C{1} - %m%n - - - - - - - - - - - - \ No newline at end of file diff --git a/services/tenant-service/src/test/resources/logging.properties b/services/tenant-service/src/test/resources/logging.properties deleted file mode 100644 index c6a1a092..00000000 --- a/services/tenant-service/src/test/resources/logging.properties +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -handlers=java.util.logging.ConsoleHandler -java.util.logging.ConsoleHandler.level=FINEST -sun.net.www.protocol.http.HttpURLConnection.level=ALL \ No newline at end of file diff --git a/services/tier-service/pom.xml b/services/tier-service/pom.xml index 040bdd26..2a7eceeb 100644 --- a/services/tier-service/pom.xml +++ b/services/tier-service/pom.xml @@ -33,10 +33,77 @@ limitations under the License. + ${project.basedir}/../.. 0 + + ${project.artifactId} + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + us-east-1 + test + test + + + + + org.jacoco + jacoco-maven-plugin + + + org.apache.maven.plugins + maven-assembly-plugin + + + io.github.git-commit-id + git-commit-id-maven-plugin + + + com.github.spotbugs + spotbugs-maven-plugin + + Max + medium + + + software.amazon.lambda.snapstart + aws-lambda-snapstart-java-rules + 0.1.0 + + + + + + + + + com.amazonaws + aws-lambda-java-tests + 1.1.1 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.9.3 + test + + + org.junit.jupiter + junit-jupiter-api + 5.9.3 + test + com.amazon.aws.partners.saasfactory.saasboost Utils @@ -61,25 +128,4 @@ limitations under the License. - - ${project.artifactId} - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - org.apache.maven.plugins - maven-assembly-plugin - - - io.github.git-commit-id - git-commit-id-maven-plugin - - - diff --git a/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Tier.java b/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Tier.java new file mode 100644 index 00000000..b9b8fa4c --- /dev/null +++ b/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Tier.java @@ -0,0 +1,115 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.UUID; + +public final class Tier { + + private UUID id; + private LocalDateTime created; + private LocalDateTime modified; + private String name; + private String description; + private boolean defaultTier; + private String billingPlan; + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + final Tier other = (Tier) obj; + return ( + Objects.equals(id, other.id) + && Objects.equals(name, other.name) + && Objects.equals(created, other.created) + && Objects.equals(modified, other.modified) + && Objects.equals(description, other.description) + && Objects.equals(billingPlan, other.billingPlan) + && defaultTier == other.defaultTier); + } + + @Override + public int hashCode() { + return Objects.hash(id, created, modified, name, description, billingPlan, defaultTier); + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public LocalDateTime getCreated() { + return created; + } + + public void setCreated(LocalDateTime created) { + this.created = created; + } + + public LocalDateTime getModified() { + return modified; + } + + public void setModified(LocalDateTime modified) { + this.modified = modified; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getBillingPlan() { + return billingPlan; + } + + public void setBillingPlan(String billingPlan) { + this.billingPlan = billingPlan; + } + + public boolean isDefaultTier() { + return defaultTier; + } + + public void setDefaultTier(boolean defaultTier) { + this.defaultTier = defaultTier; + } +} diff --git a/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TierDataAccessLayer.java b/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TierDataAccessLayer.java new file mode 100644 index 00000000..1e0dbf67 --- /dev/null +++ b/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TierDataAccessLayer.java @@ -0,0 +1,233 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.*; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.*; + +public class TierDataAccessLayer { + + private static final Logger LOGGER = LoggerFactory.getLogger(TierDataAccessLayer.class); + private final String tiersTable; + private final DynamoDbClient ddb; + + public TierDataAccessLayer(DynamoDbClient ddb, String tiersTable) { + final long startTimeMillis = System.currentTimeMillis(); + this.ddb = ddb; + this.tiersTable = tiersTable; + // Cold start performance hack -- take the TLS hit for the client in the constructor + this.ddb.describeTable(r -> r.tableName(this.tiersTable)); + LOGGER.info("Constructor init: {}", System.currentTimeMillis() - startTimeMillis); + } + + public List getTiers() { + List tiers = new ArrayList<>(); + try { + ScanResponse response = ddb.scan(request -> request.tableName(tiersTable)); + response.items().forEach(item -> + tiers.add(fromAttributeValueMap(item)) + ); + } catch (DynamoDbException e) { + LOGGER.error(e.awsErrorDetails().errorMessage()); + LOGGER.error(Utils.getFullStackTrace(e)); + throw e; + } + return tiers; + } + + public Tier getTier(UUID tierId) { + return getTier(tierId.toString()); + } + + public Tier getTier(String tierId) { + Map item; + try { + Map key = new HashMap<>(); + key.put("id", AttributeValue.builder().s(tierId).build()); + GetItemResponse response = ddb.getItem(request -> request.tableName(tiersTable).key(key)); + item = response.item(); + } catch (DynamoDbException e) { + LOGGER.error(e.awsErrorDetails().errorMessage()); + LOGGER.error(Utils.getFullStackTrace(e)); + throw e; + } + Tier tier = fromAttributeValueMap(item); + return tier; + } + + public Tier getTierByName(String name) { + Map item = null; + try { + ScanResponse scan = ddb.scan(ScanRequest.builder() + .tableName(tiersTable) + .filterExpression("#name = :name") + .expressionAttributeNames(Map.of("#name", "name")) + .expressionAttributeValues( + Map.of(":name", AttributeValue.builder().s(name).build()) + ) + .build() + ); + if (1 == scan.items().size()) { + LOGGER.info("Scanning tiers for tier name {}", name); + item = scan.items().get(0); + } else { + LOGGER.error("Tiers scan for name " + name + " returned " + + scan.items().size() + " results"); + } + } catch (DynamoDbException e) { + LOGGER.error(e.awsErrorDetails().errorMessage()); + LOGGER.error(Utils.getFullStackTrace(e)); + throw e; + } + Tier tier = fromAttributeValueMap(item); + return tier; + } + + // Choosing to do a replacement update as you might do in a RDBMS by + // setting columns = NULL when they do not exist in the updated value + public Tier updateTier(Tier tier) { + try { + // Created and Modified are owned by the DAL since they reflect when the + // object was persisted + tier.setModified(LocalDateTime.now()); + Map item = toAttributeValueMap(tier); + ddb.putItem(request -> request.tableName(tiersTable).item(item)); + } catch (DynamoDbException e) { + LOGGER.error(e.awsErrorDetails().errorMessage()); + LOGGER.error(Utils.getFullStackTrace(e)); + throw e; + } + return tier; + } + + public Tier insertTier(Tier tier) { + // Unique identifier is owned by the DAL + if (tier.getId() != null) { + throw new IllegalArgumentException("Can't insert a new tier that already has an id"); + } + UUID tierId = UUID.randomUUID(); + tier.setId(tierId); + + // Created and Modified are owned by the DAL since they reflect when the + // object was persisted + LocalDateTime now = LocalDateTime.now(); + tier.setCreated(now); + tier.setModified(now); + Map item = toAttributeValueMap(tier); + try { + ddb.putItem(request -> request.tableName(tiersTable).item(item)); + } catch (DynamoDbException e) { + LOGGER.error(e.awsErrorDetails().errorMessage()); + LOGGER.error(Utils.getFullStackTrace(e)); + throw e; + } + return tier; + } + + public Tier deleteTier(Tier tier) { + try { + ddb.deleteItem(request -> request + .tableName(tiersTable) + .key(Map.of("id", AttributeValue.builder().s(tier.getId().toString()).build())) + ); + } catch (DynamoDbException e) { + LOGGER.error(e.awsErrorDetails().errorMessage()); + LOGGER.error(Utils.getFullStackTrace(e)); + throw new RuntimeException(e); + } + return tier; + } + + public static Map toAttributeValueMap(Tier tier) { + Map item = new HashMap<>(); + item.put("id", AttributeValue.builder().s(tier.getId().toString()).build()); + item.put("default_tier", AttributeValue.builder().bool(tier.isDefaultTier()).build()); + if (tier.getCreated() != null) { + item.put("created", AttributeValue.builder().s( + tier.getCreated().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); + } + if (tier.getModified() != null) { + item.put("modified", AttributeValue.builder().s( + tier.getModified().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); + } + if (tier.getName() != null) { + item.put("name", AttributeValue.builder().s(tier.getName()).build()); + } + if (tier.getDescription() != null) { + item.put("description", AttributeValue.builder().s(tier.getDescription()).build()); + } + if (tier.getBillingPlan() != null) { + item.put("billing_plan", AttributeValue.builder().s(tier.getBillingPlan()).build()); + } + return item; + } + + public static Tier fromAttributeValueMap(Map item) { + Tier tier = null; + if (item != null && !item.isEmpty()) { + tier = new Tier(); + if (item.containsKey("id")) { + try { + tier.setId(UUID.fromString(item.get("id").s())); + } catch (IllegalArgumentException e) { + LOGGER.error("Failed to parse UUID from database: " + item.get("id").s()); + LOGGER.error(Utils.getFullStackTrace(e)); + } + } + if (item.containsKey("default_tier")) { + tier.setDefaultTier(item.get("default_tier").bool()); + } + if (item.containsKey("created")) { + try { + LocalDateTime created = LocalDateTime.parse(item.get("created").s(), + DateTimeFormatter.ISO_DATE_TIME); + tier.setCreated(created); + } catch (DateTimeParseException e) { + LOGGER.error("Failed to parse created date from database: " + item.get("created").s()); + LOGGER.error(Utils.getFullStackTrace(e)); + } + } + if (item.containsKey("modified")) { + try { + LocalDateTime created = LocalDateTime.parse(item.get("modified").s(), + DateTimeFormatter.ISO_DATE_TIME); + tier.setModified(created); + } catch (DateTimeParseException e) { + LOGGER.error("Failed to parse created date from database: " + item.get("modified").s()); + LOGGER.error(Utils.getFullStackTrace(e)); + } + } + if (item.containsKey("name")) { + tier.setName(item.get("name").s()); + } + if (item.containsKey("description")) { + tier.setDescription(item.get("description").s()); + } + if (item.containsKey("billing_plan")) { + tier.setBillingPlan(item.get("billing_plan").s()); + } + } + return tier; + } +} diff --git a/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TierService.java b/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TierService.java index bd766eb9..562788f3 100644 --- a/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TierService.java +++ b/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/TierService.java @@ -16,215 +16,263 @@ package com.amazon.aws.partners.saasfactory.saasboost; -import com.amazon.aws.partners.saasfactory.saasboost.dal.TierDataStore; -import com.amazon.aws.partners.saasfactory.saasboost.dal.ddb.DynamoTierDataStore; -import com.amazon.aws.partners.saasfactory.saasboost.dal.exception.TierNotFoundException; -import com.amazon.aws.partners.saasfactory.saasboost.model.Tier; import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import java.net.HttpURLConnection; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -public class TierService implements RequestHandler, APIGatewayProxyResponseEvent> { +public class TierService { private static final Logger LOGGER = LoggerFactory.getLogger(TierService.class); - private static final String TIERS_TABLE = System.getenv("TIERS_TABLE"); private static final Map CORS = Map.of("Access-Control-Allow-Origin", "*"); - - private final TierDataStore store; + private static final String TIERS_TABLE = System.getenv("TIERS_TABLE"); + private final TierDataAccessLayer dal; public TierService() { - final long startTimeMillis = System.currentTimeMillis(); - if (Utils.isEmpty(TIERS_TABLE)) { + this(new DefaultDependencyFactory()); + } + + // Facilitates testing by being able to mock out AWS SDK dependencies + public TierService(TierServiceDependencyFactory init) { + if (Utils.isBlank(TIERS_TABLE)) { throw new IllegalStateException("Missing environment variable TIERS_TABLE"); } LOGGER.info("Version Info: {}", Utils.version(this.getClass())); - - this.store = new DynamoTierDataStore( - Utils.sdkClient(DynamoDbClient.builder(), DynamoDbClient.SERVICE_NAME), - TIERS_TABLE); - - LOGGER.info("Constructor init: {}", System.currentTimeMillis() - startTimeMillis); - } - - @Override - public APIGatewayProxyResponseEvent handleRequest(Map event, Context context) { - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + this.dal = init.dal(); } - public APIGatewayProxyResponseEvent getTiers(Map event, Context context) { + /** + * Get tiers. Integration for GET /tiers endpoint. + * Can be filtered to search for tier by name using GET /tiers?name={name} + * @param event API Gateway proxy request event + * @param context + * @return List of tier objects + */ + public APIGatewayProxyResponseEvent getTiers(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } - final long startTimeMillis = System.currentTimeMillis(); - Utils.logRequestEvent(event); - List tiers = store.listTiers(); - if (tiers.isEmpty()) { - // we want to ensure there is always at least a default tier. - tiers.add( - store.createTier(Tier.builder() - .name("default") - .description("Default Tier") - .defaultTier(true) - .build() - ) - ); + Map params = event.getQueryStringParameters(); + if (params != null && params.containsKey("name")) { + Tier tier = dal.getTierByName(params.get("name")); + if (tier == null) { + return new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_NOT_FOUND); + } else { + return new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_OK) + .withBody(Utils.toJson(tier)); + } + } else { + List tiers = dal.getTiers(); + if (tiers.isEmpty()) { + // We want to ensure there is always at least a default tier. + Tier defaultTier = new Tier(); + defaultTier.setDefaultTier(true); + defaultTier.setName("default"); + defaultTier.setDescription("Default Tier"); + tiers.add(dal.insertTier(defaultTier)); + } + return new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_OK) + .withBody(Utils.toJson(tiers)); } - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("TierService::getTiers exec " + totalTimeMillis); - return new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(200) - .withBody(Utils.toJson(tiers)); } - public APIGatewayProxyResponseEvent getTier(Map event, Context context) { + /** + * Get tier by id. Integration for GET /tiers/{id} endpoint. + * @param event API Gateway proxy request event containing an id path parameter + * @param context + * @return Tier object for id or HTTP 404 if not found + */ + public APIGatewayProxyResponseEvent getTier(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + //LOGGER.info("Warming up"); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } - final long startTimeMillis = System.currentTimeMillis(); - Utils.logRequestEvent(event); - Map pathParams = (Map) event.get("pathParameters"); - Tier foundTier = null; - try { - foundTier = store.getTier(pathParams.get("id")); - } catch (TierNotFoundException tnfe) { - LOGGER.error("Tier not found", tnfe); - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("TierService::getTier exec " + totalTimeMillis); - return new APIGatewayProxyResponseEvent() + APIGatewayProxyResponseEvent response; + Map params = event.getPathParameters(); + String tierId = params.get("id"); + Tier tier = dal.getTier(tierId); + if (tier != null) { + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_OK) + .withBody(Utils.toJson(tier)); + } else { + response = new APIGatewayProxyResponseEvent() .withHeaders(CORS) - .withStatusCode(404) - .withBody("{\"message\":\"Tier not found.\"}"); + .withStatusCode(HttpURLConnection.HTTP_NOT_FOUND); } - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("TierService::getTier exec " + totalTimeMillis); - return new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(200) - .withBody(Utils.toJson(foundTier)); + + return response; } - public APIGatewayProxyResponseEvent createTier(Map event, Context context) { + /** + * Update a tier by id. Integration for PUT /tiers/{id} endpoint. + * @param event API Gateway proxy request event containing an id path parameter + * @param context + * @return HTTP 200 if updated, HTTP 400 on failure + */ + public APIGatewayProxyResponseEvent updateTier(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } - final long startTimeMillis = System.currentTimeMillis(); - APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent().withHeaders(CORS); - Utils.logRequestEvent(event); - Tier newTier = Utils.fromJson((String) event.get("body"), Tier.class); - if (newTier == null) { - // Utils.fromJson swallows and logs any exceptions coming from deserialization attempts - return response.withStatusCode(400).withBody("{\"message\":\"Body should represent a Tier.\"}"); - } - Tier createdTier = store.createTier(newTier); - if (createdTier.defaultTier()) { - enforceSingleDefaultTier(createdTier); + //Utils.logRequestEvent(event); + APIGatewayProxyResponseEvent response; + Map params = event.getPathParameters(); + String tierId = params.get("id"); + Tier tier = Utils.fromJson(event.getBody(), Tier.class); + if (tier == null) { + response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) + .withHeaders(CORS) + .withBody(Utils.toJson(Map.of("message", "Invalid request body"))); + } else { + if (tier.getId() == null || !tier.getId().toString().equals(tierId)) { + LOGGER.error("Can't update tier {} at resource {}", tier.getId(), tierId); + response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) + .withHeaders(CORS) + .withBody(Utils.toJson(Map.of("message", "Request body must include id"))); + } else { + Tier existing = dal.getTier(tierId); + if (existing == null) { + LOGGER.error("Can't update tier non-existent tier {}", tierId); + response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) + .withHeaders(CORS) + .withBody(Utils.toJson(Map.of("message", "No tier with id " + tierId))); + } else { + if (!existing.isDefaultTier() && tier.isDefaultTier()) { + // we weren't default but now we are, this means all other default + // Tiers should be updated to no longer be default, + // as we need to enforce only one default Tier at a given time + enforceSingleDefaultTier(tier); + } + tier = dal.updateTier(tier); + response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_OK) + .withHeaders(CORS) + .withBody(Utils.toJson(tier)); + } + } } - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("TierService::createTier exec " + totalTimeMillis); - return new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(200) - .withBody(Utils.toJson(createdTier)); + + return response; } - public APIGatewayProxyResponseEvent updateTier(Map event, Context context) { + /** + * Inserts a new tier. Integration for POST /tiers endpoint + * @param event API Gateway proxy request event containing a Tier object in the request body + * @param context + * @return Tier object in a created state or HTTP 400 if the request does not contain a name + */ + public APIGatewayProxyResponseEvent insertTier(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { - //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } - final long startTimeMillis = System.currentTimeMillis(); - Utils.logRequestEvent(event); - Tier providedTier = Utils.fromJson((String) event.get("body"), Tier.class); - if (providedTier == null) { - // Utils.fromJson swallows and logs any exceptions coming from deserialization attempts + Tier tier = Utils.fromJson(event.getBody(), Tier.class); + if (null == tier) { + LOGGER.error("Tier request is invalid"); return new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) .withHeaders(CORS) - .withStatusCode(400) - .withBody("{\"message\":\"Body should represent a Tier.\"}"); + .withBody("{\"message\": \"Invalid request body.\"}"); } - Map pathParams = (Map) event.get("pathParameters"); - if (!pathParams.get("id").equals(providedTier.getId())) { + if (Utils.isBlank(tier.getName())) { + LOGGER.error("Tier is missing name"); return new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) .withHeaders(CORS) - .withStatusCode(400) - .withBody("{\"message\":\"Tier IDs are immutable: body Tier ID must match path parameter.\"}"); + .withBody("{\"message\": \"Tier name is required.\"}"); } - Tier updatedTier = providedTier; - try { - Tier oldTier = store.getTier(providedTier.getId()); - // TODO validate that user isn't trying to update fields that should not be updated, e.g. created, id - updatedTier = store.updateTier(providedTier); - if (!oldTier.defaultTier() && updatedTier.defaultTier()) { - // we weren't default but now we are, this means all other default - // Tiers should be updated to no longer be default, - // as we need to enforce only one default Tier at a given time - enforceSingleDefaultTier(updatedTier); - } - } catch (TierNotFoundException tnfe) { - return new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(404) - .withBody("{\"message\":\"Tier not found.\"}"); + + tier = dal.insertTier(tier); + if (tier.isDefaultTier()) { + enforceSingleDefaultTier(tier); } - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("TierService::updateTier exec " + totalTimeMillis); return new APIGatewayProxyResponseEvent() .withHeaders(CORS) - .withStatusCode(200) - .withBody(Utils.toJson(updatedTier)); + .withStatusCode(HttpURLConnection.HTTP_CREATED) + .withBody(Utils.toJson(tier)); } - public APIGatewayProxyResponseEvent deleteTier(Map event, Context context) { + /** + * Delete a tier by id. Integration for DELETE /tier/{id} endpoint. + * @param event API Gateway proxy request event containing an id path parameter + * @param context + * @return HTTP 204 if deleted, HTTP 400 on failure + */ + public APIGatewayProxyResponseEvent deleteTier(APIGatewayProxyRequestEvent event, Context context) { if (Utils.warmup(event)) { //LOGGER.info("Warming up"); - return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200); + return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(HttpURLConnection.HTTP_OK); } - - final long startTimeMillis = System.currentTimeMillis(); - Utils.logRequestEvent(event); - Map pathParams = (Map) event.get("pathParameters"); - try { - store.deleteTier(pathParams.get("id")); - } catch (TierNotFoundException tnfe) { - return new APIGatewayProxyResponseEvent() + //Utils.logRequestEvent(event); + APIGatewayProxyResponseEvent response; + Map params = event.getPathParameters(); + String tierId = params.get("id"); + Tier tier = dal.getTier(tierId); + if (tier == null) { + response = new APIGatewayProxyResponseEvent() + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) .withHeaders(CORS) - .withStatusCode(404) - .withBody("{\"message\":\"Tier not found.\"}"); + .withBody(Utils.toJson(Map.of("message", "Invalid tier id"))); + } else { + try { + dal.deleteTier(tier); + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_NO_CONTENT); // No content + } catch (Exception e) { + response = new APIGatewayProxyResponseEvent() + .withHeaders(CORS) + .withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST) + .withBody(Utils.toJson(Map.of("message", "Failed to delete tier " + tierId))); + } } - long totalTimeMillis = System.currentTimeMillis() - startTimeMillis; - LOGGER.info("TierService::deleteTier exec " + totalTimeMillis); - return new APIGatewayProxyResponseEvent() - .withHeaders(CORS) - .withStatusCode(200); + return response; } public void enforceSingleDefaultTier(Tier defaultTier) { - List defaultTiers = store.listTiers().stream() - .filter(tier -> tier.defaultTier()) + List defaultTiers = dal.getTiers().stream() + .filter(tier -> tier.isDefaultTier()) .collect(Collectors.toList()); for (Tier t : defaultTiers) { if (!t.getId().equals(defaultTier.getId())) { - try { - // TODO in the event that multiple users try to update tiers at the same time, different - // TODO lambda invocations may step on each other. to get around this use DDB TransactWriteItems - store.updateTier(Tier.builder(t).defaultTier(false).build()); - } catch (TierNotFoundException tnfe) { - // race condition between the list we just pulled and the update - LOGGER.error("Could not enforce a single default tier." - + " Found {} default tiers but {} does not exist.", defaultTiers, t); - } + // TODO in the event that multiple users try to update tiers at the same time, different + // TODO lambda invocations may step on each other. to get around this use DDB TransactWriteItems + t.setDefaultTier(false); + dal.updateTier(t); } } } + + interface TierServiceDependencyFactory { + + TierDataAccessLayer dal(); + } + + private static final class DefaultDependencyFactory implements TierServiceDependencyFactory { + + @Override + public TierDataAccessLayer dal() { + return new TierDataAccessLayer(Utils.sdkClient(DynamoDbClient.builder(), DynamoDbClient.SERVICE_NAME), + TIERS_TABLE); + } + } } \ No newline at end of file diff --git a/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/dal/TierDataStore.java b/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/dal/TierDataStore.java deleted file mode 100644 index 7ac17024..00000000 --- a/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/dal/TierDataStore.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost.dal; - -import com.amazon.aws.partners.saasfactory.saasboost.TierService; -import com.amazon.aws.partners.saasfactory.saasboost.dal.exception.TierNotFoundException; -import com.amazon.aws.partners.saasfactory.saasboost.model.Tier; - -import java.util.List; - -/** - * TierDataStore represents the backing datastore required for the {@link TierService} to function. - * - * Implementations of this interface connect the DAL with the actual backing datastore and will need an adapter to - * convert between the model definition of {@link Tier} and the datastore required definition. - */ -public interface TierDataStore { - Tier getTier(String id); - - List listTiers(); - - Tier createTier(Tier tier); - - void deleteTier(String id); - - /** - * Updates the {@link Tier} with the same id as the provided Tier. - * - * @param newTier the Tier to update and the new data to supplant the old data - * @returns the updated Tier - * @throws TierNotFoundException if there is no Tier with that id - */ - Tier updateTier(Tier newTier); -} \ No newline at end of file diff --git a/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTier.java b/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTier.java deleted file mode 100644 index 9aa9f08b..00000000 --- a/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTier.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost.dal.ddb; - -import com.amazon.aws.partners.saasfactory.saasboost.model.Tier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class DynamoTier { - private static final Logger LOGGER = LoggerFactory.getLogger(DynamoTier.class); - private static final DynamoTierAttribute PRIMARY_KEY = DynamoTierAttribute.id; - public final Map attributes; - - private DynamoTier(Tier tier) { - attributes = new HashMap<>(); - for (DynamoTierAttribute tierAttribute : DynamoTierAttribute.values()) { - attributes.put(tierAttribute.name(), tierAttribute.fromTier(tier)); - } - LOGGER.debug("Created DynamoTier: {}", attributes); - } - - private Map attributesWithoutPrimaryKey() { - Map toReturn = new HashMap<>(attributes); - toReturn.remove(PRIMARY_KEY.name()); - return toReturn; - } - - public String updateExpression() { - // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html - List setUpdates = new ArrayList<>(); - for (String attributeName : attributesWithoutPrimaryKey().keySet()) { - setUpdates.add(String.format("#%s=:%s", attributeName, attributeName)); - } - // return updateExpression.toString(); - return "SET " + String.join(",", setUpdates); - } - - // because name is a reserved keyword in DynamoDB update expressions, we need to change the update expression from - // something like - // SET name=:name,description=:description - // to - // SET #name=:name,#description=:description - public Map updateAttributeNames() { - Map updateAttributeNames = new HashMap<>(); - for (String attributeName : attributesWithoutPrimaryKey().keySet()) { - updateAttributeNames.put("#" + attributeName, attributeName); - } - return updateAttributeNames; - } - - public Map updateAttributes() { - Map updateAttributes = new HashMap<>(); - for (Map.Entry attribute : attributesWithoutPrimaryKey().entrySet()) { - updateAttributes.put(":" + attribute.getKey(), attribute.getValue()); - } - return updateAttributes; - } - - public Map primaryKey() { - return Collections.singletonMap(PRIMARY_KEY.name(), attributes.get(PRIMARY_KEY.name())); - } - - public static Map primaryKey(String id) { - return Collections.singletonMap(PRIMARY_KEY.name(), AttributeValue.builder().s(id).build()); - } - - public static Tier fromAttributes(Map attributes) { - Tier.Builder tierBuilderInProgress = Tier.builder(); - for (DynamoTierAttribute tierAttribute : DynamoTierAttribute.values()) { - tierAttribute.toTier(tierBuilderInProgress, attributes.get(tierAttribute.name())); - } - return tierBuilderInProgress.build(); - } - - public static DynamoTier fromTier(Tier tier) { - return new DynamoTier(tier); - } -} diff --git a/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTierAttribute.java b/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTierAttribute.java deleted file mode 100644 index 3742a7ad..00000000 --- a/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTierAttribute.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost.dal.ddb; - -import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import com.amazon.aws.partners.saasfactory.saasboost.model.Tier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.function.*; - -public enum DynamoTierAttribute { - id(tier -> AttributeValue.builder().s(tier.getId()).build(), - attributeValue -> !Utils.isEmpty(attributeValue.s()), - (tierBuilder, attributeValue) -> tierBuilder.id(attributeValue.s())), - created(tier -> AttributeValue.builder().s( - tier.getCreated().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build(), - attributeValue -> !Utils.isEmpty(attributeValue.s()), - (tierBuilder, attributeValue) -> tierBuilder.created( - LocalDateTime.parse(attributeValue.s(), DateTimeFormatter.ISO_DATE_TIME))), - modified(tier -> AttributeValue.builder().s( - tier.getModified().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build(), - attributeValue -> !Utils.isEmpty(attributeValue.s()), - (tierBuilder, attributeValue) -> tierBuilder.modified( - LocalDateTime.parse(attributeValue.s(), DateTimeFormatter.ISO_DATE_TIME))), - name(tier -> AttributeValue.builder().s(tier.getName()).build(), - attributeValue -> !Utils.isEmpty(attributeValue.s()), - (tierBuilder, attributeValue) -> tierBuilder.name(attributeValue.s())), - description(tier -> AttributeValue.builder().s(tier.getDescription()).build(), - attributeValue -> attributeValue.s() != null, // descriptions are allowed to be empty - (tierBuilder, attributeValue) -> tierBuilder.description(attributeValue.s())), - default_tier(tier -> AttributeValue.builder().bool(tier.defaultTier()).build(), - attributeValue -> attributeValue.bool() != null, - (tierBuilder, attributeValue) -> tierBuilder.defaultTier(attributeValue.bool())); - - private static final Logger LOGGER = LoggerFactory.getLogger(DynamoTierAttribute.class); - - // used to convert the Attribute from a Tier to a DDB AttributeValue - private final Function fromTierFunction; - // used to determine if the provided AttributeValue is valid for this Attribute - private final Predicate validAttributeValueFunction; - // takes an existing Tier.Builder and adds this Attribute to it - private final BiConsumer addToTierBuilderFunction; - - DynamoTierAttribute( - Function fromTierFunction, - Predicate validAttributeValueFunction, - BiConsumer addToTierBuilderFunction) { - this.fromTierFunction = fromTierFunction; - this.validAttributeValueFunction = validAttributeValueFunction; - this.addToTierBuilderFunction = addToTierBuilderFunction; - } - - public AttributeValue fromTier(Tier tier) { - // if Tier.created or Tier.modified is null, this might throw a NullPointer - return fromTierFunction.apply(tier); - } - - public void toTier(Tier.Builder tierBuilderInProgress, AttributeValue attributeValue) { - if (!validAttributeValueFunction.test(attributeValue)) { - // most of our validity checks above are "if null" or "if empty" - throw new IllegalArgumentException("AttributeValue for " + this + " is invalid: \"" - + attributeValue.toString() + "\""); - } - try { - addToTierBuilderFunction.accept(tierBuilderInProgress, attributeValue); - } catch (DateTimeParseException dtpe) { - LOGGER.error("Failed to parse TierAttribute: " + this + " from database value: " + attributeValue); - LOGGER.error(Utils.getFullStackTrace(dtpe)); - } catch (Exception e) { - LOGGER.error("Unexpected exception parsing TierAttribute: " + this - + " from database value: " + attributeValue); - LOGGER.error(Utils.getFullStackTrace(e)); - } - } -} diff --git a/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTierDataStore.java b/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTierDataStore.java deleted file mode 100644 index 2e506358..00000000 --- a/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTierDataStore.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost.dal.ddb; - -import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import com.amazon.aws.partners.saasfactory.saasboost.dal.TierDataStore; -import com.amazon.aws.partners.saasfactory.saasboost.dal.exception.TierNotFoundException; -import com.amazon.aws.partners.saasfactory.saasboost.model.Tier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; -import software.amazon.awssdk.services.dynamodb.model.*; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; - -public class DynamoTierDataStore implements TierDataStore { - private static final Logger LOGGER = LoggerFactory.getLogger(DynamoTierDataStore.class); - private final String tableName; - private final DynamoDbClient ddb; - - public DynamoTierDataStore(DynamoDbClient ddb, String tableName) { - this.ddb = ddb; - this.tableName = tableName; - } - - @Override - public Tier getTier(String id) { - String tierNotFoundMessage = String.format("No Tier found with id: %s", id); - if (id == null) { - throw new TierNotFoundException(tierNotFoundMessage); - } - GetItemRequest getItemRequest = GetItemRequest.builder() - .tableName(tableName) - .key(DynamoTier.primaryKey(id)) - .consistentRead(true) - .build(); - GetItemResponse getItemResponse = ddb.getItem(getItemRequest); - if (getItemResponse.hasItem()) { - return DynamoTier.fromAttributes(getItemResponse.item()); - } - throw new TierNotFoundException(tierNotFoundMessage); - } - - @Override - public List listTiers() { - // TODO this doesn't do any ddb error checking - return ddb.scan(request -> request - .tableName(tableName)).items().stream() - .map(DynamoTier::fromAttributes) - .collect(Collectors.toList()); - } - - @Override - public Tier createTier(final Tier tier) { - if (tier == null) { - throw new NullPointerException("Cannot create null Tier"); - } - // we might need to modify the Tier, so create a new copy - Tier.Builder updatedTierBuilder = Tier.builder(tier); - LocalDateTime now = LocalDateTime.now(); - if (Utils.isBlank(tier.getId())) { - // in practice customers should rely on us to create IDs for them - // but we don't always override the ID in case customers want to - // specify their own and for our own testing purposes. - updatedTierBuilder.id(UUID.randomUUID().toString()); - } - if (tier.getCreated() == null) { - updatedTierBuilder.created(now); - } - Tier tierToCreate = updatedTierBuilder.modified(now).build(); - // TODO this doesn't do any ddb error checking - final PutItemRequest putItemRequest = PutItemRequest.builder() - .tableName(tableName) - .item(DynamoTier.fromTier(tierToCreate).attributes) - .build(); - ddb.putItem(putItemRequest); - return tierToCreate; - } - - @Override - public void deleteTier(String id) { - // TODO this doesn't do any ddb error checking - // deleteItem has no problem with deleting non-existent items - DeleteItemRequest deleteItemRequest = DeleteItemRequest.builder() - .tableName(tableName) - .key(DynamoTier.primaryKey(id)) - .build(); - ddb.deleteItem(deleteItemRequest); - } - - @Override - public Tier updateTier(Tier tier) { - // updating the Tier modifies it, so update the modified field - tier = Tier.builder(tier).modified(LocalDateTime.now()).build(); - - // TODO this doesn't do any ddb error checking - DynamoTier dynamoTier = DynamoTier.fromTier(tier); - - // TODO conditional on whether it exists - UpdateItemRequest updateItemRequest = UpdateItemRequest.builder() - .key(dynamoTier.primaryKey()) - .tableName(tableName) - .updateExpression(dynamoTier.updateExpression()) - .expressionAttributeValues(dynamoTier.updateAttributes()) - .expressionAttributeNames(dynamoTier.updateAttributeNames()) - .returnValues(ReturnValue.ALL_NEW) - .build(); - LOGGER.debug("Updating Tier in DDB using {}", updateItemRequest); - UpdateItemResponse updateItemResponse = ddb.updateItem(updateItemRequest); - return DynamoTier.fromAttributes(updateItemResponse.attributes()); - } -} diff --git a/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/Tier.java b/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/Tier.java deleted file mode 100644 index 6e06853b..00000000 --- a/services/tier-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/Tier.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost.model; - -import com.amazon.aws.partners.saasfactory.saasboost.Utils; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; - -import java.time.LocalDateTime; -import java.util.Objects; - -@JsonDeserialize(builder = Tier.Builder.class) -public final class Tier { - private final String id; - private final LocalDateTime created; - private final LocalDateTime modified; - private final String name; - private final String description; - private final Boolean defaultTier; - - private Tier(Tier.Builder builder) { - this.id = builder.id; - this.created = builder.created; - this.modified = builder.modified; - this.name = builder.name; - this.description = builder.description; - this.defaultTier = builder.defaultTier; - } - - public String getId() { - return this.id; - } - - public LocalDateTime getCreated() { - return this.created; - } - - public LocalDateTime getModified() { - return this.modified; - } - - public String getName() { - return this.name; - } - - public String getDescription() { - return this.description; - } - - public Boolean defaultTier() { - return this.defaultTier; - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof Tier)) { - return false; - } - Tier otherTier = (Tier)other; - return this.getId().equals(otherTier.getId()) - && this.getCreated().equals(otherTier.getCreated()) - && this.getModified().equals(otherTier.getModified()) - && this.getName().equals(otherTier.getName()) - && this.getDescription().equals(otherTier.getDescription()) - && this.defaultTier().equals(otherTier.defaultTier()); - } - - @Override - public int hashCode() { - return Objects.hash(id, created, modified, name, description, defaultTier); - } - - public static Tier.Builder builder(Tier tier) { - return builder() - .id(tier.getId()) - .created(tier.getCreated()) - .modified(tier.getModified()) - .description(tier.getDescription()) - .name(tier.getName()) - .defaultTier(tier.defaultTier()); - } - - public static Tier.Builder builder() { - return new Builder(); - } - - @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property] - public static final class Builder { - private String id; - private LocalDateTime created; - private LocalDateTime modified; - private String name; - private String description; - private Boolean defaultTier = false; - - private Builder() { - - } - - public Builder id(String id) { - this.id = id; - return this; - } - - public Builder created(LocalDateTime created) { - this.created = created; - return this; - } - - public Builder modified(LocalDateTime modified) { - this.modified = modified; - return this; - } - - public Builder name(String name) { - this.name = name; - return this; - } - - public Builder description(String description) { - this.description = description; - return this; - } - - public Builder defaultTier(Boolean defaultTier) { - this.defaultTier = defaultTier; - return this; - } - - public Tier build() { - if (Utils.isEmpty(name)) { - throw new IllegalArgumentException("Tier must include a non-null, non-empty name."); - } - return new Tier(this); - } - } -} diff --git a/services/tier-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/MockDependencyFactory.java b/services/tier-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/MockDependencyFactory.java new file mode 100644 index 00000000..44568e17 --- /dev/null +++ b/services/tier-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/MockDependencyFactory.java @@ -0,0 +1,31 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +public class MockDependencyFactory implements TierService.TierServiceDependencyFactory { + + private TierDataAccessLayer dal; + + @Override + public TierDataAccessLayer dal() { + return dal; + } + + public void setDal(TierDataAccessLayer dal) { + this.dal = dal; + } +} diff --git a/services/tier-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TierDataAccessLayerTest.java b/services/tier-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TierDataAccessLayerTest.java new file mode 100644 index 00000000..20e21a3e --- /dev/null +++ b/services/tier-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/TierDataAccessLayerTest.java @@ -0,0 +1,90 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +public class TierDataAccessLayerTest { + + private static UUID tierId; + + @BeforeAll + public static void setup() throws Exception { + tierId = UUID.fromString("c9a437c5-68bc-47ab-a4d5-4e6bbd089914"); + } + + @Test + public void testToAttributeValueMap() { + LocalDateTime created = LocalDateTime.now(); + LocalDateTime modified = LocalDateTime.now(); + + String tierName = "default"; + String tierDescription = "Default Tier"; + boolean defaultTier = true; + String bilingPlan = "Free Trial"; + + Tier tier = new Tier(); + tier.setId(tierId); + tier.setCreated(created); + tier.setModified(modified); + tier.setName(tierName); + tier.setDescription(tierDescription); + tier.setDefaultTier(defaultTier); + tier.setBillingPlan(bilingPlan); + + Map expected = new HashMap<>(); + expected.put("id", AttributeValue.builder().s(tierId.toString()).build()); + expected.put("created", AttributeValue.builder().s( + created.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); + expected.put("modified", AttributeValue.builder().s( + modified.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build()); + expected.put("name", AttributeValue.builder().s(tierName).build()); + expected.put("description", AttributeValue.builder().s(tierDescription).build()); + expected.put("default_tier", AttributeValue.builder().bool(defaultTier).build()); + expected.put("billing_plan", AttributeValue.builder().s(bilingPlan).build()); + + Map actual = TierDataAccessLayer.toAttributeValueMap(tier); + + // DynamoDB marshalling + assertEquals(expected.size(), actual.size(), + () -> "Expected size " + expected.size() + " != actual size " + actual.size()); + expected.keySet().stream().forEach(key -> { + assertEquals(expected.get(key), actual.get(key), () -> "Value mismatch for '" + key + "'"); + }); + + // Have we reflected all class properties we serialize for API calls in DynamoDB? + Map json = Utils.fromJson(Utils.toJson(tier), LinkedHashMap.class); + json.keySet().stream() + .map(key -> Utils.toSnakeCase(key)) + .forEach(key -> { + assertTrue(actual.containsKey(key), + () -> "Class property '" + key + "' does not exist in DynamoDB attribute map"); + }); + } + +} \ No newline at end of file diff --git a/services/tier-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTierAttributeTest.java b/services/tier-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTierAttributeTest.java deleted file mode 100644 index c4004b46..00000000 --- a/services/tier-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTierAttributeTest.java +++ /dev/null @@ -1,261 +0,0 @@ -package com.amazon.aws.partners.saasfactory.saasboost.dal.ddb; - -import static org.junit.Assert.assertEquals; - -import com.amazon.aws.partners.saasfactory.saasboost.model.Tier; -import org.junit.Test; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Map; -import java.util.UUID; - -public class DynamoTierAttributeTest { - // TODO migrate to ParameterizedTest in JUnit5 - private static final String VALID_ID = UUID.randomUUID().toString(); - private static final String VALID_NAME = "Ultra-Platinum"; - private static final String VALID_DESC = "The best tier, wow!"; - private static final LocalDateTime VALID_DATETIME = LocalDateTime.now(); - private static final Boolean VALID_ISDEFAULT = Boolean.TRUE; - private static final Tier VALID_TIER = Tier.builder() - .created(VALID_DATETIME) - .defaultTier(VALID_ISDEFAULT) - .description(VALID_DESC) - .id(VALID_ID) - .modified(VALID_DATETIME) - .name(VALID_NAME) - .build(); - private static final Map VALID_ATTRIBUTES = Map.of( - DynamoTierAttribute.id.name(), AttributeValue.builder().s(VALID_ID).build(), - DynamoTierAttribute.created.name(), AttributeValue.builder().s(VALID_DATETIME.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build(), - DynamoTierAttribute.modified.name(), AttributeValue.builder().s(VALID_DATETIME.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).build(), - DynamoTierAttribute.name.name(), AttributeValue.builder().s(VALID_NAME).build(), - DynamoTierAttribute.description.name(), AttributeValue.builder().s(VALID_DESC).build(), - DynamoTierAttribute.default_tier.name(), AttributeValue.builder().bool(VALID_ISDEFAULT).build() - ); - - // Test fromTier for all DynamoTierAttributes - - @Test - public void id_fromTier() { - assertEquals(VALID_ATTRIBUTES.get(DynamoTierAttribute.id.name()), DynamoTierAttribute.id.fromTier(VALID_TIER)); - } - - @Test - public void id_fromTier_null() { - Tier nullIdTier = Tier.builder(VALID_TIER).id(null).build(); - AttributeValue nullIdAttributeValue = AttributeValue.builder().s(null).build(); - assertEquals(nullIdAttributeValue, DynamoTierAttribute.id.fromTier(nullIdTier)); - } - - @Test - public void id_fromTier_empty() { - Tier emptyIdTier = Tier.builder(VALID_TIER).id("").build(); - AttributeValue emptyIdAttributeValue = AttributeValue.builder().s("").build(); - assertEquals(emptyIdAttributeValue, DynamoTierAttribute.id.fromTier(emptyIdTier)); - } - - @Test - public void created_fromTier() { - assertEquals(VALID_ATTRIBUTES.get(DynamoTierAttribute.created.name()), DynamoTierAttribute.created.fromTier(VALID_TIER)); - } - - @Test(expected = NullPointerException.class) - public void created_fromTier_null() { - Tier nullCreatedTier = Tier.builder(VALID_TIER).created(null).build(); - AttributeValue nullCreatedAttributeValue = AttributeValue.builder().s(null).build(); - assertEquals(nullCreatedAttributeValue, DynamoTierAttribute.created.fromTier(nullCreatedTier)); - } - - @Test - public void modified_fromTier() { - assertEquals(VALID_ATTRIBUTES.get(DynamoTierAttribute.modified.name()), DynamoTierAttribute.modified.fromTier(VALID_TIER)); - } - - @Test(expected = NullPointerException.class) - public void modified_fromTier_null() { - Tier nullModifiedTier = Tier.builder(VALID_TIER).modified(null).build(); - AttributeValue nullModifiedAttributeValue = AttributeValue.builder().s(null).build(); - assertEquals(nullModifiedAttributeValue, DynamoTierAttribute.modified.fromTier(nullModifiedTier)); - } - - @Test - public void name_fromTier() { - assertEquals(VALID_ATTRIBUTES.get(DynamoTierAttribute.name.name()), DynamoTierAttribute.name.fromTier(VALID_TIER)); - } - - // Tier Builder disallows a null and empty names - - @Test - public void description_fromTier() { - assertEquals(VALID_ATTRIBUTES.get(DynamoTierAttribute.description.name()), DynamoTierAttribute.description.fromTier(VALID_TIER)); - } - - @Test - public void description_fromTier_null() { - Tier nullDescriptionTier = Tier.builder(VALID_TIER).description(null).build(); - AttributeValue nullDescriptionAttributeValue = AttributeValue.builder().s(null).build(); - assertEquals(nullDescriptionAttributeValue, DynamoTierAttribute.description.fromTier(nullDescriptionTier)); - } - - @Test - public void description_fromTier_empty() { - Tier emptyDescriptionTier = Tier.builder(VALID_TIER).description("").build(); - AttributeValue emptyDescriptionAttributeValue = AttributeValue.builder().s("").build(); - assertEquals(emptyDescriptionAttributeValue, DynamoTierAttribute.description.fromTier(emptyDescriptionTier)); - } - - @Test - public void defaultTier_fromTier() { - assertEquals(VALID_ATTRIBUTES.get(DynamoTierAttribute.default_tier.name()), DynamoTierAttribute.default_tier.fromTier(VALID_TIER)); - } - - @Test - public void defaultTier_fromTier_null() { - Tier nullDefaultTier = Tier.builder(VALID_TIER).defaultTier(null).build(); - AttributeValue nullDefaultAttributeValue = AttributeValue.builder().s(null).build(); - assertEquals(nullDefaultAttributeValue, DynamoTierAttribute.default_tier.fromTier(nullDefaultTier)); - } - - // Test toTier for all DynamoTierAttributes - - @Test - public void id_toTier_valid() { - assertEquals(VALID_ID.toString(), toTierTest( - DynamoTierAttribute.id, VALID_ATTRIBUTES.get(DynamoTierAttribute.id.name())) - .getId()); - } - - @Test(expected = IllegalArgumentException.class) - public void id_toTier_null() { - toTierTest(DynamoTierAttribute.id, AttributeValue.builder().s(null).build()); - } - - @Test(expected = IllegalArgumentException.class) - public void id_toTier_empty() { - toTierTest(DynamoTierAttribute.id, AttributeValue.builder().s("").build()); - } - - @Test(expected = IllegalArgumentException.class) - public void id_toTier_nonexistent() { - toTierTest(DynamoTierAttribute.id, AttributeValue.builder().build()); - } - - @Test - public void created_toTier_valid() { - assertEquals(VALID_DATETIME.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), toTierTest( - DynamoTierAttribute.created, VALID_ATTRIBUTES.get(DynamoTierAttribute.created.name())) - .getCreated().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); - } - - @Test(expected = IllegalArgumentException.class) - public void created_toTier_null() { - toTierTest(DynamoTierAttribute.created, AttributeValue.builder().s(null).build()); - } - - @Test(expected = IllegalArgumentException.class) - public void created_toTier_empty() { - toTierTest(DynamoTierAttribute.created, AttributeValue.builder().s("").build()); - } - - @Test(expected = IllegalArgumentException.class) - public void created_toTier_nonexistent() { - toTierTest(DynamoTierAttribute.created, AttributeValue.builder().build()); - } - - @Test - public void modified_toTier_valid() { - assertEquals(VALID_DATETIME.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), toTierTest( - DynamoTierAttribute.modified, VALID_ATTRIBUTES.get(DynamoTierAttribute.modified.name())) - .getModified().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); - } - - @Test(expected = IllegalArgumentException.class) - public void modified_toTier_null() { - toTierTest(DynamoTierAttribute.modified, AttributeValue.builder().s(null).build()); - } - - @Test(expected = IllegalArgumentException.class) - public void modified_toTier_empty() { - toTierTest(DynamoTierAttribute.modified, AttributeValue.builder().s("").build()); - } - - @Test(expected = IllegalArgumentException.class) - public void modified_toTier_nonexistent() { - toTierTest(DynamoTierAttribute.modified, AttributeValue.builder().build()); - } - - @Test - public void name_toTier_valid() { - assertEquals(VALID_NAME, toTierTest( - DynamoTierAttribute.name, VALID_ATTRIBUTES.get(DynamoTierAttribute.name.name())) - .getName()); - } - - @Test(expected = IllegalArgumentException.class) - public void name_toTier_null() { - toTierTest(DynamoTierAttribute.name, AttributeValue.builder().s(null).build()); - } - - @Test(expected = IllegalArgumentException.class) - public void name_toTier_empty() { - toTierTest(DynamoTierAttribute.name, AttributeValue.builder().s("").build()); - } - - @Test(expected = IllegalArgumentException.class) - public void name_toTier_nonexistent() { - toTierTest(DynamoTierAttribute.name, AttributeValue.builder().build()); - } - - @Test - public void description_toTier_valid() { - assertEquals(VALID_DESC, toTierTest( - DynamoTierAttribute.description, VALID_ATTRIBUTES.get(DynamoTierAttribute.description.name())) - .getDescription()); - } - - @Test(expected = IllegalArgumentException.class) - public void description_toTier_null() { - toTierTest(DynamoTierAttribute.description, AttributeValue.builder().s(null).build()); - } - - public void description_toTier_empty() { - assertEquals("", toTierTest( - DynamoTierAttribute.description, AttributeValue.builder().s("").build()) - .getDescription()); - } - - @Test(expected = IllegalArgumentException.class) - public void description_toTier_nonexistent() { - toTierTest(DynamoTierAttribute.description, AttributeValue.builder().build()); - } - - @Test - public void defaultTier_toTier_valid() { - assertEquals(VALID_ISDEFAULT, toTierTest( - DynamoTierAttribute.default_tier, VALID_ATTRIBUTES.get(DynamoTierAttribute.default_tier.name())) - .defaultTier()); - } - - @Test(expected = IllegalArgumentException.class) - public void defaultTier_toTier_null() { - toTierTest(DynamoTierAttribute.default_tier, AttributeValue.builder().bool(null).build()); - } - - @Test(expected = IllegalArgumentException.class) - public void defaultTier_toTier_nonexistent() { - toTierTest(DynamoTierAttribute.default_tier, AttributeValue.builder().build()); - } - - @Test(expected = IllegalArgumentException.class) - public void defaultTier_toTier_invalid() { - toTierTest(DynamoTierAttribute.default_tier, AttributeValue.builder().s("").build()); - } - - private Tier toTierTest(DynamoTierAttribute testedAttribute, AttributeValue testAttributeValue) { - Tier.Builder testBuilder = Tier.builder(VALID_TIER); - testedAttribute.toTier(testBuilder, testAttributeValue); - return testBuilder.build(); - } -} diff --git a/services/tier-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTierDataStoreCreateTierTest.java b/services/tier-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTierDataStoreCreateTierTest.java deleted file mode 100644 index ae2b944b..00000000 --- a/services/tier-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTierDataStoreCreateTierTest.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost.dal.ddb; - -import com.amazon.aws.partners.saasfactory.saasboost.model.Tier; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; -import software.amazon.awssdk.services.dynamodb.model.*; - -import java.util.HashMap; -import java.util.Map; - -import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -public class DynamoTierDataStoreCreateTierTest { - private static final String TABLE_NAME = "test-table"; - private static final String VALID_ID = "abc-123-id-456"; - private static final String VALID_DESC = "gold tier description\n123"; - private static final String VALID_NAME = "gold-tier"; - private static final Tier TIER_WITH_ID = Tier.builder() - .name(VALID_NAME).id(VALID_ID).description(VALID_DESC).defaultTier(false).build(); - private static final Tier TIER_NO_ID = Tier.builder() - .name(VALID_NAME).description(VALID_DESC).defaultTier(false).build(); - - private DynamoDbClient mockDdb; - private DynamoTierDataStore dynamoTierDataStore; - private ArgumentCaptor requestArgumentCaptor; - - @Before - public void setup() { - mockDdb = mock(DynamoDbClient.class); - final PutItemResponse validResponse = PutItemResponse.builder().build(); - when(mockDdb.putItem(any(PutItemRequest.class))).thenReturn(validResponse); - - requestArgumentCaptor = ArgumentCaptor.forClass(PutItemRequest.class); - dynamoTierDataStore = new DynamoTierDataStore(mockDdb, TABLE_NAME); - } - - public void verifyPutItemRequest(Map expectedAttributes) { - // verify putItem was called at least once - verify(mockDdb).putItem(requestArgumentCaptor.capture()); - // assert the passed getItem request matches expectations - PutItemRequest capturedRequest = requestArgumentCaptor.getValue(); - assertEquals("Requested table name should match configuration.", - TABLE_NAME, capturedRequest.tableName()); - assertTrue(capturedRequest.hasItem()); - assertTrue("Put item attributes must contain ID.", - capturedRequest.item().containsKey(DynamoTierAttribute.id.name())); - assertTrue("Put item attributes must contain Created.", - capturedRequest.item().containsKey(DynamoTierAttribute.created.name())); - assertTrue("Put item attributes must contain Modified.", - capturedRequest.item().containsKey(DynamoTierAttribute.modified.name())); - - // we might need to modify expectedAttributes, and it might be unmodifiable. so make a quick copy. - expectedAttributes = new HashMap<>(expectedAttributes); - if (!expectedAttributes.containsKey(DynamoTierAttribute.id.name())) { - // we aren't expecting any ID in particular, so just set the expected ID to be the actual - expectedAttributes.put(DynamoTierAttribute.id.name(), capturedRequest.item().get(DynamoTierAttribute.id.name())); - } - if (!expectedAttributes.containsKey(DynamoTierAttribute.created.name())) { - // it's unreasonable to expect an exact create time, so just set the expected to be the actual - expectedAttributes.put(DynamoTierAttribute.created.name(), capturedRequest.item().get(DynamoTierAttribute.created.name())); - } - if (!expectedAttributes.containsKey(DynamoTierAttribute.modified.name())) { - // it's unreasonable to expect an exact modified time, so just set the expected to be the actual - expectedAttributes.put(DynamoTierAttribute.modified.name(), capturedRequest.item().get(DynamoTierAttribute.modified.name())); - } - assertEquals("Put item attributes should match expected.", - expectedAttributes, - capturedRequest.item()); - } - - @Test - public void withoutId() { - dynamoTierDataStore.createTier(TIER_NO_ID); - verifyPutItemRequest(Map.of( - DynamoTierAttribute.description.name(), AttributeValue.builder().s(VALID_DESC).build(), - DynamoTierAttribute.name.name(), AttributeValue.builder().s(VALID_NAME).build(), - DynamoTierAttribute.default_tier.name(), AttributeValue.builder().bool(false).build() - )); - } - - @Test - public void withId() { - dynamoTierDataStore.createTier(TIER_WITH_ID); - verifyPutItemRequest(Map.of( - DynamoTierAttribute.description.name(), AttributeValue.builder().s(VALID_DESC).build(), - DynamoTierAttribute.name.name(), AttributeValue.builder().s(VALID_NAME).build(), - DynamoTierAttribute.id.name(), AttributeValue.builder().s(VALID_ID).build(), - DynamoTierAttribute.default_tier.name(), AttributeValue.builder().bool(false).build() - )); - } - - @Test(expected = NullPointerException.class) - public void nullTier() { - dynamoTierDataStore.createTier(null); - } -} diff --git a/services/tier-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTierDataStoreGetTierTest.java b/services/tier-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTierDataStoreGetTierTest.java deleted file mode 100644 index b7e108f8..00000000 --- a/services/tier-service/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/dal/ddb/DynamoTierDataStoreGetTierTest.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.amazon.aws.partners.saasfactory.saasboost.dal.ddb; - -import com.amazon.aws.partners.saasfactory.saasboost.dal.exception.TierNotFoundException; -import com.amazon.aws.partners.saasfactory.saasboost.model.Tier; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatcher; -import org.mockito.ArgumentMatchers; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; -import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; -import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Map; - -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; - -public class DynamoTierDataStoreGetTierTest { - private static final String TABLE_NAME = "test-table"; - private static final String VALID_ID = "abc-123-id-456"; - private static final String VALID_DESC = "gold tier description\n123"; - private static final String VALID_NAME = "gold-tier"; - private static final LocalDateTime VALID_DATETIME = LocalDateTime.now(); - private static final String VALID_TIME = VALID_DATETIME.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); - private static final Tier VALID_TIER = Tier.builder() - .name(VALID_NAME) - .created(VALID_DATETIME) - .modified(VALID_DATETIME) - .id(VALID_ID) - .description(VALID_DESC) - .defaultTier(false) - .build(); - private static final ArgumentMatcher VALID_REQUEST = - getItemRequest -> getItemRequest != null && getItemRequest.hasKey() - && VALID_ID.equals(getItemRequest.key().get(DynamoTierAttribute.id.name()).s()); - private static final ArgumentMatcher INVALID_REQUEST = - getItemRequest -> !VALID_REQUEST.matches(getItemRequest); - - private DynamoDbClient mockDdb; - private ArgumentCaptor requestArgumentCaptor; - private DynamoTierDataStore dynamoTierDataStore; - - @Before - public void setup() { - mockDdb = mock(DynamoDbClient.class); - final GetItemResponse validResponse = GetItemResponse.builder() - .item(Map.of( - DynamoTierAttribute.id.name(), AttributeValue.builder().s(VALID_ID).build(), - DynamoTierAttribute.created.name(), AttributeValue.builder().s(VALID_TIME).build(), - DynamoTierAttribute.modified.name(), AttributeValue.builder().s(VALID_TIME).build(), - DynamoTierAttribute.description.name(), AttributeValue.builder().s(VALID_DESC).build(), - DynamoTierAttribute.name.name(), AttributeValue.builder().s(VALID_NAME).build(), - DynamoTierAttribute.default_tier.name(), AttributeValue.builder().bool(false).build())) - .build(); - final GetItemResponse invalidResponse = GetItemResponse.builder().build(); - when(mockDdb.getItem(ArgumentMatchers.argThat(VALID_REQUEST))).thenReturn(validResponse); - when(mockDdb.getItem(ArgumentMatchers.argThat(INVALID_REQUEST))).thenReturn(invalidResponse); - - requestArgumentCaptor = ArgumentCaptor.forClass(GetItemRequest.class); - dynamoTierDataStore = new DynamoTierDataStore(mockDdb, TABLE_NAME); - } - - public void verifyGetItemRequest(String expectedId) { - // verify getItem was called at least once - verify(mockDdb).getItem(requestArgumentCaptor.capture()); - // assert the passed getItem request matches expectations - GetItemRequest capturedRequest = requestArgumentCaptor.getValue(); - assertEquals("Requested table name should match configuration.", - TABLE_NAME, capturedRequest.tableName()); - assertTrue(capturedRequest.hasKey()); - assertEquals("Requested primary key should be id:configuredId.", - Map.of(DynamoTierAttribute.id.name(), AttributeValue.builder().s(expectedId).build()), - capturedRequest.key()); - assertTrue("GetItem should use Consistent Reads", capturedRequest.consistentRead()); - } - - @Test - public void validId() { - Tier retrievedTier = dynamoTierDataStore.getTier(VALID_ID); - verifyGetItemRequest(VALID_ID); - // assert the retrievedTier matches expectations - assertEquals("Tiers should be equal.", VALID_TIER, retrievedTier); - } - - @Test(expected = TierNotFoundException.class) - public void getTierTest_nullId() { - dynamoTierDataStore.getTier(null); - } - - @Test - public void getTierTest_invalidNonnullId() { - // If there is no matching item, GetItem does not return any data and there will be no Item element in the response. - final String invalidId = VALID_ID + "-different"; - try { - dynamoTierDataStore.getTier(invalidId); - fail("getTier with a non-existent id should throw TierNotFoundException"); - } catch (TierNotFoundException tnfe) { - verifyGetItemRequest(invalidId); - } - } -} diff --git a/services/tier-service/update.sh b/services/tier-service/update.sh index b95f3742..dddd7df5 100755 --- a/services/tier-service/update.sh +++ b/services/tier-service/update.sh @@ -49,7 +49,7 @@ fi aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ # Find all the functions for this microservice -eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-tier-\`)] | [].FunctionName' --output text"\) +eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-tiers-\`)] | [].FunctionName' --output text"\) FUNCTIONS=($FUNCTIONS) for FX in "${FUNCTIONS[@]}"; do printf "Updating function code for %s\n" $FX