agent/envoy_bootstrap/platforminfo/platform_info_collector.go (282 lines of code) (raw):
// 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. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 platforminfo
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os/exec"
"strconv"
"strings"
"github.com/aws/aws-app-mesh-agent/agent/client"
"github.com/aws/aws-app-mesh-agent/agent/envoy_bootstrap/env"
log "github.com/sirupsen/logrus"
)
const (
metadataNamespace = "aws.appmesh.platformInfo"
// K8s Info
k8sVersionEnvVar = "APPMESH_PLATFORM_K8S_VERSION"
podUidEnvVar = "APPMESH_PLATFORM_K8S_POD_UID"
appMeshControllerVersionEnvVar = "APPMESH_PLATFORM_APP_MESH_CONTROLLER_VERSION"
k8sPlatformInfoKey = "k8sPlatformInfo"
k8sVersionKey = "k8sVersion"
podUidKey = "podUid"
appMeshControllerVersionKey = "appMeshControllerVersion"
// ECS Info
ecsExecutionEnvVar = "AWS_EXECUTION_ENV"
ecsContainerMetadataUriEnv = "ECS_CONTAINER_METADATA_URI"
ecsContainerMetadataUriV4Env = "ECS_CONTAINER_METADATA_URI_V4"
ecsContainerMetadataTaskPath = "/task"
ecsPlatformInfoKey = "ecsPlatformInfo"
ecsLaunchTypeKey = "ecsLaunchType"
ecsClusterArnKey = "ecsClusterArn"
ecsTaskArnKey = "ecsTaskArn"
ecsEnvoyContainerCpuLimit = "CPU"
ecsEnvoyContainerMemoryLimit = "Memory"
ecsContainerInstanceArnEnvVar = "ECS_CONTAINER_INSTANCE_ARN"
ecsContainerInstanceArnKey = "ecsContainerInstanceArn"
// Platform independent information
ec2MetadataUriEnvForTesting = "EC2_METADATA_HOST_ONLY_FOR_TESTING"
ec2MetadataHost = "http://169.254.169.254"
azQuery = "placement/availability-zone"
azIDQuery = "placement/availability-zone-id"
AvailabilityZoneKey = "AvailabilityZone"
AvailabilityZoneIDKey = "AvailabilityZoneID"
supportedIPFamiliesKey = "supportedIPFamilies"
ec2MetadataTokenResource = "/latest/api/token"
ec2ImdsTokenHeader = "X-aws-ec2-metadata-token"
ec2ImdsTokenTtlHeader = "X-aws-ec2-metadata-token-ttl-seconds"
// System Information
systemInfoKey = "systemInfo"
sysPlatformKey = "systemPlatform"
sysKernelVersionKey = "systemKernelVersion"
)
func buildMetadataForK8sPlatform(mapping map[string]interface{}) {
k8sVersion := env.Get(k8sVersionEnvVar)
podUid := env.Get(podUidEnvVar)
appMeshControllerVersion := env.Get(appMeshControllerVersionEnvVar)
// TODO: Add EKS cluster info when available
if k8sVersion != "" && podUid != "" && appMeshControllerVersion != "" {
mapping[k8sPlatformInfoKey] = map[string]interface{}{
k8sVersionKey: k8sVersion,
podUidKey: podUid,
appMeshControllerVersionKey: appMeshControllerVersion,
}
// Since IMDS is not accessible from inside ECS, making below 2 calls only on EKS platform.
// Fetch AZ from EC2 instance metadata if possible.
if availabilityZone, err := getEc2InstanceMetadata(azQuery); err != nil {
log.Warnf("Couldn't determine the AZ due to: %v", err)
} else if availabilityZone != "" {
mapping[AvailabilityZoneKey] = availabilityZone
}
// Fetch AZ ID info as AZ can map differently for each account but AZ IDs are the same for
// every account https://docs.aws.amazon.com/ram/latest/userguide/working-with-az-ids.html
if availabilityZoneID, err := getEc2InstanceMetadata(azIDQuery); err != nil {
// Just log info if we can't get this information
log.Warnf("Couldn't determine the AZ ID due to: %v", err)
} else if availabilityZoneID != "" {
mapping[AvailabilityZoneIDKey] = availabilityZoneID
}
}
}
func buildMetadataForEcsPlatform(mapping map[string]interface{}) {
// ECS platform information
// Networks info: supportedIPFamilies, it's not an ECS only info, for others we may also need to set this
supportedIPFamilies := ""
ecsLaunchType := env.Get(ecsExecutionEnvVar)
if ecsLaunchType != "" {
ecsMetadata := map[string]interface{}{
ecsLaunchTypeKey: ecsLaunchType,
}
ecsContainerInstanceArn := env.Get(ecsContainerInstanceArnEnvVar)
if ecsContainerInstanceArn != "" {
ecsMetadata[ecsContainerInstanceArnKey] = ecsContainerInstanceArn
}
// Look for V4 URI first and fallback on V3 URI
ecsContainerMetadataUri := env.Or(ecsContainerMetadataUriV4Env, env.Get(ecsContainerMetadataUriEnv))
// Get ECS container metadata
if ecsContainerMetadataUri != "" {
getEcsContainerMetadata(ecsContainerMetadataUri+ecsContainerMetadataTaskPath, ecsMetadata)
getEcsEnvoyContainerMetadata(ecsContainerMetadataUri, ecsMetadata)
supportedIPFamilies = getEcsContainerSupportedIPFamilies(ecsContainerMetadataUri + ecsContainerMetadataTaskPath)
}
// The AZ info is available from ECS container metadata itself
if availabilityZone, exists := ecsMetadata[AvailabilityZoneKey]; exists {
mapping[AvailabilityZoneKey] = availabilityZone
delete(ecsMetadata, AvailabilityZoneKey)
}
// Build SupportedIPFamilies info in platform
if supportedIPFamilies != "" {
mapping[supportedIPFamiliesKey] = supportedIPFamilies
}
mapping[ecsPlatformInfoKey] = ecsMetadata
}
}
func buildMetadataFromSystemInfo(mapping map[string]interface{}) {
// System information
systemInfo := make(map[string]interface{})
if platform, err := RunCommand("uname", "-p"); err != nil {
log.Errorf("Unable to get system platform info: %v", err)
} else {
systemInfo[sysPlatformKey] = platform
}
if kernelVersion, err := RunCommand("uname", "-r"); err != nil {
log.Errorf("Unable to get system kernel version: %v", err)
} else {
systemInfo[sysKernelVersionKey] = kernelVersion
}
if len(systemInfo) > 0 {
mapping[systemInfoKey] = systemInfo
}
}
func BuildMetadata() (*map[string]interface{}, error) {
md := make(map[string]interface{})
mapping := make(map[string]interface{})
buildMetadataForK8sPlatform(mapping)
buildMetadataForEcsPlatform(mapping)
buildMetadataFromSystemInfo(mapping)
if len(mapping) != 0 {
md[metadataNamespace] = mapping
}
return &md, nil
}
func getEcsContainerMetadata(uri string, ecsMetadata map[string]interface{}) {
metadataMap, err := getEcsMetadata(uri)
if err != nil {
log.Warnf("Failed generating ECS platform info from ECS metadata: %v", err)
return
}
// For reference on all the information that is returned from the task metadata endpoint, see
// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4.html
// Here we only pick the ones that are needed.
if ecsClusterArn := metadataMap["Cluster"]; ecsClusterArn != "" {
ecsMetadata[ecsClusterArnKey] = ecsClusterArn
}
if ecsTaskArn := metadataMap["TaskARN"]; ecsTaskArn != "" {
ecsMetadata[ecsTaskArnKey] = ecsTaskArn
}
if availabilityZone := metadataMap["AvailabilityZone"]; availabilityZone != "" {
ecsMetadata[AvailabilityZoneKey] = availabilityZone
}
}
func getEcsEnvoyContainerMetadata(uri string, ecsMetadata map[string]interface{}) {
response, err := http.Get(uri)
if err != nil {
log.Warnf("Unable to fetch ECS envoy container metadata from %s: %v", uri, err)
return
}
defer response.Body.Close()
responseBody, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Warnf("Unable to read ECS envoy container metadata: %v", err)
return
}
var metadataMap map[string]interface{}
err = json.Unmarshal(responseBody, &metadataMap)
if err != nil {
log.Warnf("Unable to parse ECS envoy container metadata: %v", err)
return
}
// For reference on all the information that is returned from the task metadata endpoint, see
// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4.html
// Here we only pick the ones that are needed.
if CPULimit := fmt.Sprintf("%v", metadataMap["Limits"].(map[string]interface{})["CPU"]); CPULimit != "" {
ecsMetadata[ecsEnvoyContainerCpuLimit] = CPULimit
}
if MemoryLimit := fmt.Sprintf("%v", metadataMap["Limits"].(map[string]interface{})["Memory"]); MemoryLimit != "" {
ecsMetadata[ecsEnvoyContainerMemoryLimit] = MemoryLimit
}
}
func getEcsMetadata(uri string) (map[string]interface{}, error) {
var metadataMap map[string]interface{}
response, err := http.Get(uri)
if err != nil {
log.Warnf("Unable to fetch ECS container metadata from %s: %v", uri, err)
return nil, err
}
defer response.Body.Close()
responseBody, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Warnf("Unable to read ECS container metadata: %v", err)
return nil, err
}
err = json.Unmarshal(responseBody, &metadataMap)
if err != nil {
log.Warnf("Unable to parse ECS container metadata: %s, %v", responseBody, err)
return nil, err
}
return metadataMap, nil
}
func getEcsContainerSupportedIPFamilies(uri string) string {
metadataMap, err := getEcsMetadata(uri)
if err != nil {
log.Warnf("Failed generating SupportedIPFamilies info from ECS metadata: %v", err)
return ""
}
containers := metadataMap["Containers"]
if containers == nil || len(containers.([]interface{})) == 0 {
log.Warnf("Containers info not found in ECS metadata: %v", metadataMap)
return ""
}
// all containers share the same networks
containerInfo := containers.([]interface{})[0]
networks := containerInfo.(map[string]interface{})["Networks"]
if networks == nil || len(networks.([]interface{})) == 0 {
log.Warnf("Networks info not found in container info in ECS metadata: %v", containerInfo)
return ""
}
hasIPv4Addresses := false
hasIPv6Addresses := false
networksArray := networks.([]interface{})
for i := 0; i < len(networksArray); i++ {
if networksArray[i].(map[string]interface{})["IPv4Addresses"] != nil {
hasIPv4Addresses = true
}
if networksArray[i].(map[string]interface{})["IPv6Addresses"] != nil {
hasIPv6Addresses = true
}
}
if hasIPv4Addresses && hasIPv6Addresses {
return "ALL"
}
if hasIPv4Addresses {
return "IPv4_ONLY"
}
if hasIPv6Addresses {
return "IPv6_ONLY"
}
log.Warnf("Neither IPv4 or IPv6 addresses are found in ECS metadata Networks")
return ""
}
func getEc2InstanceMetadata(query string) (string, error) {
httpClient := client.CreateDefaultHttpClient()
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html
// EC2 Instance Metadata url to get the token: http://169.254.169.254/latest/api/token
token := ""
tokenRequestUrl := env.Or(ec2MetadataUriEnvForTesting, ec2MetadataHost) + ec2MetadataTokenResource
tokenRequest, err := client.CreateStandardAgentHttpRequest(http.MethodPut, tokenRequestUrl, nil)
if err != nil {
log.Debugf("unable to create http request: %v. request url: %s", err, tokenRequestUrl)
} else {
// Setting token expiry time to just 2 seconds instead of default 21600 seconds
tokenRequest.Header.Add(ec2ImdsTokenTtlHeader, "2")
tokenResponse, err := httpClient.Do(tokenRequest)
if err != nil || tokenResponse == nil || tokenResponse.Body == nil {
log.Debugf("unable to make a put call to EC2 Instance Metadata, request url: %s, error: %s "+
"to fetch the instance metadata token. Falling back to insure way of calling EC2 Instance Metadata.",
tokenRequestUrl, err)
} else {
defer tokenResponse.Body.Close()
if tokenResponse.StatusCode != 200 {
log.Debugf("unable to make a put call to EC2 Instance Metadata, request url: %s, code: %s "+
"to fetch the instance metadata token. Falling back to insure way of calling EC2 Instance Metadata.",
tokenRequestUrl, strconv.Itoa(tokenResponse.StatusCode))
} else if responseBody, err := ioutil.ReadAll(tokenResponse.Body); err != nil {
log.Debugf("unable to make a put call to EC2 Instance Metadata, request url: %s, error: %s "+
"to fetch the instance metadata token. Falling back to insure way of calling EC2 Instance Metadata.",
tokenRequestUrl, err)
} else {
log.Debugf("Successfully obtained token to make secure call to EC2 Instance Metadata")
token = string(responseBody)
}
}
}
// EC2 Instance Metadata url: http://169.254.169.254/latest/meta-data/
requestUrl := env.Or(ec2MetadataUriEnvForTesting, ec2MetadataHost) + "/latest/meta-data/" + query
imdsRequest, err := client.CreateStandardAgentHttpRequest(http.MethodGet, requestUrl, nil)
if err != nil {
return "", fmt.Errorf("unable to create http request: %v. request url: %s", err, requestUrl)
}
if token != "" {
imdsRequest.Header.Add(ec2ImdsTokenHeader, token)
}
response, err := httpClient.Do(imdsRequest)
if err != nil {
return "", fmt.Errorf("unable to query from IMDSv1, request url: %s, error: %s", requestUrl, err)
}
defer response.Body.Close()
if responseBody, err := ioutil.ReadAll(response.Body); err != nil {
return "", fmt.Errorf("unable to read EC2 instance metadata for query %s: %v", query, err)
} else {
return string(responseBody), nil
}
}
func RunCommand(name string, args ...string) (string, error) {
var out bytes.Buffer
cmd := exec.Command(name, args...)
cmd.Stdout = &out
err := cmd.Run()
return strings.TrimSuffix(out.String(), "\n"), err
}