Skip to content

Commit

Permalink
Merge pull request #437 from gabriel-samfira/add-multi-mime-tests
Browse files Browse the repository at this point in the history
Add tests for multipart mime in coreos-cloudinit
  • Loading branch information
gabriel-samfira authored Jun 8, 2023
2 parents 09cf13a + a3c8476 commit ede426f
Show file tree
Hide file tree
Showing 3 changed files with 427 additions and 12 deletions.
160 changes: 160 additions & 0 deletions kola/tests/misc/cloudinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,136 @@
package misc

import (
"fmt"
"strings"

"github.com/coreos/go-semver/semver"
"github.com/flatcar/mantle/kola/cluster"
"github.com/flatcar/mantle/kola/register"
"github.com/flatcar/mantle/platform/conf"
)

var multipartMimeUserdata = `Content-Type: multipart/mixed; boundary="MIMEMULTIPART"
MIME-Version: 1.0
--MIMEMULTIPART
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud-config"
ssh_authorized_keys:
- ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEftQIHTRvUmyDCN7VGve4srz03Jmq6rPnqq+XMHMQUIL9c/b0l7B5tWfQvQecKyLte94HOPzAyMJlktWTVGQnY=
--MIMEMULTIPART
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud-config"
hostname: "example"
--MIMEMULTIPART
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud-config"
write_files:
- encoding: b64
content: NDI=
path: /tmp/kola_b64
permissions: '0644'
- encoding: base64
content: NDI=
path: /tmp/kola_b64_1
permissions: '0644'
- encoding: gzip
content: !!binary |
H4sIAGUfoFQC/zMxAgCIsCQyAgAAAA==
path: /tmp/kola_gzip
permissions: '0644'
- encoding: gz
content: !!binary |
H4sIAGUfoFQC/zMxAgCIsCQyAgAAAA==
path: /tmp/kola_gzip_1
permissions: '0644'
- encoding: gz+base64
content: H4sIAGUfoFQC/zMxAgCIsCQyAgAAAA==
path: /tmp/kola_gzip_base64
permissions: '0644'
- encoding: gzip+base64
content: H4sIAGUfoFQC/zMxAgCIsCQyAgAAAA==
path: /tmp/kola_gzip_base64_1
permissions: '0644'
- encoding: gz+b64
content: H4sIAGUfoFQC/zMxAgCIsCQyAgAAAA==
path: /tmp/kola_gzip_base64_2
permissions: '0644'
--MIMEMULTIPART
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="create_file.ps1"
#!/bin/sh
touch /coreos-cloudinit_multipart.txt
--MIMEMULTIPART
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud-config"
#test_to_check_if_cloud_config_can_contain_a_comment
--MIMEMULTIPART
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="some_text.txt"
This is just some random text.
--MIMEMULTIPART
Content-Type: application/json; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="ignition.txt"
{
"ignitionVersion": 1,
"This ignition configuration will be ignored because it's just embedded": "only cloud-init will run",
"ignition": {
"version": "3.0.0"
},
"systemd": {
"units": [{
"name": "example.service",
"enabled": true,
"contents": "[Service]\nType=oneshot\nExecStart=/usr/bin/echo Hello World\n\n[Install]\nWantedBy=multi-user.target"
}]
}
}
--MIMEMULTIPART
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="incognito_cloud_config.txt"
#cloud-config
write_files:
- encoding: b64
content: NDI=
path: /kola_undercover
permissions: '0644'
--MIMEMULTIPART--
`

