Skip to content

Commit

Permalink
user-data Support (#171)
Browse files Browse the repository at this point in the history
* initial user-data commit

* small changes in support of userdata PR #171

* Initial userdata test commit

* some minor changes in support of userdata PR.  Fix tests.

* additional userdata test fixes.  Unified userdata values.

* updated comment

Co-authored-by: Jim Shaver <elitest@Jims-MBP.lan>
Co-authored-by: Jim Shaver <elitest@Jims-MBP.localdomain>
  • Loading branch information
3 people committed Jun 6, 2022
1 parent 33cad60 commit 0965dfc
Show file tree
Hide file tree
Showing 12 changed files with 291 additions and 8 deletions.
2 changes: 2 additions & 0 deletions pkg/cmd/cmdutil/cmdutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/aws/amazon-ec2-metadata-mock/pkg/mock/imdsv2"
"github.com/aws/amazon-ec2-metadata-mock/pkg/mock/spot"
"github.com/aws/amazon-ec2-metadata-mock/pkg/mock/static"
"github.com/aws/amazon-ec2-metadata-mock/pkg/mock/userdata"
"github.com/aws/amazon-ec2-metadata-mock/pkg/server"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -91,6 +92,7 @@ func RegisterHandlers(cmd *cobra.Command, config cfg.Config) {

static.RegisterHandlers(config)
dynamic.RegisterHandlers(config)
userdata.RegisterHandlers(config)

// paths without explicit handler bindings will fallback to CatchAllHandler
server.HandleFuncPrefix("/", handlers.CatchAllHandler)
Expand Down
1 change: 1 addition & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ func LoadConfigForRoot(configFileFlagName string, cmdDefaults map[string]interfa
LoadConfigFromDefaults(cmdDefaults)
SetMetadataDefaults(defaults.GetDefaultValues())
SetDynamicDefaults(defaults.GetDefaultValues())
SetUserdataDefaults(defaults.GetDefaultValues())
SetServerCfgDefaults()

// read in config using viper
Expand Down
8 changes: 8 additions & 0 deletions pkg/config/defaults/aemm-metadata-default-values.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,5 +175,13 @@
"instance-identity-signature": "TesTTKmBbj+DUw6ut6BOr4mFGpax/k6BhIbsotUHvSIhqv7oKqwB4HZhgGP2Gvcxtz5m3QGUbnwI\nhy33GWxjn7+qfZ/GUeZB1Ilc+3rW/P9G/tGxIB3HtqB6q2J6B4DOh6CJiH+BnrHazGW+bJD406Nz\neP9n/rGEGGm0cGEbbeB=",
"fws-instance-monitoring": "disabled"
}
},
"userdata": {
"paths": {
"userdata": "/latest/user-data"
},
"values": {
"userdata": "1234,john,reboot,true|4512,richard,|173,,,"
}
}
}
2 changes: 1 addition & 1 deletion pkg/config/defaults/defaults.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// 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
Expand Down
18 changes: 18 additions & 0 deletions pkg/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
type Config struct {
// ----- metadata config ----- //
Metadata Metadata `mapstructure:"metadata"`
// ----- userdata config ----- //
Userdata Userdata `mapstructure:"userdata"`

// ----- CLI config ----- //
// config keys that are also cli flags
Expand Down Expand Up @@ -57,6 +59,12 @@ type Metadata struct {
Values Values `mapstructure:"values"`
}

// Userdata represents userdata config used by the mock (Json values in metadata-config.json)
type Userdata struct {
Paths UserdataPaths `mapstructure:"paths"`
Values UserdataValues `mapstructure:"values"`
}

// Dynamic represents metadata config used by the mock (Json values in metadata-config.json)
type Dynamic struct {
Paths DynamicPaths `mapstructure:"paths"`
Expand Down Expand Up @@ -194,6 +202,16 @@ type Values struct {
TagsInstanceTest string `mapstructure:"tags-instance-test"`
}

// UserdataPaths represents EC2 userdata paths
type UserdataPaths struct {
Userdata string `mapstructure:"userdata"`
}

// UserdataValues represents EC2 userdata paths
type UserdataValues struct {
Userdata string `mapstructure:"userdata"`
}

// DynamicPaths represents EC2 dynamic paths
type DynamicPaths struct {
InstanceIdentityDocument string `mapstructure:"instance-identity-document"`
Expand Down
85 changes: 85 additions & 0 deletions pkg/config/userdata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// 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 config

import (
"encoding/json"

"github.com/spf13/pflag"
)

var (
udCfgPrefix = "userdata."
udPathsCfgPrefix = udCfgPrefix + "paths."
udValuesCfgPrefix = udCfgPrefix + "values."

// mapping of udValue KEYS to udPath KEYS requiring path substitutions on override
udValueToPlaceholderPathsKeyMap = map[string][]string{}

// supported URL paths to run a mock
udPathsDefaults = map[string]interface{}{}

// values in mock responses
udValuesDefaults = map[string]interface{}{}
)

// GetCfgUdValPrefix returns the prefix to use to access userdata values in config
func GetCfgUdValPrefix() string {
return udCfgPrefix + "." + udValuesCfgPrefix + "."
}

// GetCfgUdPathsPrefix returns the prefix to use to access userdata values in config
func GetCfgUdPathsPrefix() string {
return udCfgPrefix + "." + udPathsCfgPrefix + "."
}

// BindUserdataCfg binds a flag that represents a userdata value to configuration
func BindUserdataCfg(flag *pflag.Flag) {
bindFlagWithKeyPrefix(flag, udValuesCfgPrefix)
}

// SetUserdataDefaults sets config defaults for userdata paths and values
func SetUserdataDefaults(jsonWithDefaults []byte) {
// Unmarshal to map to preserve keys for Paths and Values
var defaultsMap map[string]interface{}
json.Unmarshal(jsonWithDefaults, &defaultsMap)
udPaths := defaultsMap["userdata"].(map[string]interface{})["paths"].(map[string]interface{})

udValues := defaultsMap["userdata"].(map[string]interface{})["values"].(map[string]interface{})

for k, v := range udPaths {
newKey := udPathsCfgPrefix + k
// ex: "userdata": "/latest/user-data"
udPathsDefaults[newKey] = v
}

for k, v := range udValues {
newKey := udValuesCfgPrefix + k
// ex: "userdata": "1234,john,reboot,true|4512,richard,|173,,,"
udValuesDefaults[newKey] = v
}

LoadConfigFromDefaults(udPathsDefaults)
LoadConfigFromDefaults(udValuesDefaults)
}

// GetUserdataDefaults returns config defaults for userdata paths and values
func GetUserdataDefaults() (map[string]interface{}, map[string]interface{}) {
return udPathsDefaults, udValuesDefaults
}

// GetUserdataValueToPlaceholderPathsKeyMap returns collection of userdata values that are substituted into paths
func GetUserdataValueToPlaceholderPathsKeyMap() map[string][]string {
return udValueToPlaceholderPathsKeyMap
}
28 changes: 23 additions & 5 deletions pkg/mock/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

"github.com/aws/amazon-ec2-metadata-mock/pkg/mock/dynamic"
"github.com/aws/amazon-ec2-metadata-mock/pkg/mock/static"
"github.com/aws/amazon-ec2-metadata-mock/pkg/mock/userdata"
"github.com/aws/amazon-ec2-metadata-mock/pkg/server"
)

Expand All @@ -35,11 +36,12 @@ var (
routeLookupTable = make(map[string][]string)

supportedVersions = []string{"latest"}
supportedCategories = []string{"dynamic", "meta-data"}
supportedCategories = []string{"dynamic", "meta-data", "user-data"}

// trimmedRoutes represents the list of routes served by the http server without "latest/meta-data/" prefix
trimmedRoutes []string
trimmedRoutesDynamic []string
trimmedRoutes []string
trimmedRoutesDynamic []string
trimmedRoutesUserdata []string
)

// CatchAllHandler returns subpath listings, if available; 404 status code otherwise
Expand All @@ -62,6 +64,10 @@ func CatchAllHandler(res http.ResponseWriter, req *http.Request) {
trimmedRoute = strings.TrimPrefix(trimmedRoute, dynamic.ServicePath+"/")
log.Println("dynamic prefix detected..trimming: ", trimmedRoute)
routes = trimmedRoutesDynamic
} else if strings.HasPrefix(trimmedRoute, userdata.ServicePath) {
trimmedRoute = strings.TrimPrefix(trimmedRoute, userdata.ServicePath+"/")
log.Println("userdata prefix detected..trimming: ", trimmedRoute)
routes = trimmedRoutesUserdata
} else {
server.ReturnNotFoundResponse(res)
return
Expand Down Expand Up @@ -124,6 +130,8 @@ func ListRoutesHandler(res http.ResponseWriter, req *http.Request) {

// these paths do not use routeLookupTable due to inconsistency of trailing "/" with IMDS
switch req.URL.Path {
case userdata.ServicePath:
server.FormatAndReturnOctetResponse(res, strings.Join(trimmedRoutesUserdata, "\n")+"\n")
case static.ServicePath:
server.FormatAndReturnTextResponse(res, strings.Join(trimmedRoutes, "\n")+"\n")
case dynamic.ServicePath:
Expand All @@ -142,15 +150,24 @@ func formatRoutes() {
var trimmedRoute string
for _, route := range server.Routes {
if strings.HasPrefix(route, dynamic.ServicePath) {
// Omit /latest/dynamic
// Omit /latest/dynamic and /latest/user-data
trimmedRoute = strings.TrimPrefix(route, dynamic.ServicePath)
// Omit empty paths and "/"
if len(trimmedRoute) >= shortestRouteLength {
trimmedRoute = strings.TrimPrefix(trimmedRoute, "/")
trimmedRoutesDynamic = append(trimmedRoutesDynamic, trimmedRoute)
}
} else if strings.HasPrefix(route, userdata.ServicePath) {
// Omit /latest/dynamic and /latest/meta-data
trimmedRoute = strings.TrimPrefix(route, userdata.ServicePath)
// Omit empty paths and "/"
if len(trimmedRoute) >= shortestRouteLength {
trimmedRoute = strings.TrimPrefix(trimmedRoute, "/")
trimmedRoutesUserdata = append(trimmedRoutesUserdata, trimmedRoute)
}

} else {
// Omit /latest/meta-data
// Omit /latest/meta-data and /latest/user-data
trimmedRoute = strings.TrimPrefix(route, static.ServicePath)
// Omit empty paths and "/"
if len(trimmedRoute) >= shortestRouteLength {
Expand All @@ -161,4 +178,5 @@ func formatRoutes() {
}
sort.Sort(sort.StringSlice(trimmedRoutes))
sort.Sort(sort.StringSlice(trimmedRoutesDynamic))
sort.Sort(sort.StringSlice(trimmedRoutesUserdata))
}
74 changes: 74 additions & 0 deletions pkg/mock/userdata/userdata.go
Original file line number Diff line number Diff line change
@@ -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. 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 userdata

import (
"log"
"net/http"
"reflect"

cfg "github.com/aws/amazon-ec2-metadata-mock/pkg/config"
"github.com/aws/amazon-ec2-metadata-mock/pkg/mock/imdsv2"
"github.com/aws/amazon-ec2-metadata-mock/pkg/server"
)

var (
supportedPaths = make(map[string]interface{})
response interface{}
// ServicePath defines the userdata service path
ServicePath = "/latest/user-data"
)

// Handler processes http requests
func Handler(res http.ResponseWriter, req *http.Request) {
log.Println("Received request to mock userdata:", req.URL.Path)

if val, ok := supportedPaths[req.URL.Path]; ok {

response = val
} else {
response = "Something went wrong with: " + req.URL.Path
}
server.FormatAndReturnOctetResponse(res, response.(string))

}

// RegisterHandlers registers handlers for userdata paths
func RegisterHandlers(config cfg.Config) {
pathValues := reflect.ValueOf(config.Userdata.Paths)
udValues := reflect.ValueOf(config.Userdata.Values)
// Iterate over fields in config.Userdata.Paths to
// determine intersections with config.Userdata.Values.
// Intersections represent which paths and values to bind.
for i := 0; i < pathValues.NumField(); i++ {
pathFieldName := pathValues.Type().Field(i).Name
udValueFieldName := udValues.FieldByName(pathFieldName)
if udValueFieldName.IsValid() {
path := pathValues.Field(i).Interface().(string)
value := udValueFieldName.Interface()
if path != "" && value != nil {
// Ex: "/latest/meta-data/instance-id" : "i-1234567890abcdef0"
supportedPaths[path] = value
if config.Imdsv2Required {
server.HandleFunc(path, imdsv2.ValidateToken(Handler))
} else {

server.HandleFunc(path, Handler)
}
} else {
log.Printf("There was an issue registering path %v with udValue: %v", path, value)
}
}
}
}
8 changes: 8 additions & 0 deletions pkg/server/httpserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ func FormatAndReturnTextResponse(res http.ResponseWriter, data string) {
return
}

// FormatAndReturnOctetResponse formats the given data into an octet stream and returns the response
func FormatAndReturnOctetResponse(res http.ResponseWriter, data string) {
res.Header().Set("Content-Type", "application/octet-stream")
res.Write([]byte(data))
log.Println("Returned octet stream response successfully.")
return
}

// FormatAndReturnJSONTextResponse formats the given data into JSON and returns a plaintext response
func FormatAndReturnJSONTextResponse(res http.ResponseWriter, data interface{}) {
res.Header().Set("Content-Type", "text/plain")
Expand Down
59 changes: 59 additions & 0 deletions test/e2e/cmd/userdata-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#! /usr/bin/env bash

set -euo pipefail

TEST_CONFIG_FILE="$SCRIPTPATH/testdata/aemm-config-integ.json"

USERDATA_DEFAULT="1234,john,reboot,true|4512,richard,|173,,,"
USERDATA_OVERRIDDEN="1234"

ROOT_PATH="http://$HOSTNAME:$AEMM_PORT"
USERDATA_TEST_PATH="$ROOT_PATH/latest/user-data"

function test_userdata_defaults() {
pid=$1
test_url=$2
test_name=$3
tput setaf $BLUE
health_check $test_url
TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT)

actual_userdata=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $USERDATA_TEST_PATH)
assert_value "$actual_userdata" $USERDATA_DEFAULT "userdata $test_name"

clean_up $pid
}

function test_userdata_overrides() {
pid=$1
test_url=$2
test_name=$3
tput setaf $BLUE
health_check $test_url
TOKEN=$(get_v2Token $MAX_TOKEN_TTL $AEMM_PORT)

updated_userdata=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" $USERDATA_TEST_PATH)
assert_value "$updated_userdata" $USERDATA_OVERRIDDEN "userdata $test_name"

clean_up $pid
}

tput setaf $BLUE
echo "======================================================================================================"
echo "🥑 Starting userdata integration tests $METADATA_VERSION"
echo "======================================================================================================"


# userdata data defaults
start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT)
$start_cmd &
USERDATA_PID=$!
test_userdata_defaults $USERDATA_PID $USERDATA_TEST_PATH $USERDATA_DEFAULT

# userdata data overrides
start_cmd=$(create_cmd $METADATA_VERSION --port $AEMM_PORT -c $TEST_CONFIG_FILE)
$start_cmd &
USERDATA_PID=$!
test_userdata_overrides $USERDATA_PID $USERDATA_TEST_PATH $USERDATA_OVERRIDDEN

exit $EXIT_CODE_TO_RETURN
Loading

0 comments on commit 0965dfc

Please sign in to comment.