// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // GameKit #include <aws/gamekit/core/feature_resources.h> #include <aws/gamekit/core/gamekit_settings.h> #include <aws/gamekit/core/internal/platform_string.h> #include <aws/gamekit/core/internal/wrap_boost_filesystem.h> #include <aws/gamekit/core/utils/file_utils.h> #include <aws/gamekit/core/gamekit_account.h> // Boost #include <boost/filesystem.hpp> #include <boost/algorithm/string/replace.hpp> using namespace GameKit; using namespace GameKit::Logger; using namespace Aws::CloudFormation; namespace CfnModel = Aws::CloudFormation::Model; namespace LambdaModel = Aws::Lambda::Model; namespace S3Model = Aws::S3::Model; namespace SSMModel = Aws::SSM::Model; namespace fs = boost::filesystem; #pragma region Constructors/Destructor GameKitFeatureResources::GameKitFeatureResources(const AccountInfo accountInfo, const AccountCredentials credentials, FeatureType featureType, FuncLogCallback logCb) : GameKitFeatureResources(CreateAccountInfoCopy(accountInfo), CreateAccountCredentialsCopy(credentials), featureType, logCb) {} GameKitFeatureResources::GameKitFeatureResources(const AccountInfoCopy& accountInfo, const AccountCredentialsCopy& credentials, FeatureType featureType, FuncLogCallback logCb) { m_accountInfo = accountInfo; m_credentials = credentials; m_credentials.accountId = accountInfo.accountId; m_featureType = featureType; m_logCb = logCb; m_resouceStatusMap = {}; m_stackName = GetStackName(); GameKit::AwsApiInitializer::Initialize(m_logCb, this); this->InitializeDefaultAwsClients(); Logging::Log(m_logCb, Level::Info, "GameKitFeatureResources()", this); } GameKitFeatureResources::~GameKitFeatureResources() { Logging::Log(m_logCb, Level::Info, "~GameKitFeatureResources()", this); if (!m_isUsingSharedS3Client) { Logging::Log(m_logCb, Level::Info, "~GameKitFeatureResources() m_s3Client", this); delete(m_s3Client); } if (!m_isUsingSharedSSMClient) { Logging::Log(m_logCb, Level::Info, "~GameKitFeatureResources() m_ssmClient", this); delete(m_ssmClient); } if (!m_isUsingSharedCfClient) { Logging::Log(m_logCb, Level::Info, "~GameKitFeatureResources() m_cfClient", this); delete(m_cfClient); } if (!m_isUsingSharedLambdaClient) { Logging::Log(m_logCb, Level::Info, "~GameKitFeatureResources() m_lambdaClient", this); delete(m_lambdaClient); } // Safe to shutdown here. Other objects that rely on it will shut it down when they go // out of scope or are deleted. GameKit::AwsApiInitializer::Shutdown(m_logCb, this); if (m_isUsingSharedS3Client || m_isUsingSharedSSMClient || m_isUsingSharedCfClient || m_isUsingSharedLambdaClient) { return; } } #pragma endregion #pragma region Public Methods void GameKitFeatureResources::InitializeDefaultAwsClients() { this->SetS3Client(DefaultClients::GetDefaultS3Client(this->GetAccountCredentials()), false); this->SetCloudFormationClient(DefaultClients::GetDefaultCloudFormationClient(this->GetAccountCredentials()), false); this->SetSSMClient(DefaultClients::GetDefaultSSMClient(this->GetAccountCredentials()), false); this->SetLambdaClient(DefaultClients::GetDefaultLambdaClient(this->GetAccountCredentials()), false); } bool GameKit::GameKitFeatureResources::IsCloudFormationInstanceTemplatePresent() const { return boost::filesystem::exists(m_instanceCloudformationPath); } bool GameKit::GameKitFeatureResources::AreLayerInstancesPresent() const { return boost::filesystem::exists(m_instanceLayersPath); } bool GameKit::GameKitFeatureResources::AreFunctionInstancesPresent() const { return boost::filesystem::exists(m_instanceFunctionsPath); } unsigned int GameKitFeatureResources::SaveDeployedCloudFormationTemplate() const { std::string templateBody; const auto getTemplateResult = getDeployedTemplateBody(m_stackName, templateBody); if (getTemplateResult != GAMEKIT_SUCCESS) { return getTemplateResult; } const auto writeResult = writeCloudFormationTemplateInstance(templateBody); if (writeResult != GAMEKIT_SUCCESS) { return writeResult; } const auto describeStackResourcesRequest = CfnModel::DescribeStackResourcesRequest().WithStackName(ToAwsString(m_stackName)); CfnModel::DescribeStackResourcesOutcome describeResourcesOutcome = m_cfClient->DescribeStackResources(describeStackResourcesRequest); Aws::Vector<CfnModel::StackResource> resources = describeResourcesOutcome.GetResult().GetStackResources(); for (auto& resource : resources) { const std::string physicalId = ToStdString(resource.GetPhysicalResourceId()); if (resource.GetResourceType() == "AWS::CloudFormation::Stack") { // A Stack's physical ID is the ARN. We need to strip out the ARN strings so we only get the stack name. But we're only interested in CloudWatchDashboardStack std::smatch match; const std::regex re("arn:aws:cloudformation:[a-z0-9-]+:[0-9]{12}:stack/([a-zA-Z0-9-]+-CloudWatchDashboardStack-[a-zA-Z0-9-]+)/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"); if (!std::regex_search(physicalId, match, re)) { continue; } // We know this is a valid stack since it was returned as a CloudFormation resource. We can ignore the results of these calls. const std::string nestedStackName = match[1]; getDeployedTemplateBody(nestedStackName, templateBody); writeCloudFormationDashboardInstance(templateBody); break; } } return GAMEKIT_SUCCESS; } unsigned int GameKitFeatureResources::GetDeployedCloudFormationParameters(DeployedParametersCallback callback) const { // If we're given no callback function, this serves no purpose if (callback == nullptr) return GAMEKIT_ERROR_GENERAL; const auto describeStackReq = CfnModel::DescribeStacksRequest() .WithStackName(ToAwsString(m_stackName)); auto outcome = m_cfClient->DescribeStacks(describeStackReq); if (!outcome.IsSuccess()) { return GAMEKIT_ERROR_CLOUDFORMATION_DESCRIBE_STACKS_FAILED; } Aws::Vector<Aws::CloudFormation::Model::Stack> stacks = outcome.GetResult().GetStacks(); if (stacks.size() > 0) { // Build a parameter map for easier lookups later Aws::Vector<Aws::CloudFormation::Model::Parameter> params = stacks.at(0).GetParameters(); std::map<std::string, std::string> paramMap; for (auto param : params) { paramMap.insert({ ToStdString(param.GetParameterKey()), ToStdString(param.GetParameterValue()) }); } YAML::Node cfnParams; Utils::FileUtils::ReadFileAsYAML(m_baseCloudformationPath + TemplateFileNames::PARAMETERS_FILE, cfnParams); for (YAML::const_iterator it = cfnParams.begin(); it != cfnParams.end(); ++it) { std::string key = it->first.as<std::string>(); YAML::Node nestedNode = it->second.as<YAML::Node>(); // There should only be `value:` as a nested key std::string internalVariableName = nestedNode.begin()->second.as<std::string>(); // check to see if this is a templated param we should save from CF if (internalVariableName.find(TemplateVars::AWS_GAMEKIT_USERVAR_PREFIX) != std::string::npos) { boost::replace_all(internalVariableName, TemplateVars::AWS_GAMEKIT_USERVAR_PREFIX, ""); boost::replace_all(internalVariableName, TemplateVars::BEGIN_NO_ESCAPE, ""); boost::replace_all(internalVariableName, TemplateVars::END_NO_ESCAPE, ""); std::string existingValue = paramMap.at(key); if (existingValue.length() > 0) { callback(internalVariableName.c_str(), existingValue.c_str()); } } } return GAMEKIT_SUCCESS; } return GAMEKIT_ERROR_CLOUDFORMATION_DESCRIBE_STACKS_FAILED; } unsigned int GameKitFeatureResources::SaveCloudFormationInstance() { return SaveCloudFormationInstance("UNKNOWN", "UNKNOWN"); } unsigned int GameKitFeatureResources::SaveCloudFormationInstance(std::string sourceEngine, std::string pluginVersion) { // If region name cannot be converted to short region code, return an error (all s3 buckets use 5-letter short region codes) const std::string shortRegionCode = getShortRegionCode(); if (shortRegionCode.empty()) { return GameKit::GAMEKIT_ERROR_REGION_CODE_CONVERSION_FAILED; } auto cfTemplate = this->getCloudFormationTemplate(TemplateType::Base); auto cfDashboard = this->getFeatureDashboardTemplate(TemplateType::Base); auto cfParams = this->getRawStackParameters(TemplateType::Base); // regex swap description to describe the engine and version std::regex targetLine("Description: \\(GAMEKIT(.*)\\).*"); std::string replacementDescription = "Description: (GAMEKIT-$1-" + sourceEngine + ") The AWS CloudFormation template for AWS GameKit" + GetFeatureTypeString(m_featureType) + ". v" + pluginVersion; cfTemplate = std::regex_replace(cfTemplate, targetLine, replacementDescription); cfDashboard = std::regex_replace(cfDashboard, targetLine, replacementDescription); // replace occurrances of GAMEKIT System Variables AWSGAMEKIT::SYS::* std::regex envVar(TemplateVars::BEGIN + TemplateVars::AWS_GAMEKIT_ENVIRONMENT + TemplateVars::END); cfTemplate = std::regex_replace(cfTemplate, envVar, m_accountInfo.environment.GetEnvironmentString()); cfDashboard = std::regex_replace(cfDashboard, envVar, m_accountInfo.environment.GetEnvironmentString()); cfParams = std::regex_replace(cfParams, envVar, m_accountInfo.environment.GetEnvironmentString()); std::regex gameName(TemplateVars::BEGIN + TemplateVars::AWS_GAMEKIT_GAMENAME + TemplateVars::END); cfTemplate = std::regex_replace(cfTemplate, gameName, m_accountInfo.gameName); cfDashboard = std::regex_replace(cfDashboard, gameName, m_accountInfo.gameName); cfParams = std::regex_replace(cfParams, gameName, m_accountInfo.gameName); const std::string base36AwsAccountIdStr = GameKit::Utils::EncodingUtils::DecimalToBase(m_accountInfo.accountId, GameKit::Utils::BASE_36); std::regex base36AwsAccountId(TemplateVars::BEGIN + TemplateVars::AWS_GAMEKIT_BASE36_AWS_ACCOUNTID + TemplateVars::END); cfTemplate = std::regex_replace(cfTemplate, base36AwsAccountId, base36AwsAccountIdStr); cfDashboard = std::regex_replace(cfDashboard, base36AwsAccountId, base36AwsAccountIdStr); cfParams = std::regex_replace(cfParams, base36AwsAccountId, base36AwsAccountIdStr); const std::regex shortRegionCodeRegex(TemplateVars::BEGIN + TemplateVars::AWS_GAMEKIT_SHORT_REGION_CODE + TemplateVars::END); cfTemplate = std::regex_replace(cfTemplate, shortRegionCodeRegex, shortRegionCode); cfDashboard = std::regex_replace(cfDashboard, shortRegionCodeRegex, shortRegionCode); cfParams = std::regex_replace(cfParams, shortRegionCodeRegex, shortRegionCode); // save to GAMEKIT_ROOT auto writeResult = writeCloudFormationParameterInstance(cfParams); if (writeResult != GAMEKIT_SUCCESS) { return writeResult; } writeResult = writeCloudFormationTemplateInstance(cfTemplate); if (writeResult != GAMEKIT_SUCCESS) { return writeResult; } writeResult = writeCloudFormationDashboardInstance(cfDashboard); if (writeResult != GAMEKIT_SUCCESS) { return writeResult; } return GAMEKIT_SUCCESS; } unsigned int GameKitFeatureResources::UpdateCloudFormationParameters() { // If region name cannot be converted to short region code, return an error (all s3 buckets use 5-letter short region codes) const std::string shortRegionCode = getShortRegionCode(); if (shortRegionCode.empty()) { return GameKit::GAMEKIT_ERROR_REGION_CODE_CONVERSION_FAILED; } auto cfParams = this->getRawStackParameters(TemplateType::Base); // replace occurrances of GAMEKIT System Variables AWSGAMEKIT::SYS::* const std::regex envVar(TemplateVars::BEGIN + TemplateVars::AWS_GAMEKIT_ENVIRONMENT + TemplateVars::END); cfParams = std::regex_replace(cfParams, envVar, m_accountInfo.environment.GetEnvironmentString()); const std::regex gameName(TemplateVars::BEGIN + TemplateVars::AWS_GAMEKIT_GAMENAME + TemplateVars::END); cfParams = std::regex_replace(cfParams, gameName, m_accountInfo.gameName); const std::regex base36AwsAccountId(TemplateVars::BEGIN + TemplateVars::AWS_GAMEKIT_BASE36_AWS_ACCOUNTID + TemplateVars::END); cfParams = std::regex_replace(cfParams, base36AwsAccountId, GameKit::Utils::EncodingUtils::DecimalToBase(m_accountInfo.accountId, GameKit::Utils::BASE_36)); const std::regex shortRegionCodeRegex(TemplateVars::BEGIN + TemplateVars::AWS_GAMEKIT_SHORT_REGION_CODE + TemplateVars::END); cfParams = std::regex_replace(cfParams, shortRegionCodeRegex, shortRegionCode); // Do not replace AWSGAMEKIT::VARS::* values; these will be replaced at time of deployment // save to GAMEKIT_ROOT return writeCloudFormationParameterInstance(cfParams); } unsigned int GameKitFeatureResources::SaveLayerInstances() const { // if there's nothing to copy, just return success if (!boost::filesystem::exists(m_baseLayersPath)) { return GAMEKIT_SUCCESS; } // create destination instance directory and copy the base layers directory to the instance layers directory std::string logMsg; boost::filesystem::create_directories(m_instanceLayersPath); boost::system::error_code err; boost::filesystem::copy(m_baseLayersPath, m_instanceLayersPath, boost::filesystem::copy_options::recursive | boost::filesystem::copy_options::overwrite_existing, err); if (err.failed()) { logMsg = std::string("Failed to copy Lambda Layers to ").append(m_instanceLayersPath).append("; ").append(err.message()); Logging::Log(m_logCb, Level::Error, logMsg.c_str(), this); return GAMEKIT_ERROR_FUNCTIONS_COPY_FAILED; } logMsg = std::string("Lambda Layers copied to ").append(m_instanceLayersPath); Logging::Log(m_logCb, Level::Info, logMsg.c_str(), this); return GAMEKIT_SUCCESS; } unsigned int GameKitFeatureResources::SaveFunctionInstances() const { // if there's nothing to copy, just return success if (!boost::filesystem::exists(m_baseFunctionsPath)) { return GAMEKIT_SUCCESS; } // create destination instance directory and copy the base function directory to the instance function directory std::string logMsg; boost::filesystem::create_directories(m_instanceFunctionsPath); boost::system::error_code err; boost::filesystem::copy(m_baseFunctionsPath, m_instanceFunctionsPath, boost::filesystem::copy_options::recursive | boost::filesystem::copy_options::overwrite_existing, err); if (err.failed()) { logMsg = std::string("Failed to copy Lambda Functions to ").append(m_instanceFunctionsPath).append("; ").append(err.message()); Logging::Log(m_logCb, Level::Error, logMsg.c_str(), this); return GAMEKIT_ERROR_FUNCTIONS_COPY_FAILED; } logMsg = std::string("Lambda Functions copied to ").append(m_instanceFunctionsPath); Logging::Log(m_logCb, Level::Info, logMsg.c_str(), this); return GAMEKIT_SUCCESS; } void GameKitFeatureResources::SetLayersReplacementId(const std::string& replacementId) { m_layersReplacementId = replacementId; } void GameKitFeatureResources::SetFunctionsReplacementId(const std::string& replacementId) { m_functionsReplacementId = replacementId; } unsigned int GameKitFeatureResources::CreateAndSetLayersReplacementId() { // get current time in milliseconds and use it as the function replacement id const std::chrono::milliseconds ts = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()); const std::string replacementId = std::to_string(ts.count()); const std::string paramName = GetLambdaLayerReplacementIDParamName(); // add GAMEKIT_LAMBDA_LAYERS_REPLACEMENT_ID to Parameter Store with the replacement id SSMModel::PutParameterRequest putParamRequest; putParamRequest.SetType(SSMModel::ParameterType::String); putParamRequest.SetName(ToAwsString(paramName)); putParamRequest.SetValue(replacementId.c_str()); putParamRequest.SetOverwrite(true); auto putParamOutcome = m_ssmClient->PutParameter(putParamRequest); if (!putParamOutcome.IsSuccess()) { Logging::Log(m_logCb, Level::Error, putParamOutcome.GetError().GetMessage().c_str(), this); return GAMEKIT_ERROR_PARAMSTORE_WRITE_FAILED; } m_layersReplacementId = replacementId; return GAMEKIT_SUCCESS; } unsigned int GameKitFeatureResources::CreateAndSetFunctionsReplacementId() { // get current time and use it as the function replacement id const std::chrono::milliseconds ts = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()); const std::string replacementId = std::to_string(ts.count()); const std::string paramName = GetLambdaFunctionReplacementIDParamName(); // add GAMEKIT_LAMBDA_FUNCTIONS_REPLACEMENT_ID to Parameter Store with the replacement id SSMModel::PutParameterRequest putParamRequest; putParamRequest.SetType(SSMModel::ParameterType::String); putParamRequest.SetName(ToAwsString(paramName)); putParamRequest.SetValue(ToAwsString(replacementId)); putParamRequest.SetOverwrite(true); auto putParamOutcome = m_ssmClient->PutParameter(putParamRequest); if (!putParamOutcome.IsSuccess()) { Logging::Log(m_logCb, Level::Error, putParamOutcome.GetError().GetMessage().c_str(), this); return GAMEKIT_ERROR_PARAMSTORE_WRITE_FAILED; } m_functionsReplacementId = replacementId; return GAMEKIT_SUCCESS; } unsigned int GameKitFeatureResources::UploadDashboard(const std::string& path) { Logging::Log(m_logCb, Level::Verbose, "Start UploadDashboard()", this); const fs::path cp = (path + "/" + TemplateFileNames::FEATURE_DASHBOARD_FILE); fs::directory_iterator endIterator; // Verify that the path exists and is a directory if (fs::exists(cp) && fs::is_regular_file(cp)) { assert(GameKit::AwsApiInitializer::IsInitialized()); // If region name cannot be converted to short region code, return an error (all s3 buckets use 5-letter short region codes) const std::string shortRegionCode = getShortRegionCode(); if (shortRegionCode.empty()) { return GameKit::GAMEKIT_ERROR_REGION_CODE_CONVERSION_FAILED; } // set put request params const std::string objectName = std::string("cloudformation/") .append(GameKit::GetFeatureTypeString(m_featureType)) .append("/") .append(TemplateFileNames::FEATURE_DASHBOARD_FILE); const std::shared_ptr<Aws::IOStream> inputData = Aws::MakeShared<Aws::FStream>( cp.string().c_str(), cp.native(), std::ios_base::in | std::ios_base::binary); S3Model::PutObjectRequest putObjRequest; putObjRequest.SetBucket(ToAwsString(GetBootstrapBucketName(m_accountInfo, shortRegionCode))); putObjRequest.SetKey(ToAwsString(objectName)); putObjRequest.SetBody(inputData); putObjRequest.SetExpectedBucketOwner(ToAwsString(m_accountInfo.accountId)); // upload yaml file Logging::Log(m_logCb, Level::Verbose, "GameKitFeatureResources::UploadDashboard() Start put object", m_s3Client); auto putObjOutcome = m_s3Client->PutObject(putObjRequest); Logging::Log(m_logCb, Level::Verbose, "GameKitFeatureResources::UploadDashboard() End put object", m_s3Client); if (!putObjOutcome.IsSuccess()) { Logging::Log(m_logCb, Level::Error, putObjOutcome.GetError().GetMessage().c_str(), this); return GAMEKIT_ERROR_BOOTSTRAP_BUCKET_UPLOAD_FAILED; } } Logging::Log(m_logCb, Level::Verbose, "End UploadDashboard()", this); return GAMEKIT_SUCCESS; } unsigned int GameKitFeatureResources::CompressFeatureLayers() { // loop through the feature's layers directory std::string layerName; Aws::UniquePtr<Zipper> zipper; fs::path p(m_instanceLayersPath); fs::directory_iterator endIterator; // Verify that path exists and is a directory if (fs::exists(p) && fs::is_directory(p)) { for (fs::directory_iterator dirIterator(p); dirIterator != endIterator; ++dirIterator) { // create zip file for every layers directory const fs::path cp = (*dirIterator); // Verify that path is a directory if (fs::is_directory(cp)) { layerName = cp.stem().string(); std::string layerHash; const unsigned int result = GameKit::Utils::FileUtils::CalculateDirectoryHash(cp.string(), layerHash); if (result == GAMEKIT_SUCCESS && !lambdaLayerHashChanged(layerName, layerHash)) { // update hash param if (createAndSetLambdaLayerHash(layerName, layerHash) != GAMEKIT_SUCCESS) { std::string msg = std::string("Unable to save layer hash for ").append(layerName); Logging::Log(m_logCb, Level::Error, msg.c_str()); } // create output directory in temp path std::string tempLayersPath = getTempLayersPath(); fs::create_directories(tempLayersPath); // create zip file std::string zipFileName = tempLayersPath + "/" + layerName + ".zip"; zipper = Aws::MakeUnique<Zipper>(zipFileName.c_str(), cp.string(), zipFileName); if (!zipper->AddDirectoryToZipFile(cp.string())) { std::string msg = std::string("Unable to initialize ").append(zipFileName); Logging::Log(m_logCb, Level::Error, msg.c_str()); return GAMEKIT_ERROR_LAYER_ZIP_INIT_FAILED; } // write zip file to disk if (!zipper->CloseZipFile()) { std::string msg = std::string("Unable to write ").append(zipFileName).append(" to disk"); Logging::Log(m_logCb, Level::Error, msg.c_str(), this); return GAMEKIT_ERROR_LAYER_ZIP_WRITE_FAILED; } // zip file creation successful std::string msg = std::string("Zip file ") .append(zipFileName) .append(" created"); Logging::Log(m_logCb, Level::Info, msg.c_str(), this); } } } } return GAMEKIT_SUCCESS; } unsigned int GameKitFeatureResources::UploadFeatureLayers() { Logging::Log(m_logCb, Level::Verbose, "Start UploadFeatureLayers()", this); // loop through feature layers temp directory const std::string tempLayersPath = getTempLayersPath(); const fs::path p(tempLayersPath); fs::directory_iterator endIterator; // If region name cannot be converted to short region code, return an error (all s3 buckets use 5-letter short region codes) const std::string shortRegionCode = getShortRegionCode(); if (shortRegionCode.empty()) { return GameKit::GAMEKIT_ERROR_REGION_CODE_CONVERSION_FAILED; } // Verify that the path exists and is a directory if (fs::exists(p) && fs::is_directory(p)) { for (fs::directory_iterator dirIterator(p); dirIterator != endIterator; ++dirIterator) { assert(GameKit::AwsApiInitializer::IsInitialized()); const fs::path cp = (*dirIterator); // Verify that the path is a file if (fs::is_regular_file(cp)) { std::string layerDirName = cp.stem().string(); // set put request params std::string objectName = std::string("layers/") .append(GameKit::GetFeatureTypeString(m_featureType)) .append("/") .append(layerDirName) .append(".") .append(m_layersReplacementId) .append(cp.extension().string()); std::shared_ptr<Aws::IOStream> inputData = Aws::MakeShared<Aws::FStream>( cp.string().c_str(), cp.native(), std::ios_base::in | std::ios_base::binary); S3Model::PutObjectRequest putObjRequest; putObjRequest.SetBucket(ToAwsString(GetBootstrapBucketName(m_accountInfo, shortRegionCode))); putObjRequest.SetKey(ToAwsString(objectName)); putObjRequest.SetBody(inputData); putObjRequest.SetExpectedBucketOwner(ToAwsString(m_accountInfo.accountId)); // upload zip file Logging::Log(m_logCb, Level::Verbose, "GameKitFeatureResources::UploadFeatureLayers() Start put object", m_s3Client); auto putObjOutcome = m_s3Client->PutObject(putObjRequest); Logging::Log(m_logCb, Level::Verbose, "GameKitFeatureResources::UploadFeatureLayers() End put object", m_s3Client); if (!putObjOutcome.IsSuccess()) { Logging::Log(m_logCb, Level::Error, putObjOutcome.GetError().GetMessage().c_str(), this); return GAMEKIT_ERROR_BOOTSTRAP_BUCKET_UPLOAD_FAILED; } std::string msg = std::string("Object: ") .append(objectName) .append(" uploaded to: ") .append(GetBootstrapBucketName(m_accountInfo, shortRegionCode)) .append("; ETag: ") .append(ToStdString(putObjOutcome.GetResult().GetETag())); Logging::Log(m_logCb, Level::Info, msg.c_str(), this); // create Lambda layer msg = std::string("GameKitFeatureResources::UploadFeatureLayers() Creating Lambda Layer for ").append(layerDirName); auto layerCreationOutcome = createFeatureLayer(layerDirName, objectName); if (!layerCreationOutcome.IsSuccess()) { return GAMEKIT_ERROR_LAYER_CREATION_FAILED; } // get latest version ARN and set it in parameter store std::string latestArn = ToStdString(layerCreationOutcome.GetResult().GetLayerVersionArn()); unsigned int paramWriteResult = createAndSetLambdaLayerArn(layerDirName, latestArn); if (paramWriteResult != GAMEKIT_SUCCESS) { return paramWriteResult; } } } } Logging::Log(m_logCb, Level::Verbose, "End UploadFeatureLayers()", this); return GAMEKIT_SUCCESS; } unsigned int GameKitFeatureResources::DeployFeatureLayers() { unsigned int result = CreateAndSetLayersReplacementId(); if (result != GameKit::GAMEKIT_SUCCESS) { return result; } result = CompressFeatureLayers(); if (result != GameKit::GAMEKIT_SUCCESS) { CleanupTempFiles(); return result; } result = UploadFeatureLayers(); if (result != GameKit::GAMEKIT_SUCCESS) { CleanupTempFiles(); return result; } CleanupTempFiles(); return result; } unsigned int GameKitFeatureResources::CompressFeatureFunctions() { // loop through the feature's functions directory std::string functionName; Aws::UniquePtr<Zipper> zipper; const fs::path p(m_instanceFunctionsPath); fs::directory_iterator endIterator; // Verify that path exists and is a directory if (fs::exists(p) && fs::is_directory(p)) { for (fs::directory_iterator dirIterator(p); dirIterator != endIterator; ++dirIterator) { // create zip file for every function directory const fs::path cp = (*dirIterator); // Verify that path is a directory if (fs::is_directory(cp)) { // create output directory in temp path std::string tempFunctionsPath = getTempFunctionsPath(); fs::create_directories(tempFunctionsPath); // create zip file functionName = cp.stem().string(); std::string zipFileName = tempFunctionsPath + "/" + functionName + ".zip"; zipper = Aws::MakeUnique<Zipper>(zipFileName.c_str(), cp.string(), zipFileName); bool result = zipper->AddDirectoryToZipFile(cp.string()); if (!result) { std::string msg = std::string("Unable to initialize ").append(zipFileName); Logging::Log(m_logCb, Level::Error, msg.c_str()); return GAMEKIT_ERROR_FUNCTION_ZIP_INIT_FAILED; } // write zip file to disk result = zipper->CloseZipFile(); if (!result) { std::string msg = std::string("Unable to write ").append(zipFileName).append(" to disk"); Logging::Log(m_logCb, Level::Error, msg.c_str(), this); return GAMEKIT_ERROR_FUNCTION_ZIP_WRITE_FAILED; } // zip file creation successful std::string msg = std::string("Zip file ") .append(zipFileName) .append(" created"); Logging::Log(m_logCb, Level::Info, msg.c_str(), this); } } } return GAMEKIT_SUCCESS; } unsigned int GameKitFeatureResources::UploadFeatureFunctions() { Logging::Log(m_logCb, Level::Verbose, "Start UploadFeatureFunctions()", this); // loop through feature functions temp directory const std::string tempFunctionsPath = getTempFunctionsPath(); const fs::path p(tempFunctionsPath); fs::directory_iterator endIterator; // If region name cannot be converted to short region code, return an error (all s3 buckets use 5-letter short region codes) const std::string shortRegionCode = getShortRegionCode(); if (shortRegionCode.empty()) { return GameKit::GAMEKIT_ERROR_REGION_CODE_CONVERSION_FAILED; } const std::string bootstrapBucketName = GetBootstrapBucketName(m_accountInfo, shortRegionCode); // Verify that path exists and is a directory if (fs::exists(p) && fs::is_directory(p)) { for (fs::directory_iterator dirIterator(p); dirIterator != endIterator; ++dirIterator) { assert(GameKit::AwsApiInitializer::IsInitialized()); const fs::path cp = (*dirIterator); // Verify that path is a file if (fs::is_regular_file(cp)) { // set put request params std::string objectName = std::string("functions/") .append(GameKit::GetFeatureTypeString(m_featureType)) .append("/") .append(cp.stem().string()) .append(".") .append(m_functionsReplacementId) .append(cp.extension().string()); std::shared_ptr<Aws::IOStream> inputData = Aws::MakeShared<Aws::FStream>( cp.string().c_str(), cp.native(), std::ios_base::in | std::ios_base::binary); S3Model::PutObjectRequest putObjRequest; putObjRequest.SetExpectedBucketOwner(ToAwsString(m_accountInfo.accountId)); putObjRequest.SetBucket(ToAwsString(bootstrapBucketName)); putObjRequest.SetKey(ToAwsString(objectName)); putObjRequest.SetBody(inputData); // upload zip file Logging::Log(m_logCb, Level::Verbose, "GameKitFeatureResources::UploadFeatureFunctions() Start put object", m_s3Client); auto putObjOutcome = m_s3Client->PutObject(putObjRequest); Logging::Log(m_logCb, Level::Verbose, "GameKitFeatureResources::UploadFeatureFunctions() End put object", m_s3Client); if (putObjOutcome.IsSuccess()) { std::string msg = std::string("Object: ") .append(objectName) .append(" uploaded to: ") .append(bootstrapBucketName.c_str()) .append("; ETag: ") .append(ToStdString(putObjOutcome.GetResult().GetETag())); Logging::Log(m_logCb, Level::Info, msg.c_str(), this); } else { Logging::Log(m_logCb, Level::Error, putObjOutcome.GetError().GetMessage().c_str(), this); return GAMEKIT_ERROR_BOOTSTRAP_BUCKET_UPLOAD_FAILED; } } } } Logging::Log(m_logCb, Level::Verbose, "End UploadFeatureFunctions()", this); return GAMEKIT_SUCCESS; } unsigned int GameKitFeatureResources::DeployFeatureFunctions() { unsigned int result = CreateAndSetFunctionsReplacementId(); if (result != GameKit::GAMEKIT_SUCCESS) { return result; } result = CompressFeatureFunctions(); if (result != GameKit::GAMEKIT_SUCCESS) { CleanupTempFiles(); return result; } result = UploadFeatureFunctions(); if (result != GameKit::GAMEKIT_SUCCESS) { CleanupTempFiles(); return result; } CleanupTempFiles(); return result; } void GameKitFeatureResources::CleanupTempFiles() { if (!m_functionsReplacementId.empty()) { const std::string functionsPath = getTempFunctionsPath(); std::string message = "Deleting temp files from " + functionsPath; Logging::Log(m_logCb, Level::Info, message.c_str()); fs::remove_all(functionsPath); } if (!m_layersReplacementId.empty()) { const std::string layersPath = getTempLayersPath(); std::string message = "Deleting temp files from " + layersPath; Logging::Log(m_logCb, Level::Info, message.c_str()); fs::remove_all(layersPath); } } std::string GameKitFeatureResources::GetCurrentStackStatus() const { const auto describeStackReq = CfnModel::DescribeStacksRequest() .WithStackName(m_stackName.c_str()); auto outcome = m_cfClient->DescribeStacks(describeStackReq); auto stackStatus = CfnModel::StackStatus::NOT_SET; auto stacks = outcome.GetResult().GetStacks(); if (stacks.size() > 0) { stackStatus = stacks.at(0).GetStackStatus(); } if (stackStatus == CfnModel::StackStatus::CREATE_COMPLETE || stackStatus == CfnModel::StackStatus::UPDATE_COMPLETE) { const auto outputs = stacks.at(0).GetOutputs(); const auto writeResult = this->writeClientConfigurationWithOutputs(outputs); if (writeResult != GAMEKIT_SUCCESS) { std::string msg = std::string("Failed to write client configuration parameters for ").append(m_stackName); Logging::Log(m_logCb, Level::Warning, msg.c_str(), this); } } const std::string status = ToStdString(CfnModel::StackStatusMapper::GetNameForStackStatus(stackStatus)); // NOT_SET status maps to an empty string, give an actual status. return status.empty() ? GameKit::ERR_STACK_CURRENT_STATUS_UNDEPLOYED : status; } void GameKitFeatureResources::UpdateDashboardDeployStatus(std::unordered_set<FeatureType> features) const { Aws::String nextToken = ""; auto stackFilter = Aws::Vector<CfnModel::StackStatus>(); stackFilter.push_back(CfnModel::StackStatus::CREATE_COMPLETE); stackFilter.push_back(CfnModel::StackStatus::UPDATE_COMPLETE); GameKitSettings settings = GameKitSettings(m_gamekitRoot, "", m_accountInfo.gameName, m_accountInfo.environment.GetEnvironmentString(), m_logCb); std::map<std::string, std::string> enabledMap = { {"cloudwatch_dashboard_enabled", "true"} }; std::map<std::string, std::string> disabledMap = { {"cloudwatch_dashboard_enabled", "false"} }; std::unordered_set<FeatureType> enabledFeatureDashboards; // Loop for pagination do { // List functioning cloudformation stacks auto listRequest = CfnModel::ListStacksRequest().WithStackStatusFilter(stackFilter); if (!nextToken.empty()) { listRequest.SetNextToken(nextToken); } CfnModel::ListStacksOutcome outcome = m_cfClient->ListStacks(listRequest); if (!outcome.IsSuccess()) { return; } nextToken = outcome.GetResult().GetNextToken(); Aws::Vector<CfnModel::StackSummary> stacks = outcome.GetResult().GetStackSummaries(); for (auto s : stacks) { // Check if this stack matches the name of the dashboard for the feature given Aws::String awsStackName = s.GetStackName(); std::string stackName = ToStdString(awsStackName); for (FeatureType feature : features) { if (stackName.substr(0, getStackName(feature).size()) == getStackName(feature) && (stackName.find("CloudWatchDashboardStack") != Aws::String::npos)) { settings.SetFeatureVariables(feature, enabledMap); enabledFeatureDashboards.insert(feature); } } } } while(!nextToken.empty()); for (FeatureType f : enabledFeatureDashboards) { features.erase(f); } for (FeatureType f : features) { // These features don't have a dashboard deployed, make sure they're set to disabled settings.SetFeatureVariables(f, disabledMap); } settings.SaveSettings(); } unsigned int GameKitFeatureResources::internalDescribeFeatureResources(FuncResourceInfoCallback resourceInfoCb, DISPATCH_RECEIVER_HANDLE receiver, DispatchedResourceInfoCallback dispatchedCb) const { auto describeStackResourcesRequest = CfnModel::DescribeStackResourcesRequest().WithStackName(ToAwsString(m_stackName)); auto outcome = m_cfClient->DescribeStackResources(describeStackResourcesRequest); if (outcome.IsSuccess()) { auto& resources = outcome.GetResult().GetStackResources(); for (auto& resource : resources) { const char* logicalResourceId = resource.GetLogicalResourceId().c_str(); const char* resourceType = resource.GetResourceType().c_str(); CfnModel::ResourceStatus status = resource.GetResourceStatus(); std::string statusStr = ToStdString(CfnModel::ResourceStatusMapper::GetNameForResourceStatus(status)); if (receiver != nullptr && dispatchedCb != nullptr) { dispatchedCb(receiver, logicalResourceId, resourceType, statusStr.c_str()); } else if (resourceInfoCb != nullptr) { resourceInfoCb(logicalResourceId, resourceType, statusStr.c_str()); } } return GameKit::GAMEKIT_SUCCESS; } Logging::Log(m_logCb, Level::Error, outcome.GetError().GetMessage().c_str(), this); return GameKit::GAMEKIT_ERROR_CLOUDFORMATION_DESCRIBE_RESOURCE_FAILED; } unsigned int GameKitFeatureResources::DescribeStackResources(FuncResourceInfoCallback resourceInfoCb) const { return internalDescribeFeatureResources(resourceInfoCb=resourceInfoCb); } unsigned int GameKitFeatureResources::DescribeStackResources(const DISPATCH_RECEIVER_HANDLE dispatchReceiver, DispatchedResourceInfoCallback resourceInfoCb) const { return internalDescribeFeatureResources(nullptr, dispatchReceiver, resourceInfoCb); } std::string GameKitFeatureResources::getFeatureLayerNameFromDirName(const std::string& layerDirName) const { return std::string("gamekit_") .append(m_accountInfo.environment.GetEnvironmentString() .append("_")) .append(m_accountInfo.gameName) .append("_") .append(layerDirName); } LambdaModel::PublishLayerVersionOutcome GameKitFeatureResources::createFeatureLayer(const std::string& layerDirName, const std::string& s3ObjectName) { const auto layerContent = LambdaModel::LayerVersionContentInput() .WithS3Bucket(ToAwsString(GetBootstrapBucketName(m_accountInfo, getShortRegionCode()))) .WithS3Key(ToAwsString(s3ObjectName)); const std::string layerName = getFeatureLayerNameFromDirName(layerDirName); const auto publishRequest = LambdaModel::PublishLayerVersionRequest() .AddCompatibleRuntimes(LambdaModel::Runtime::python3_7) .WithContent(layerContent) .WithDescription(ToAwsString(GameKit::GetFeatureTypeString(m_featureType).append(" ").append("Lambda Layer ").append(layerDirName))) .WithLayerName(ToAwsString(layerName)); return m_lambdaClient->PublishLayerVersion(publishRequest); } bool GameKitFeatureResources::lambdaLayerHashChanged(const std::string& layerName, const std::string& layerHash) const { const std::string paramName = GetLambdaLayerHashParamName(layerName); SSMModel::GetParameterRequest getParamRequest; getParamRequest.SetName(ToAwsString(paramName)); const auto getParamOutcome = m_ssmClient->GetParameter(getParamRequest); if (!getParamOutcome.IsSuccess()) { // SSM returns 400 for all errors except internal server error (500) // Warn if not internal server error, otherwise log as Error by default Level level = Level::Error; std::string defaultErrorMessage = std::string("Lambda Layer hash parameter not found for layer ").append(layerName); if (getParamOutcome.GetError().GetResponseCode() != Aws::Http::HttpResponseCode::INTERNAL_SERVER_ERROR) { level = Level::Warning; defaultErrorMessage.append(". This is expected when you deploy your first GameKit feature."); } // Returned error message may be empty. Use default error message instead. std::string errorMessage = (getParamOutcome.GetError().GetMessage().empty()) ? defaultErrorMessage : ToStdString(getParamOutcome.GetError().GetMessage()); Logging::Log(m_logCb, level, errorMessage.c_str(), this); return false; } const auto lastRecordedHash = getParamOutcome.GetResult().GetParameter(); if (std::string(lastRecordedHash.GetValue()) != layerHash) { return false; } return true; } unsigned int GameKitFeatureResources::createAndSetLambdaLayerHash(const std::string& layerName, const std::string& layerHash) const { const std::string paramName = GetLambdaLayerHashParamName(layerName); SSMModel::PutParameterRequest putParamRequest; putParamRequest.SetType(SSMModel::ParameterType::String); putParamRequest.SetName(ToAwsString(paramName)); putParamRequest.SetValue(ToAwsString(layerHash)); putParamRequest.SetOverwrite(true); const auto putParamOutcome = m_ssmClient->PutParameter(putParamRequest); if (!putParamOutcome.IsSuccess()) { Logging::Log(m_logCb, Level::Error, putParamOutcome.GetError().GetMessage().c_str(), this); return GAMEKIT_ERROR_PARAMSTORE_WRITE_FAILED; } return GAMEKIT_SUCCESS; } unsigned int GameKitFeatureResources::createAndSetLambdaLayerArn(const std::string& layerName, const std::string& layerArn) const { // add GAMEKIT_LAMBDA_LAYER_ARN to Parameter Store with the replacement id const auto paramName = GetLambdaLayerARNParamName(layerName); SSMModel::PutParameterRequest putParamRequest; putParamRequest.SetType(SSMModel::ParameterType::String); putParamRequest.SetName(ToAwsString(paramName)); putParamRequest.SetValue(ToAwsString(layerArn)); putParamRequest.SetOverwrite(true); const auto putParamOutcome = m_ssmClient->PutParameter(putParamRequest); if (!putParamOutcome.IsSuccess()) { Logging::Log(m_logCb, Level::Error, putParamOutcome.GetError().GetMessage().c_str(), this); return GAMEKIT_ERROR_PARAMSTORE_WRITE_FAILED; } return GAMEKIT_SUCCESS; } std::string GameKitFeatureResources::getShortRegionCode() { if (GetPluginRoot().empty()) { return ""; } AwsRegionMappings& regionMappings = AwsRegionMappings::getInstance(GetPluginRoot(), m_logCb); return regionMappings.getFiveLetterRegionCode(m_credentials.region); } unsigned int GameKitFeatureResources::CreateOrUpdateFeatureStack() { const auto describeStackReq = CfnModel::DescribeStacksRequest().WithStackName(ToAwsString(m_stackName)); const auto outcome = m_cfClient->DescribeStacks(describeStackReq); unsigned int createUpdateResult; if (!outcome.IsSuccess()) { createUpdateResult = createStack(); } else { createUpdateResult = updateStack(); } if (createUpdateResult != GAMEKIT_SUCCESS) { return createUpdateResult; } char buffer[256]; snprintf(buffer, 256, "Creating stack resources for stack: %s", m_stackName.c_str()); Logging::Log(m_logCb, Level::Info, buffer, this); const auto stackStatus = periodicallyDescribeStackEvents(); // if last stack status is a failed state or deletion completion or deletion in progress, return a failed creation error code if (isFailedState(stackStatus) || stackStatus == CfnModel::StackStatus::DELETE_IN_PROGRESS || stackStatus == CfnModel::StackStatus::DELETE_COMPLETE) { Logging::Log(m_logCb, Level::Error, "CloudFormation creation failed.", this); return GameKit::GAMEKIT_ERROR_CLOUDFORMATION_RESOURCE_CREATION_FAILED; } // update clientConfig.yml const auto writeStatus = this->WriteClientConfiguration(); if (writeStatus != GAMEKIT_SUCCESS) { snprintf(buffer, 256, "Failed to update clientConfig.yml for feature %s", GetFeatureTypeString(m_featureType).c_str()); Logging::Log(m_logCb, Level::Error, buffer, this); } return GameKit::GAMEKIT_SUCCESS; } std::string GameKitFeatureResources::getClientConfigFilePath() const { const std::string configDirectory = m_gamekitRoot + "/" + m_accountInfo.gameName + "/" + m_accountInfo.environment.GetEnvironmentString(); const std::string configFilePath = configDirectory + "/" + TemplateFileNames::GAMEKIT_CLIENT_CONFIGURATION_FILE; return configFilePath; } unsigned int GameKitFeatureResources::writeClientConfigYamlToDisk(const YAML::Node& paramsYml) const { return Utils::FileUtils::WriteYAMLToFile(paramsYml, getClientConfigFilePath(), Configuration::DO_NOT_EDIT, m_logCb); } unsigned int GameKitFeatureResources::removeOutputsFromClientConfiguration() const { YAML::Node paramsYml = this->getClientConfigYaml(); auto configParams = this->getConfigOutputParameters(); if (configParams.size() == 0) { return GameKit::GAMEKIT_SUCCESS; } for (auto& param : configParams) { std::string paramKey = std::get<0>(param); paramsYml.remove(paramKey); } // Write updated config file return this->writeClientConfigYamlToDisk(paramsYml); } unsigned int GameKitFeatureResources::writeClientConfigurationWithOutputs(Aws::Vector<CfnModel::Output> outputs) const { // Defensively check to make sure we actually are being passed new data and we are not working with Main stack, // otherwise just return success if (outputs.size() == 0 || m_featureType == FeatureType::Main) { return GameKit::GAMEKIT_SUCCESS; } bool newCloudFormationOutputValues = false; // Read feature-specific config settings YAML::Node paramsYml; // For the client config file, "not found" is expected, because it never exists before the first feature's successful deployment. if (!fs::exists(getClientConfigFilePath())) { // Log that new file will be created. std::string msg = std::string("Client Config file not found at ") .append(getClientConfigFilePath()) .append(" . This is expected when you deploy your first GameKit feature. Creating a new one."); Logging::Log(m_logCb, Level::Info, msg.c_str()); WriteEmptyClientConfiguration(); // Set flag so empty file is created even if no config values to append. newCloudFormationOutputValues = true; } else { paramsYml = this->getClientConfigYaml(); } auto configParams = this->getConfigOutputParameters(); for (auto& param : configParams) { std::string paramKey = std::get<0>(param); std::string paramVal = std::get<1>(param); for (auto& output : outputs) { // replace occurrences of CloudFormation output vars AWSGAMEKIT::CFNOUTPUT::* std::regex key(TemplateVars::BEGIN + TemplateVars::AWS_GAMEKIT_CLOUDFORMATION_OUTPUT_PREFIX + ToStdString(output.GetOutputKey()) + TemplateVars::END); paramVal = std::regex_replace(paramVal, key, output.GetOutputValue()); } std::string existingVal = paramsYml[paramKey].Scalar(); if (existingVal != paramVal) { // replacement values in actual config paramsYml[paramKey] = paramVal; newCloudFormationOutputValues = true; } } // prevent unnecessary disk writes by making sure we actually have changes first if (!newCloudFormationOutputValues) { return GAMEKIT_SUCCESS; } return this->writeClientConfigYamlToDisk(paramsYml); } unsigned int GameKitFeatureResources::WriteEmptyClientConfiguration() const { // Empty params since this should only be called when submitting an environment for the first time const YAML::Node paramsYml; return this->writeClientConfigYamlToDisk(paramsYml); } unsigned int GameKitFeatureResources::WriteClientConfiguration() const { // Get stack Outputs const auto describeStackReq = CfnModel::DescribeStacksRequest().WithStackName(ToAwsString(m_stackName)); auto outcome = m_cfClient->DescribeStacks(describeStackReq); if (!outcome.IsSuccess()) { Logging::Log(m_logCb, Level::Error, outcome.GetError().GetMessage().c_str(), this); return GAMEKIT_ERROR_CLOUDFORMATION_DESCRIBE_STACKS_FAILED; } Aws::Vector<CfnModel::Output> outputs; if (outcome.GetResult().GetStacks().size() > 0) { outputs = outcome.GetResult().GetStacks().at(0).GetOutputs(); } if (outputs.size() == 0) { // nothing to use for replacement; just return success return GameKit::GAMEKIT_SUCCESS; } return this->writeClientConfigurationWithOutputs(outputs); } unsigned int GameKitFeatureResources::DeleteFeatureStack() { const auto describeStackReq = CfnModel::DescribeStacksRequest().WithStackName(ToAwsString(m_stackName)); const auto outcome = m_cfClient->DescribeStacks(describeStackReq); unsigned int deleteResult = GAMEKIT_ERROR_CLOUDFORMATION_STACK_DELETE_FAILED; if (outcome.IsSuccess()) { deleteResult = deleteStack(); } if (deleteResult != GAMEKIT_SUCCESS) { return deleteResult; } char buffer[256]; snprintf(buffer, 256, "Deleting stack resources for stack: %s", m_stackName.c_str()); Logging::Log(m_logCb, Level::Info, buffer, this); const auto stackStatus = periodicallyDescribeStackEvents(); // Deleted stacks do not show up in the DescribeStacks API (by stack name) if the deletion has been completed successfully, // so the last status could be DELETE_IN_PROGRESS for successfully deleted stacks. if (stackStatus != CfnModel::StackStatus::DELETE_COMPLETE && stackStatus != CfnModel::StackStatus::DELETE_IN_PROGRESS) { std::string msg = std::string("CloudFormation stack ").append(m_stackName).append(" deletion failed."); Logging::Log(m_logCb, Level::Error, msg.c_str(), this); return GameKit::GAMEKIT_ERROR_CLOUDFORMATION_STACK_DELETE_FAILED; } const auto writeResult = this->removeOutputsFromClientConfiguration(); if (writeResult != GAMEKIT_SUCCESS) { std::string msg = std::string("Failed to delete output parameters from client configuration file for ").append(m_stackName); Logging::Log(m_logCb, Level::Warning, msg.c_str(), this); } return GameKit::GAMEKIT_SUCCESS; } std::string GameKitFeatureResources::GetStackName() const { return getStackName(m_featureType); } std::string GameKitFeatureResources::getStackName(FeatureType featureType) const { const std::string gameName = m_accountInfo.gameName; std::string stackName = "gamekit-"; stackName.append(m_accountInfo.environment.GetEnvironmentString()) .append("-") .append(gameName) .append("-") .append(GetFeatureTypeString(featureType)); return stackName; } #pragma endregion #pragma region Private/Helper Methods Aws::Vector<CfnModel::Parameter> GameKitFeatureResources::getStackParameters(TemplateType templateType) const { auto cfPath = m_baseCloudformationPath; if (templateType == TemplateType::Instance) { cfPath = m_instanceCloudformationPath; } // extract user parameters for the current feature from settings file const GameKitSettings settings = GameKitSettings(m_gamekitRoot, "", m_accountInfo.gameName, m_accountInfo.environment.GetEnvironmentString(), m_logCb); const std::map<std::string, std::string> userParams = settings.GetFeatureVariables(m_featureType); // Replace all instances of AWSGAMEKIT::VARS::* values with user parameter values std::string rawParams = this->getRawStackParameters(templateType); for (const std::pair<std::string, std::string>& entry : userParams) { std::regex key(TemplateVars::BEGIN + TemplateVars::AWS_GAMEKIT_USERVAR_PREFIX + entry.first + TemplateVars::END); rawParams = std::regex_replace(rawParams, key, entry.second); } YAML::Node paramsYml = YAML::Load(rawParams); // read parameters and put them in a vector Aws::Vector<CfnModel::Parameter> params; for (YAML::const_iterator it = paramsYml.begin(); it != paramsYml.end(); ++it) { CfnModel::Parameter p; p.SetParameterKey(it->first.as<std::string>().c_str()); p.SetParameterValue(it->second["value"].as<std::string>().c_str()); params.push_back(p); } return params; } std::string GameKitFeatureResources::getRawStackParameters(TemplateType templateType) const { auto cfPath = m_baseCloudformationPath; if (templateType == TemplateType::Instance) { cfPath = m_instanceCloudformationPath; } std::string loadedString; GameKit::Utils::FileUtils::ReadFileIntoString(cfPath + TemplateFileNames::PARAMETERS_FILE, loadedString); return loadedString; } std::string GameKitFeatureResources::getFeatureDashboardTemplate(TemplateType templateType) const { auto cfPath = m_baseCloudformationPath; if (templateType == TemplateType::Instance) { cfPath = m_instanceCloudformationPath; } std::string loadedString; GameKit::Utils::FileUtils::ReadFileIntoString(cfPath + TemplateFileNames::FEATURE_DASHBOARD_FILE, loadedString); return loadedString; } std::string GameKitFeatureResources::getCloudFormationTemplate(TemplateType templateType) const { auto cfPath = m_baseCloudformationPath; if (templateType == TemplateType::Instance) { cfPath = m_instanceCloudformationPath; } std::string loadedString; GameKit::Utils::FileUtils::ReadFileIntoString(cfPath + TemplateFileNames::CLOUDFORMATION_FILE, loadedString); return loadedString; } unsigned int GameKitFeatureResources::createStack() const { char buffer[256]; snprintf(buffer, 256, "Creating stack: %s", m_stackName.c_str()); Logging::Log(m_logCb, Level::Info, buffer); auto createStackReq = CfnModel::CreateStackRequest(); createStackReq.SetStackName(ToAwsString(m_stackName)); createStackReq.SetTemplateBody(ToAwsString(this->getCloudFormationTemplate(TemplateType::Instance))); createStackReq.SetParameters(this->getStackParameters(TemplateType::Instance)); createStackReq.AddCapabilities(CfnModel::Capability::CAPABILITY_IAM); createStackReq.AddCapabilities(CfnModel::Capability::CAPABILITY_NAMED_IAM); createStackReq.SetOnFailure(CfnModel::OnFailure::DELETE_); auto futureOutcome = m_cfClient->CreateStackCallable(createStackReq); futureOutcome.wait(); unsigned int createStackResult = GAMEKIT_SUCCESS; auto outcome = futureOutcome.get(); Level level = Level::Info; if (outcome.IsSuccess()) { snprintf(buffer, 256, "CreateStack Successful; StackId: %s", outcome.GetResult().GetStackId().c_str()); } else { snprintf(buffer, 256, "CreateStack Failed: %s", outcome.GetError().GetMessage().c_str()); level = Level::Error; createStackResult = GAMEKIT_ERROR_CLOUDFORMATION_RESOURCE_CREATION_FAILED; } Logging::Log(m_logCb, level, buffer, this); return createStackResult; } unsigned int GameKitFeatureResources::updateStack() const { char buffer[256]; snprintf(buffer, 256, "Updating stack: %s", m_stackName.c_str()); Logging::Log(m_logCb, Level::Info, buffer); auto updateStackReq = CfnModel::UpdateStackRequest(); updateStackReq.SetStackName(ToAwsString(m_stackName)); updateStackReq.SetTemplateBody(ToAwsString(this->getCloudFormationTemplate(TemplateType::Instance))); updateStackReq.SetParameters(this->getStackParameters(TemplateType::Instance)); updateStackReq.AddCapabilities(CfnModel::Capability::CAPABILITY_IAM); updateStackReq.AddCapabilities(CfnModel::Capability::CAPABILITY_NAMED_IAM); auto futureOutcome = m_cfClient->UpdateStackCallable(updateStackReq); futureOutcome.wait(); auto updateStackResult = GAMEKIT_SUCCESS; auto outcome = futureOutcome.get(); enum Level level = Level::Info; if (outcome.IsSuccess()) { snprintf(buffer, 256, "UpdateStack Successful; StackId: %s", outcome.GetResult().GetStackId().c_str()); } else { snprintf(buffer, 256, "UpdateStack Failed: %s", outcome.GetError().GetMessage().c_str()); level = Level::Error; // If the update failed because there are not CloudFormation changes, return GAMEKIT_SUCCESS auto updateError = outcome.GetError(); if (updateError.GetErrorType() == Aws::CloudFormation::CloudFormationErrors::VALIDATION && updateError.GetMessage() == "No updates are to be performed.") { updateStackResult = GAMEKIT_SUCCESS; } else { Logging::Log(m_logCb, Level::Error, updateError.GetMessage().c_str()); updateStackResult = GAMEKIT_ERROR_CLOUDFORMATION_STACK_UPDATE_FAILED; } } Logging::Log(m_logCb, level, buffer, this); return updateStackResult; } unsigned int GameKitFeatureResources::deleteStack() const { char buffer[256]; snprintf(buffer, 256, "Deleting stack: %s", m_stackName.c_str()); Logging::Log(m_logCb, Level::Info, buffer); auto deleteStackReq = CfnModel::DeleteStackRequest(); deleteStackReq.SetStackName(ToAwsString(m_stackName)); auto futureOutcome = m_cfClient->DeleteStackCallable(deleteStackReq); futureOutcome.wait(); auto deleteStackResult = GAMEKIT_SUCCESS; auto outcome = futureOutcome.get(); enum Level level = Level::Info; if (outcome.IsSuccess()) { snprintf(buffer, 256, "DeleteStack Started; StackName: %s", m_stackName.c_str()); } else { snprintf(buffer, 256, "DeleteStack Failed: %s", outcome.GetError().GetMessage().c_str()); level = Level::Error; deleteStackResult = GAMEKIT_ERROR_CLOUDFORMATION_STACK_DELETE_FAILED; } Logging::Log(m_logCb, level, buffer, this); return deleteStackResult; } CfnModel::StackStatus GameKitFeatureResources::periodicallyDescribeStackEvents() { const auto describeStackReq = CfnModel::DescribeStacksRequest().WithStackName(ToAwsString(m_stackName)); auto outcome = m_cfClient->DescribeStacks(describeStackReq); auto stackStatus = CfnModel::StackStatus::NOT_SET; if (outcome.GetResult().GetStacks().size() > 0) { stackStatus = outcome.GetResult().GetStacks().at(0).GetStackStatus(); } // get first description of stack events (we may not even go into the loop below if process is already complete) describeStackEvents(); while (outcome.IsSuccess() && !isTerminalState(stackStatus)) { outcome = m_cfClient->DescribeStacks(describeStackReq); if (outcome.GetResult().GetStacks().size() > 0) { stackStatus = outcome.GetResult().GetStacks().at(0).GetStackStatus(); } // get stack events describeStackEvents(); std::this_thread::sleep_for(std::chrono::seconds(1)); } return stackStatus; } void GameKitFeatureResources::describeStackEvents() { auto describeStackEventsReq = CfnModel::DescribeStackEventsRequest(); describeStackEventsReq.SetStackName(ToAwsString(m_stackName)); auto futureOutcome = m_cfClient->DescribeStackEventsCallable(describeStackEventsReq); futureOutcome.wait(); auto describeStackOutcome = futureOutcome.get(); auto resourceStatus = CfnModel::ResourceStatusMapper::GetNameForResourceStatus(CfnModel::ResourceStatus::NOT_SET); auto events = describeStackOutcome.GetResult().GetStackEvents(); if (events.size() > 0) { auto event = events.at(0); resourceStatus = CfnModel::ResourceStatusMapper::GetNameForResourceStatus(event.GetResourceStatus()); Aws::String resourceId = event.GetLogicalResourceId(); if (!m_resouceStatusMap[ToStdString(resourceId)]) { m_resouceStatusMap[ToStdString(resourceId)] = true; char buffer[1024] = ""; snprintf(buffer, 1024, "%s: %s | %s: %s", m_stackName.c_str(), resourceId.c_str(), resourceStatus.c_str(), event.GetResourceStatusReason().c_str()); Logging::Log(m_logCb, Level::Info, buffer, this); } } } unsigned int GameKitFeatureResources::getDeployedTemplateBody(const std::string& stackName, std::string& templateBody) const { const auto getTemplateRequest = CfnModel::GetTemplateRequest().WithStackName(ToAwsString(stackName)); auto getTemplateOutcome = m_cfClient->GetTemplate(getTemplateRequest); if (!getTemplateOutcome.IsSuccess()) { return GAMEKIT_ERROR_CLOUDFORMATION_GET_TEMPLATE_FAILED; } templateBody = getTemplateOutcome.GetResult().GetTemplateBody(); return GAMEKIT_SUCCESS; } bool GameKitFeatureResources::isTerminalState(CfnModel::StackStatus status) { return status == CfnModel::StackStatus::CREATE_FAILED || status == CfnModel::StackStatus::CREATE_COMPLETE || status == CfnModel::StackStatus::ROLLBACK_FAILED || status == CfnModel::StackStatus::ROLLBACK_COMPLETE || status == CfnModel::StackStatus::DELETE_FAILED || status == CfnModel::StackStatus::DELETE_COMPLETE || status == CfnModel::StackStatus::UPDATE_COMPLETE || status == CfnModel::StackStatus::UPDATE_ROLLBACK_FAILED || status == CfnModel::StackStatus::UPDATE_ROLLBACK_COMPLETE || status == CfnModel::StackStatus::IMPORT_COMPLETE || status == CfnModel::StackStatus::IMPORT_ROLLBACK_FAILED || status == CfnModel::StackStatus::IMPORT_ROLLBACK_COMPLETE; } bool GameKitFeatureResources::isFailedState(CfnModel::StackStatus status) { return status == CfnModel::StackStatus::CREATE_FAILED || status == CfnModel::StackStatus::ROLLBACK_FAILED || status == CfnModel::StackStatus::DELETE_FAILED || status == CfnModel::StackStatus::UPDATE_ROLLBACK_FAILED || status == CfnModel::StackStatus::IMPORT_ROLLBACK_FAILED; } std::string GameKitFeatureResources::getTempLayersPath() const { return fs::temp_directory_path() .append("gamekit_layers") .append(m_layersReplacementId) .append(GameKit::GetFeatureTypeString(m_featureType)).string(); } std::string GameKitFeatureResources::getTempFunctionsPath() const { return fs::temp_directory_path() .append("gamekit_functions") .append(m_functionsReplacementId) .append(GameKit::GetFeatureTypeString(m_featureType)).string(); } YAML::Node GameKitFeatureResources::getClientConfigYaml() const { YAML::Node node; Utils::FileUtils::ReadFileAsYAML(getClientConfigFilePath(), node, m_logCb); return node; } std::vector<std::tuple<std::string, std::string>> GameKitFeatureResources::getConfigOutputParameters() const { std::vector<std::tuple<std::string, std::string>> params; const auto configPath = m_baseConfigOutputsPath + TemplateFileNames::FEATURE_CLIENT_CONFIGURATION_FILE; YAML::Node paramsYml; Utils::FileUtils::ReadFileAsYAML(configPath, paramsYml, m_logCb); for (YAML::const_iterator it = paramsYml.begin(); it != paramsYml.end(); ++it) { std::tuple<std::string, std::string> t = std::make_tuple(it->first.as<std::string>(), it->second.as<std::string>()); params.push_back(t); } return params; } unsigned int GameKitFeatureResources::writeCloudFormationParameterInstance(const std::string& cfParams) const { std::string logMsg; boost::filesystem::create_directories(m_instanceCloudformationPath); const auto writeResult = GameKit::Utils::FileUtils::WriteStringToFile(cfParams, m_instanceCloudformationPath + TemplateFileNames::PARAMETERS_FILE, m_logCb); if (writeResult != GAMEKIT_SUCCESS) { logMsg = std::string("Failed to saved parameters file to ").append(m_instanceCloudformationPath); Logging::Log(m_logCb, Level::Error, logMsg.c_str(), this); return GAMEKIT_ERROR_PARAMETERS_FILE_SAVE_FAILED; } logMsg = std::string("Parameters file saved to ").append(m_instanceCloudformationPath); Logging::Log(m_logCb, Level::Info, logMsg.c_str(), this); return GAMEKIT_SUCCESS; } unsigned int GameKitFeatureResources::writeCloudFormationTemplateInstance(const std::string& cfTemplate) const { std::string logMsg; boost::filesystem::create_directories(m_instanceCloudformationPath); const auto writeResult = GameKit::Utils::FileUtils::WriteStringToFile(cfTemplate, m_instanceCloudformationPath + TemplateFileNames::CLOUDFORMATION_FILE, m_logCb); if (writeResult != GAMEKIT_SUCCESS) { logMsg = std::string("Failed to saved CloudFormation file to ").append(m_instanceCloudformationPath); Logging::Log(m_logCb, Level::Error, logMsg.c_str(), this); return GAMEKIT_ERROR_CLOUDFORMATION_FILE_SAVE_FAILED; } logMsg = std::string("CloudFormation file saved to ").append(m_instanceCloudformationPath); Logging::Log(m_logCb, Level::Info, logMsg.c_str(), this); return GAMEKIT_SUCCESS; } unsigned int GameKitFeatureResources::writeCloudFormationDashboardInstance(const std::string& cfDashboard) const { std::string logMsg; boost::filesystem::create_directories(m_instanceCloudformationPath); auto writeResult = GameKit::Utils::FileUtils::WriteStringToFile(cfDashboard, m_instanceCloudformationPath + TemplateFileNames::FEATURE_DASHBOARD_FILE, m_logCb); if (writeResult != GAMEKIT_SUCCESS) { logMsg = std::string("Failed to saved CloudFormation Dashboard file to ").append(m_instanceCloudformationPath); Logging::Log(m_logCb, Level::Error, logMsg.c_str(), this); return GAMEKIT_ERROR_CLOUDFORMATION_FILE_SAVE_FAILED; } logMsg = std::string("CloudFormation Dashboard file saved to ").append(m_instanceCloudformationPath); Logging::Log(m_logCb, Level::Info, logMsg.c_str(), this); return GAMEKIT_SUCCESS; } unsigned int GameKitFeatureResources::ConditionallyCreateOrUpdateFeatureResources(FeatureType targetFeature, const DISPATCH_RECEIVER_HANDLE dispatchReceiver, const CharPtrCallback responseCallback) { unsigned int result = GAMEKIT_SUCCESS; FeatureStatus stackStatus = GetFeatureStatusFromCloudFormationStackStatus(GetCurrentStackStatus()); if (stackStatus == FeatureStatus::Running) { if (dispatchReceiver != nullptr && responseCallback != nullptr) { const std::string output = "The AWS resources for this game feature are currently being updated by another user."; responseCallback(dispatchReceiver, output.c_str()); } return GAMEKIT_SUCCESS; } if (stackStatus == FeatureStatus::Undeployed) { if (boost::filesystem::exists(m_instanceLayersPath)) { std::string logMsg = std::string("Using existing Lambda layer instance files."); Logging::Log(m_logCb, Level::Info, logMsg.c_str(), this); } else { result = SaveLayerInstances(); if (result != GAMEKIT_SUCCESS) { if (dispatchReceiver != nullptr && responseCallback != nullptr) { const std::string output = "Unable to retrieve deployed Lambda Layers for feature"; responseCallback(dispatchReceiver, output.c_str()); } return result; } } if (boost::filesystem::exists(m_instanceFunctionsPath)) { std::string logMsg = std::string("Using existing Lambda Function instance files."); Logging::Log(m_logCb, Level::Info, logMsg.c_str(), this); } else { result = SaveFunctionInstances(); if (result != GAMEKIT_SUCCESS) { if (dispatchReceiver != nullptr && responseCallback != nullptr) { const std::string output = "Unable to retrieve deployed Lambda Function for feature"; responseCallback(dispatchReceiver, output.c_str()); } return result; } } } if (!IsCloudFormationInstanceTemplatePresent()) { result = SaveDeployedCloudFormationTemplate(); if (result != GAMEKIT_SUCCESS) { if (dispatchReceiver != nullptr && responseCallback != nullptr) { const std::string output = "Unable to retrieve deployed CloudFormation template for feature"; responseCallback(dispatchReceiver, output.c_str()); } return result; } } GameKit::GameKitAccount gamekitAccount(m_accountInfo, m_credentials, m_logCb); gamekitAccount.SetPluginRoot(m_pluginRoot); gamekitAccount.SetGameKitRoot(m_gamekitRoot); gamekitAccount.InitializeDefaultAwsClients(); result = gamekitAccount.UploadDashboards(); if (result != GAMEKIT_SUCCESS) { if (dispatchReceiver != nullptr && responseCallback != nullptr) { const std::string output = "Failed to upload Dashboard"; responseCallback(dispatchReceiver, output.c_str()); } return result; } result = UploadFeatureLayers(); if (result != GAMEKIT_SUCCESS) { if (dispatchReceiver != nullptr && responseCallback != nullptr) { const std::string output = "Failed to upload feature layers"; responseCallback(dispatchReceiver, output.c_str()); } return result; } result = UploadFeatureFunctions(); if (result != GAMEKIT_SUCCESS) { if (dispatchReceiver != nullptr && responseCallback != nullptr) { const std::string output = "Failed to upload feature functions"; responseCallback(dispatchReceiver, output.c_str()); } return result; } result = CreateOrUpdateFeatureStack(); if (result != GAMEKIT_SUCCESS) { if (dispatchReceiver != nullptr && responseCallback != nullptr) { const std::string output = "Failed to create feature stack"; responseCallback(dispatchReceiver, output.c_str()); } return result; } result = gamekitAccount.DeployApiGatewayStage(); if (result != GAMEKIT_SUCCESS) { if (dispatchReceiver != nullptr && responseCallback != nullptr) { const std::string output = "Failed to deploy API Gateway"; responseCallback(dispatchReceiver, output.c_str()); } return result; } return result; } #pragma endregion