Skip to content

Commit 068403d

Browse files
author
Jeremi Piotrowski
authored
feat: shrink flatcar customdata size (#7755)
1 parent 6021df6 commit 068403d

10 files changed

Lines changed: 813 additions & 1084 deletions

File tree

parts/linux/cloud-init/flatcar.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,23 @@ systemd:
1818
1919
[Install]
2020
WantedBy=sysinit.target
21+
22+
- name: ignition-file-extract.service
23+
enabled: true
24+
contents: |
25+
[Unit]
26+
Description=Extract Ignition file payload
27+
DefaultDependencies=no
28+
After=local-fs.target
29+
Before=sysinit.target ignition-bootcmds.service
30+
ConditionPathExists=/var/lib/ignition/ignition-files.tar
31+
32+
[Service]
33+
Type=oneshot
34+
ExecStart=tar -xvf /var/lib/ignition/ignition-files.tar -C /
35+
ExecStart=rm -f /var/lib/ignition/ignition-files.tar
36+
ExecStart=systemctl daemon-reload
37+
RemainAfterExit=yes
38+
39+
[Install]
40+
WantedBy=sysinit.target

pkg/agent/baker.go

Lines changed: 115 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package agent
55

66
import (
7+
"archive/tar"
78
"archive/zip"
89
"bytes"
910
"encoding/base64"
@@ -79,36 +80,101 @@ func (t *TemplateGenerator) getLinuxNodeCustomDataJSONObject(config *datamodel.N
7980
return fmt.Sprintf("{\"customData\": \"%s\"}", str)
8081
}
8182

82-
func toButaneFile(file cloudInitWriteFile) (*base0_5.File, error) {
83-
newfile := base0_5.File{}
84-
newfile.Path = file.Path
85-
newfile.User.Name = &file.Owner
86-
newfile.Overwrite = to.BoolPtr(true)
87-
mode, e := strconv.ParseInt(file.Permissions, 8, 32)
88-
if e != nil {
89-
return nil, fmt.Errorf("failed to parse file mode: %w", e)
83+
const (
84+
ignitionFilesTarPath = "/var/lib/ignition/ignition-files.tar"
85+
ignitionBootcmdScriptPath = "/etc/ignition-bootcmds.sh"
86+
ignitionTarUnitName = "ignition-file-extract.service"
87+
)
88+
89+
type ignitionTarEntry struct {
90+
path string
91+
mode int64
92+
owner string
93+
contents []byte
94+
}
95+
96+
// buildIgnitionTarEntries converts cloud-init write_files and bootcmds into tar entries.
97+
func buildIgnitionTarEntries(customData cloudInit) ([]ignitionTarEntry, error) {
98+
entries := make([]ignitionTarEntry, 0, len(customData.WriteFiles)+1)
99+
100+
for _, file := range customData.WriteFiles {
101+
mode, err := strconv.ParseInt(file.Permissions, 8, 32)
102+
if err != nil {
103+
return nil, fmt.Errorf("failed to parse file mode: %w", err)
104+
}
105+
106+
var contents []byte
107+
switch {
108+
case file.Content == "" || file.Encoding == "":
109+
contents = []byte(file.Content)
110+
case file.Encoding == "gzip":
111+
decoded, err := getGzipDecodedValue([]byte(file.Content))
112+
if err != nil {
113+
return nil, fmt.Errorf("failed to decode gzip content for %s: %w", file.Path, err)
114+
}
115+
contents = decoded
116+
case file.Encoding == "base64":
117+
decoded, err := base64.StdEncoding.DecodeString(file.Content)
118+
if err != nil {
119+
return nil, fmt.Errorf("failed to decode base64 content: %w", err)
120+
}
121+
contents = decoded
122+
default:
123+
return nil, fmt.Errorf("unsupported encoding %s for %s", file.Encoding, file.Path)
124+
}
125+
126+
entry := ignitionTarEntry{
127+
path: file.Path,
128+
mode: mode,
129+
owner: file.Owner,
130+
contents: contents,
131+
}
132+
entries = append(entries, entry)
133+
}
134+
135+
if len(customData.BootCommands) > 0 {
136+
contents := strings.Join(append([]string{"#!/bin/sh"}, customData.BootCommands...), "\n")
137+
entries = append(entries, ignitionTarEntry{
138+
path: ignitionBootcmdScriptPath,
139+
mode: 0o755,
140+
owner: "root",
141+
contents: []byte(contents),
142+
})
90143
}
91-
newfile.Mode = to.IntPtr(int(mode))
92-
switch file.Encoding {
93-
case "gzip":
94-
newfile.Contents.Inline = &file.Content
95-
// This is hit for AKSCustomCloud file
96-
if file.Content != "" {
97-
newfile.Contents.Compression = &file.Encoding
144+
145+
return entries, nil
146+
}
147+
148+
// buildIgnitionTarball builds a tar archive with relative paths for extraction at root.
149+
func buildIgnitionTarball(entries []ignitionTarEntry) ([]byte, error) {
150+
buf := new(bytes.Buffer)
151+
tarWriter := tar.NewWriter(buf)
152+
153+
for _, entry := range entries {
154+
path := strings.TrimLeft(entry.path, "/")
155+
header := &tar.Header{
156+
Name: path,
157+
Mode: entry.mode,
158+
Size: int64(len(entry.contents)),
159+
Uid: 0,
160+
Gid: 0,
161+
}
162+
if entry.owner != "" && entry.owner != "root" {
163+
header.Uname = entry.owner
98164
}
99-
case "base64":
100-
inline, e := base64.StdEncoding.DecodeString(file.Content)
101-
if e != nil {
102-
return nil, fmt.Errorf("failed to decode base64 content: %w", e)
165+
if err := tarWriter.WriteHeader(header); err != nil {
166+
return nil, fmt.Errorf("failed to write tar header for %s: %w", entry.path, err)
103167
}
104-
newfile.Contents.Inline = to.StringPtr(string(inline))
105-
newfile.Contents.Compression = nil
106-
case "":
107-
newfile.Contents.Inline = to.StringPtr(file.Content)
108-
default:
109-
return nil, fmt.Errorf("unsupported encoding: %s", file.Encoding)
168+
if _, err := tarWriter.Write(entry.contents); err != nil {
169+
return nil, fmt.Errorf("failed to write tar contents for %s: %w", entry.path, err)
170+
}
171+
}
172+
173+
if err := tarWriter.Close(); err != nil {
174+
return nil, fmt.Errorf("failed to close tar writer: %w", err)
110175
}
111-
return &newfile, nil
176+
177+
return buf.Bytes(), nil
112178
}
113179

114180
func cloudInitToButane(customData cloudInit) flatcar1_1.Config {
@@ -121,28 +187,31 @@ func cloudInitToButane(customData cloudInit) flatcar1_1.Config {
121187
panic(fmt.Errorf("failed to unmarshal butane config: %w", e))
122188
}
123189

124-
newfiles := make([]base0_5.File, 0)
125-
var newfile *base0_5.File
126-
for _, file := range customData.WriteFiles {
127-
newfile, e = toButaneFile(file)
128-
if e != nil {
129-
panic(fmt.Errorf("failed to convert cloudInit file to butane file: %w", e))
130-
}
131-
newfiles = append(newfiles, *newfile)
190+
entries, err := buildIgnitionTarEntries(customData)
191+
if err != nil {
192+
panic(fmt.Errorf("failed to convert cloudInit data to tar entries: %w", err))
132193
}
133-
if len(customData.BootCommands) > 0 {
134-
var contents = strings.Join(append([]string{"#!/bin/sh"}, customData.BootCommands...), "\n")
135-
newfiles = append(newfiles, base0_5.File{
136-
Path: "/etc/ignition-bootcmds.sh",
137-
User: base0_5.NodeUser{Name: to.StringPtr("root")},
138-
Group: base0_5.NodeGroup{Name: to.StringPtr("root")},
139-
Mode: to.IntPtr(0o755),
140-
Overwrite: to.BoolPtr(true),
141-
Contents: base0_5.Resource{Inline: to.StringPtr(contents)},
142-
})
194+
if len(entries) == 0 {
195+
return butaneconfig
196+
}
197+
198+
tarball, err := buildIgnitionTarball(entries)
199+
if err != nil {
200+
panic(fmt.Errorf("failed to build ignition tarball: %w", err))
143201
}
202+
compressedTarball := getGzippedBufferFromBytes(tarball)
203+
dataURL := "data:;base64," + base64.StdEncoding.EncodeToString(compressedTarball)
144204

145-
butaneconfig.Storage.Files = append(newfiles, butaneconfig.Storage.Files...)
205+
tarFile := base0_5.File{
206+
Path: ignitionFilesTarPath,
207+
Mode: to.IntPtr(0o600),
208+
Overwrite: to.BoolPtr(true),
209+
Contents: base0_5.Resource{
210+
Source: to.StringPtr(dataURL),
211+
Compression: to.StringPtr("gzip"),
212+
},
213+
}
214+
butaneconfig.Storage.Files = append(butaneconfig.Storage.Files, tarFile)
146215
return butaneconfig
147216
}
148217

@@ -178,35 +247,7 @@ func (t *TemplateGenerator) getFlatcarLinuxNodeCustomDataJSONObject(config *data
178247
panic(fmt.Errorf("failed to marshal Ignition config: %w", e))
179248
}
180249

181-
envelope := flatcar1_1.Config{
182-
Config: base0_5.Config{
183-
Variant: "flatcar",
184-
Version: "1.1.0",
185-
Ignition: base0_5.Ignition{
186-
Config: base0_5.IgnitionConfig{
187-
Replace: base0_5.Resource{
188-
Inline: to.StringPtr(string(ignjson)),
189-
// TODO: butane 0.24.0 broke support for explicit compression
190-
// so we depend on automatic resource compression.
191-
// Compression: to.StringPtr("gzip"),
192-
},
193-
},
194-
},
195-
},
196-
}
197-
wrapped, report, e := envelope.ToIgn3_4(butanecommon.TranslateOptions{})
198-
if e != nil {
199-
panic(fmt.Errorf("butane -> ignition: error: %w:\n%s", e, report.String()))
200-
}
201-
if len(report.Entries) > 0 {
202-
panic(fmt.Errorf("butane -> ignition: warning:\n%s", report.String()))
203-
}
204-
// Marshal the Ignition config to JSON
205-
enc, err := json.Marshal(wrapped)
206-
if err != nil {
207-
panic(fmt.Errorf("failed to marshal Ignition config: %w", err))
208-
}
209-
escstr := escapeSingleLine(string(enc))
250+
escstr := escapeSingleLine(string(ignjson))
210251

211252
return fmt.Sprintf("{\"customData\": \"%s\"}", escstr)
212253
}

0 commit comments

Comments
 (0)