Skip to content

Commit

Permalink
nodeadm(feat): add mime multi-part support to file config provider (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
ndbaker1 authored Oct 4, 2024
1 parent 46aee72 commit 1484b5f
Show file tree
Hide file tree
Showing 15 changed files with 282 additions and 157 deletions.
23 changes: 0 additions & 23 deletions nodeadm/example/node.eks.aws_v1alpha1_nodeconfig.yaml

This file was deleted.

6 changes: 6 additions & 0 deletions nodeadm/internal/api/merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ func TestMerge(t *testing.T) {
"verbosity": 5,
},
"podsPerCore": 20,
"systemReserved": map[string]interface{}{
"cpu": "150m",
},
}),
Flags: []string{
"--node-labels=nodegroup=example",
Expand Down Expand Up @@ -167,6 +170,9 @@ discard_unpacked_layers = false`),
},
"maxPods": 150,
"podsPerCore": 20,
"systemReserved": map[string]interface{}{
"cpu": "150m",
},
}),
Flags: []string{
"--node-labels=nodegroup=example",
Expand Down
42 changes: 42 additions & 0 deletions nodeadm/internal/configprovider/encoding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package configprovider

import (
"bytes"
"compress/gzip"
"encoding/base64"
"fmt"
"io"
)

func decodeIfBase64(data []byte) ([]byte, error) {
e := base64.StdEncoding
maxDecodedLen := e.DecodedLen(len(data))
decodedData := make([]byte, maxDecodedLen)
decodedLen, err := e.Decode(decodedData, data)
if err != nil {
return data, nil
}
return decodedData[:decodedLen], nil
}

// https://en.wikipedia.org/wiki/Gzip
const gzipMagicNumber = uint16(0x1f8b)

func decompressIfGZIP(data []byte) ([]byte, error) {
if len(data) < 2 {
return data, nil
}
preamble := uint16(data[0])<<8 | uint16(data[1])
if preamble == gzipMagicNumber {
reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("failed to create GZIP reader: %v", err)
}
if decompressed, err := io.ReadAll(reader); err != nil {
return nil, fmt.Errorf("failed to read from GZIP reader: %v", err)
} else {
return decompressed, nil
}
}
return data, nil
}
7 changes: 1 addition & 6 deletions nodeadm/internal/configprovider/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"os"

internalapi "github.com/awslabs/amazon-eks-ami/nodeadm/internal/api"
apibridge "github.com/awslabs/amazon-eks-ami/nodeadm/internal/api/bridge"
)

type fileConfigProvider struct {
Expand Down Expand Up @@ -36,9 +35,5 @@ func (fcs *fileConfigProvider) Provide() (*internalapi.NodeConfig, error) {
if err != nil {
return nil, err
}
config, err := apibridge.DecodeNodeConfig(data)
if err != nil {
return nil, err
}
return config, nil
return parseMaybeMultipart(data)
}
96 changes: 96 additions & 0 deletions nodeadm/internal/configprovider/mime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package configprovider

import (
"bytes"
"fmt"
"io"
"mime"
"mime/multipart"
"net/mail"
"strings"

internalapi "github.com/awslabs/amazon-eks-ami/nodeadm/internal/api"
apibridge "github.com/awslabs/amazon-eks-ami/nodeadm/internal/api/bridge"
)

func parseMaybeMultipart(data []byte) (*internalapi.NodeConfig, error) {
// if the MIME data fails to parse as a multipart document, then fall back
// to parsing the entire userdata as the node config.
if multipartReader, err := getMultipartReader(data); err == nil {
config, err := parseMultipart(multipartReader)
if err != nil {
return nil, err
}
return config, nil
} else {
config, err := apibridge.DecodeNodeConfig(data)
if err != nil {
return nil, err
}
return config, nil
}
}

func parseMultipart(userDataReader *multipart.Reader) (*internalapi.NodeConfig, error) {
var nodeConfigs []*internalapi.NodeConfig
for {
part, err := userDataReader.NextPart()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if partHeader := part.Header.Get(contentTypeHeader); len(partHeader) > 0 {
mediaType, _, err := mime.ParseMediaType(partHeader)
if err != nil {
return nil, err
}
if mediaType == nodeConfigMediaType {
nodeConfigPart, err := io.ReadAll(part)
if err != nil {
return nil, err
}
nodeConfigPart, err = decodeIfBase64(nodeConfigPart)
if err != nil {
return nil, err
}
nodeConfigPart, err = decompressIfGZIP(nodeConfigPart)
if err != nil {
return nil, err
}
decodedConfig, err := apibridge.DecodeNodeConfig(nodeConfigPart)
if err != nil {
return nil, err
}
nodeConfigs = append(nodeConfigs, decodedConfig)
}
}
}
if len(nodeConfigs) > 0 {
var config = nodeConfigs[0]
for _, nodeConfig := range nodeConfigs[1:] {
if err := config.Merge(nodeConfig); err != nil {
return nil, err
}
}
return config, nil
} else {
return nil, fmt.Errorf("could not find NodeConfig within UserData")
}
}

func getMultipartReader(data []byte) (*multipart.Reader, error) {
msg, err := mail.ReadMessage(bytes.NewReader(data))
if err != nil {
return nil, err
}
mediaType, params, err := mime.ParseMediaType(msg.Header.Get(contentTypeHeader))
if err != nil {
return nil, err
}
if !strings.HasPrefix(mediaType, multipartContentTypePrefix) {
return nil, fmt.Errorf("MIME type is not multipart")
}
return multipart.NewReader(msg.Body, params[mimeBoundaryParam]), nil
}
122 changes: 1 addition & 121 deletions nodeadm/internal/configprovider/userdata.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
package configprovider

import (
"bytes"
"compress/gzip"
"encoding/base64"
"fmt"
"io"
"mime"
"mime/multipart"
"net/mail"
"strings"

"github.com/awslabs/amazon-eks-ami/nodeadm/api"
internalapi "github.com/awslabs/amazon-eks-ami/nodeadm/internal/api"
apibridge "github.com/awslabs/amazon-eks-ami/nodeadm/internal/api/bridge"
imds "github.com/awslabs/amazon-eks-ami/nodeadm/internal/aws/imds"
)

Expand Down Expand Up @@ -57,116 +48,5 @@ func (p *userDataConfigProvider) Provide() (*internalapi.NodeConfig, error) {
if err != nil {
return nil, fmt.Errorf("failed to decompress user data: %v", err)
}
// if the MIME data fails to parse as a multipart document, then fall back
// to parsing the entire userdata as the node config.
if multipartReader, err := getMIMEMultipartReader(userData); err == nil {
config, err := parseMultipart(multipartReader)
if err != nil {
return nil, err
}
return config, nil
} else {
config, err := apibridge.DecodeNodeConfig(userData)
if err != nil {
return nil, err
}
return config, nil
}
}

func getMIMEMultipartReader(data []byte) (*multipart.Reader, error) {
msg, err := mail.ReadMessage(bytes.NewReader(data))
if err != nil {
return nil, err
}
mediaType, params, err := mime.ParseMediaType(msg.Header.Get(contentTypeHeader))
if err != nil {
return nil, err
}
if !strings.HasPrefix(mediaType, multipartContentTypePrefix) {
return nil, fmt.Errorf("MIME type is not multipart")
}
return multipart.NewReader(msg.Body, params[mimeBoundaryParam]), nil
}

func parseMultipart(userDataReader *multipart.Reader) (*internalapi.NodeConfig, error) {
var nodeConfigs []*internalapi.NodeConfig
for {
part, err := userDataReader.NextPart()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if partHeader := part.Header.Get(contentTypeHeader); len(partHeader) > 0 {
mediaType, _, err := mime.ParseMediaType(partHeader)
if err != nil {
return nil, err
}
if mediaType == nodeConfigMediaType {
nodeConfigPart, err := io.ReadAll(part)
if err != nil {
return nil, err
}
nodeConfigPart, err = decodeIfBase64(nodeConfigPart)
if err != nil {
return nil, err
}
nodeConfigPart, err = decompressIfGZIP(nodeConfigPart)
if err != nil {
return nil, err
}
decodedConfig, err := apibridge.DecodeNodeConfig(nodeConfigPart)
if err != nil {
return nil, err
}
nodeConfigs = append(nodeConfigs, decodedConfig)
}
}
}
if len(nodeConfigs) > 0 {
var config = nodeConfigs[0]
for _, nodeConfig := range nodeConfigs[1:] {
if err := config.Merge(nodeConfig); err != nil {
return nil, err
}
}
return config, nil
} else {
return nil, fmt.Errorf("could not find NodeConfig within UserData")
}
}

func decodeIfBase64(data []byte) ([]byte, error) {
e := base64.StdEncoding
maxDecodedLen := e.DecodedLen(len(data))
decodedData := make([]byte, maxDecodedLen)
decodedLen, err := e.Decode(decodedData, data)
if err != nil {
return data, nil
}
return decodedData[:decodedLen], nil
}

// https://en.wikipedia.org/wiki/Gzip
const gzipMagicNumber = uint16(0x1f8b)

func decompressIfGZIP(data []byte) ([]byte, error) {
if len(data) < 2 {
return data, nil
}
preamble := uint16(data[0])<<8 | uint16(data[1])
if preamble == gzipMagicNumber {
reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("failed to create GZIP reader: %v", err)
}
if decompressed, err := io.ReadAll(reader); err != nil {
return nil, fmt.Errorf("failed to read from GZIP reader: %v", err)
} else {
return decompressed, nil
}
}
return data, nil
return parseMaybeMultipart(userData)
}
9 changes: 6 additions & 3 deletions nodeadm/internal/configprovider/userdata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ func Test_Provide(t *testing.T) {
" config:",
" maxPods: 150",
" podsPerCore: 20",
" systemReserved:",
" cpu: 150m",
" flags:",
" - --v=5",
" - --node-labels=foo=baz",
Expand All @@ -117,9 +119,10 @@ func Test_Provide(t *testing.T) {
},
Kubelet: api.KubeletOptions{
Config: api.InlineDocument{
"maxPods": runtime.RawExtension{Raw: []byte("150")},
"podsPerCore": runtime.RawExtension{Raw: []byte("20")},
"port": runtime.RawExtension{Raw: []byte("1010")},
"maxPods": runtime.RawExtension{Raw: []byte("150")},
"podsPerCore": runtime.RawExtension{Raw: []byte("20")},
"port": runtime.RawExtension{Raw: []byte("1010")},
"systemReserved": runtime.RawExtension{Raw: []byte(`{"cpu":"150m"}`)},
},
Flags: []string{
"--v=2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ set -o pipefail

source /helpers.sh

mock::aws /etc/aemm-inf1-config.json
mock::aws aemm-inf1-config.json
mock::kubelet 1.27.0
wait::dbus-ready

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ set -o pipefail

source /helpers.sh

mock::aws /etc/aemm-g5-config.json
mock::aws aemm-g5-config.json
mock::kubelet 1.27.0
wait::dbus-ready

Expand Down
Loading

0 comments on commit 1484b5f

Please sign in to comment.