parser-service/recommender.js (164 lines of code) (raw):

/** * Copyright 2019 Google LLC * * 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 * * https://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. */ /** * Recommender.js (module) handles all tasks related to recommendations such * as fetching recommendations, updating recommendation state and parsing * recommendations. */ import { google } from 'googleapis'; import axios from 'axios'; import sampleRecommendationVM from './stub/vm.json' assert {type:'json'}; import sampleRecommendationIAM from './stub/iam.json' assert {type:'json'}; /** * Asynchronously fetches and returns a Google authentication client. * * @returns {Promise<GoogleAuth>} A promise that resolves to the Google authentication client. */ const fetchAuthClient = async () => { const auth = new google.auth.GoogleAuth({ scopes: ['https://www.googleapis.com/auth/cloud-platform'], }); return await auth.getClient(); }; /** * Fetches recommendation stubs for testing purposes. * * @param {string} type The type of recommendation to fetch ('VM' or 'IAM'). * @returns {Promise<Object>} A promise that resolves to the recommendation data. */ const fetchRecommendationsStub = async (type) => type === 'VM' ? sampleRecommendationVM : sampleRecommendationIAM; /** * Asynchronously fetches recommendations from the Google Recommender API or stubs based on the specified type. * * @param {string} type The type of recommendations to fetch ('VM' or 'IAM'). * @param {Array<string>} projects An array of project IDs for which recommendations are fetched. * @param {boolean} stub Determines whether to use stub data for recommendations. * @param {string} location The location for which recommendations are fetched. * @returns {Promise<Array>} A promise that resolves to an array of recommendations. */ const fetchRecommendations = async (type, projects, stub, location) => { if (stub) return fetchRecommendationsStub(type); const authClient = await fetchAuthClient(); const accessToken = await authClient.getAccessToken(); const typeURLPath = type === 'IAM' ? 'google.iam.policy.Recommender' : 'google.compute.instance.MachineTypeRecommender'; const recommendationPromises = projects.map((project) => axios.get( `https://recommender.googleapis.com/v1beta1/projects/${project}/locations/${location}/recommenders/${typeURLPath}/recommendations`, { headers: { Authorization: `Bearer ${accessToken.token}`, 'x-goog-user-project': project, }, }) ); const recommendationsFromAPI = await Promise.all(recommendationPromises); return recommendationsFromAPI .map(({ data }) => data) .filter((r) => r.recommendations) .flatMap((r) => r.recommendations); }; /** * Lists VM resize recommendations for the given project IDs. * * @param {Array<string>} projectIDs An array of project IDs to fetch VM resize recommendations for. * @param {boolean} isStub Whether to use stub data. * @param {string} location The location for the recommendations. * @returns {Promise<Array>} A promise that resolves to an array of VM resize recommendations. */ const listVMResizeRecommendations = async (projectIDs, isStub, location) => { const recommendations = await fetchRecommendations('VM', projectIDs, isStub, location); const vmsToSize = filterVMSizeRecommendations(recommendations); console.log('Completed listVMResizeRecommendations', JSON.stringify(vmsToSize)); return vmsToSize; }; /** * Lists IAM recommendations for the given project IDs. * * @param {Array<string>} projectIDs An array of project IDs to fetch IAM recommendations for. * @param {boolean} isStub Whether to use stub data. * @param {string} location The location for the recommendations. * @returns {Promise<Array>} A promise that resolves to an array of IAM recommendations. */ const listIAMRecommendations = async (projectIDs, isStub, location) => { const recommendations = await fetchRecommendations('IAM', projectIDs, isStub, location); const iamRecommendations = filterIAMRecommendations(recommendations); console.log('Completed listIAMRecommendations', JSON.stringify(iamRecommendations)); return iamRecommendations; }; /** * Sets the status for a list of recommendations. * * @param {Array<Object>} recommendationsIDsAndETags An array of objects containing recommendation IDs and their corresponding ETags. * @param {string} newStatus The new status to set for the recommendations. * @returns {Promise<void>} A promise that resolves when the status updates are complete. */ const setRecommendationStatus = async (recommendationsIDsAndETags, newStatus) => { const authClient = await fetchAuthClient(); const accessToken = await authClient.getAccessToken(); const promises = recommendationsIDsAndETags.map(({ id, etag }) => axios.post( `https://recommender.googleapis.com/v1beta1/${id}:${newStatus}`, { etag, }, { headers: { Authorization: `Bearer ${accessToken.token}`, 'x-goog-user-project': id.split('/')[1], }, }).catch(function (error) { if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx console.log(error.response.data); console.log(error.response.status); console.log(error.response.headers); } else if (error.request) { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of // http.ClientRequest in node.js console.log(error.request); } else { // Something happened in setting up the request that triggered an Error console.log('Error', error.message); } console.log(error.config); }) ); await Promise.all(promises); }; /** * Fetches recommendation details for a given array of recommendation IDs. * * @param {Array<string>} recommendationIDs An array of recommendation IDs to fetch details for. * @returns {Promise<Array>} A promise that resolves to an array of recommendation details. */ const getRecommendations = async (recommendationIDs) => { const authClient = await fetchAuthClient(); const accessToken = await authClient.getAccessToken(); const promises = recommendationIDs.map((id) => axios.get( `https://recommender.googleapis.com/v1beta1/${id}`, { headers: { Authorization: `Bearer ${accessToken.token}`, 'x-goog-user-project': id.split('/')[1], }, } )); const results = await Promise.all(promises); return results.map(({ data }) => data); }; /** * Review the recommendations payload to create an array of instances for * active recommendations. This array will comprise of the instance selfLink and * recommended machine type * @param recommendations is a list of recommendations from the API * [{ name: string, description: string, stateInfo: object, etag: string * lastRefreshTime: datetime, content: object }] * @return VMS to resize * [{ instanceID: string, size: string, recommendationID: string * recommendationETAG: string}] * */ const filterVMSizeRecommendations = (recommendations) => { const vmsToResize = [] recommendations.forEach(recommendation => { if (recommendation.stateInfo.state == "ACTIVE") { recommendation.content.operationGroups.forEach(group => { group.operations.forEach(operation => { if (operation.action == 'replace' && operation.resourceType == 'compute.googleapis.com/Instance' && operation.path == '/machineType') { const value = operation.value.split('/') const size = value[value.length - 1] const recommendationID = recommendation.name const recommendationETAG = recommendation.etag vmsToResize.push({ instanceID: operation.resource, size, recommendationID, recommendationETAG }) } }) }) } }) return vmsToResize } /** * Filters and processes IAM recommendations from a given list of recommendations. * * @param {Array<Object>} recommendations An array of recommendation objects to be processed. * @returns {Array<Object>} An array of processed IAM recommendations. */ const filterIAMRecommendations = (recommendations) => { const removeRecommendations = [] recommendations.forEach(recommendation => { if (recommendation.stateInfo.state == 'ACTIVE') { recommendation.content.operationGroups.forEach(group => { group.operations.forEach(operation => { if (operation.action == 'remove' && operation.resourceType == 'cloudresourcemanager.googleapis.com/Project' && operation.path == '/iamPolicy/bindings/*/members/*') { const project = recommendation.name.split('/')[1] const member = operation.pathFilters["/iamPolicy/bindings/*/members/*"] const role = operation.pathFilters["/iamPolicy/bindings/*/role"] const recommendationID = recommendation.name const recommendationETAG = recommendation.etag let add = '' // Find add recommendations for the same remove group.operations.forEach(operation => { if (operation.action == 'add' && operation.resourceType == 'cloudresourcemanager.googleapis.com/Project' && operation.path == '/iamPolicy/bindings/*/members/-' && operation.value == member && operation.resource.split('/').pop() == project ) { add = operation.pathFilters["/iamPolicy/bindings/*/role"] } }) removeRecommendations.push({ project, member, role: processRole(role), add: add ? processRole(add) : '', recommendationID, recommendationETAG }) } }) }) } }) return removeRecommendations } /** * Processes and formats a role string from its full path. * * @param {string} role The full path of the role to be processed. * @returns {string} The processed role string. */ const processRole = (role) => { const splitPortions = role.split('/'); return `roles/${splitPortions[splitPortions.length - 1]}`; }; export { listVMResizeRecommendations, listIAMRecommendations, setRecommendationStatus, getRecommendations, };