func init() {
register.Register(&register.Test{
Run: CloudInitBasic,
Expand Down Expand Up @@ -54,6 +177,15 @@ chmod 600 ~core/.ssh/authorized_keys`),
// When cl.cloudinit.basic passed we don't need to run this on all clouds
Platforms: []string{"qemu", "qemu-unpriv"},
})
register.Register(&register.Test{
Run: CloudInitMultipartMime,
ClusterSize: 1,
Name: "cl.cloudinit.multipart-mime",
UserData: conf.MultipartMimeConfig(multipartMimeUserdata),
Distros: []string{"cl"},
Platforms: []string{"qemu", "qemu-unpriv"},
MinVersion: semver.Version{Major: 3620},
})
}

func CloudInitBasic(c cluster.TestCluster) {
Expand All @@ -78,3 +210,31 @@ func CloudInitScript(c cluster.TestCluster) {
c.Fatalf("userdata script produced unexpected value %q", out)
}
}

func CloudInitMultipartMime(c cluster.TestCluster) {
m := c.Machines()[0]

expectKey := "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEftQIHTRvUmyDCN7VGve4srz03Jmq6rPnqq+XMHMQUIL9c/b0l7B5tWfQvQecKyLte94HOPzAyMJlktWTVGQnY="

// Test that the hostname was set by the first multipart mime part that declares the "hostname"
// cloud-config option. The second one at the end should be ignored.
out := c.MustSSH(m, "hostnamectl")
if !strings.Contains(string(out), "Static hostname: example") {
c.Fatalf("hostname wasn't set correctly:\n%s", out)
}

// we can ignore the output. If the command fails, MustSSH will fail the test.
c.MustSSH(m, fmt.Sprintf("grep %s ~core/.ssh/authorized_keys", expectKey))

out = c.MustSSH(m, "ls -l /tmp/kola_*| wc -l")
if string(strings.Replace(string(out), "\n", "", -1)) != "7" {
c.Fatalf("expected 7 files in /tmp, found %q", out)
}

// All files should have the same content (42). These files should have been created by the cloud-config part
// that declares the write_files option.
c.MustSSH(m, `for f in /tmp/kola_*; do OUT=$(cat $f); if [ "$OUT" != 42 ]; then exit 1; fi; done`)
// Check that the x-shellscript part was executed.
c.MustSSH(m, "test -f /coreos-cloudinit_multipart.txt")
c.MustSSH(m, "test -f /kola_undercover")
}
100 changes: 88 additions & 12 deletions platform/conf/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@
package conf

import (
"bufio"
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io/ioutil"
"net/textproto"
"net/url"
"os"
"reflect"
Expand Down Expand Up @@ -52,6 +56,7 @@ import (
ignvalidate "github.com/flatcar/ignition/config/validate"
"github.com/vincent-petithory/dataurl"
"golang.org/x/crypto/ssh/agent"
"gopkg.in/yaml.v3"
)

type kind int
Expand All @@ -63,6 +68,7 @@ const (
kindContainerLinuxConfig
kindScript
kindButane
kindMultipartMime
)

var plog = capnslog.NewPackageLogger("github.com/flatcar/mantle", "platform/conf")
Expand All @@ -80,18 +86,19 @@ type UserData struct {
// Conf is a configuration for a Container Linux machine. It may be either a
// coreos-cloudconfig or an ignition configuration.
type Conf struct {
ignitionV1 *v1types.Config
ignitionV2 *v2types.Config
ignitionV21 *v21types.Config
ignitionV22 *v22types.Config
ignitionV23 *v23types.Config
ignitionV3 *v3types.Config
ignitionV31 *v31types.Config
ignitionV32 *v32types.Config
ignitionV33 *v33types.Config
cloudconfig *cci.CloudConfig
script string
user string
ignitionV1 *v1types.Config
ignitionV2 *v2types.Config
ignitionV21 *v21types.Config
ignitionV22 *v22types.Config
ignitionV23 *v23types.Config
ignitionV3 *v3types.Config
ignitionV31 *v31types.Config
ignitionV32 *v32types.Config
ignitionV33 *v33types.Config
cloudconfig *cci.CloudConfig
script string
multipartMime *MultipartUserdata
user string
}

func Empty() *UserData {
Expand All @@ -100,6 +107,13 @@ func Empty() *UserData {
}
}

func MultipartMimeConfig(data string) *UserData {
return &UserData{
kind: kindMultipartMime,
data: data,
}
}

func ContainerLinuxConfig(data string) *UserData {
return &UserData{
kind: kindContainerLinuxConfig,
Expand Down Expand Up @@ -135,6 +149,32 @@ func Script(data string) *UserData {
}
}

func decompressIfGzipped(data []byte) []byte {
if reader, err := gzip.NewReader(bytes.NewReader(data)); err == nil {
uncompressedData, err := ioutil.ReadAll(reader)
reader.Close()
if err == nil {
return uncompressedData
} else {
return data
}
} else {
return data
}
}

func isMultipartMime(userdata []byte) bool {
userdata = decompressIfGzipped(userdata)
mimeReader := textproto.NewReader(bufio.NewReader(bytes.NewReader(userdata)))
header, err := mimeReader.ReadMIMEHeader()
if err != nil {
return false
}
contentType := header.Get("Content-Type")

return strings.Contains(contentType, "multipart/mixed")
}

func Unknown(data string) *UserData {
u := &UserData{
data: data,
Expand All @@ -149,6 +189,10 @@ func Unknown(data string) *UserData {
case ignerr.ErrScript:
u.kind = kindScript
default:
if isMultipartMime([]byte(data)) {
u.kind = kindMultipartMime
break
}
// Guess whether this is an Ignition config or a CLC.
// This treats an invalid Ignition config as a CLC, and a
// CLC in the JSON subset of YAML as an Ignition config.
Expand Down Expand Up @@ -291,6 +335,12 @@ func (u *UserData) Render(ctPlatform string) (*Conf, error) {
case kindScript:
// pass through scripts unmodified, you are on your own.
c.script = u.data
case kindMultipartMime:
data, err := NewMultipartUserdata(u.data)
if err != nil {
return nil, err
}
c.multipartMime = data
case kindIgnition:
err := renderIgnition()
if err != nil {
Expand Down Expand Up @@ -388,6 +438,9 @@ func (c *Conf) String() string {
return c.cloudconfig.String()
} else if c.script != "" {
return c.script
} else if c.multipartMime != nil {
data, _ := c.multipartMime.Serialize()
return data
}

return ""
Expand Down Expand Up @@ -1245,6 +1298,27 @@ func (c *Conf) copyKeysScript(keys []*agent.Key) {
c.script = strings.Replace(c.script, "@SSH_KEYS@", keyString, -1)
}

func (c *Conf) copyKeysMultipartMime(keys []*agent.Key) {
keysAsStrings := keysToStrings(keys)
header := textproto.MIMEHeader{
"Content-Type": []string{"text/cloud-config; charset=\"us-ascii\""},
"MIME-Version": []string{"1.0"},
"Content-Transfer-Encoding": []string{"7bit"},
"Content-Disposition": []string{"attachment; filename=\"testing-keys.yaml\""},
}

udata := map[string][]string{
"ssh_authorized_keys": keysAsStrings,
}

asYaml, err := yaml.Marshal(udata)
if err != nil {
plog.Errorf("failed to marshal yaml: %v", err)
return
}
c.multipartMime.AddPart(header, asYaml)
}

// CopyKeys copies public keys from agent ag into the configuration to the
// appropriate configuration section for the core user.
func (c *Conf) CopyKeys(keys []*agent.Key) {
Expand All @@ -1270,6 +1344,8 @@ func (c *Conf) CopyKeys(keys []*agent.Key) {
c.copyKeysCloudConfig(keys)
} else if c.script != "" {
c.copyKeysScript(keys)
} else if c.multipartMime != nil {
c.copyKeysMultipartMime(keys)
}
}

Expand Down
Loading

0 comments on commit ede426f

Please sign in to comment.