Skip to content

Commit b4325ec

Browse files
authored
Fix userdata append header restrictions (#9575)
1 parent 48e745c commit b4325ec

File tree

3 files changed

+56
-13
lines changed

3 files changed

+56
-13
lines changed

engine/userdata/cloud-init/src/main/java/org/apache/cloudstack/userdata/CloudInitUserDataProvider.java

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,20 @@ protected String extractUserDataHeader(String userdata) {
8888
.filter(x -> (x.startsWith("#") && !x.startsWith("##")) || (x.startsWith("Content-Type:")))
8989
.collect(Collectors.toList());
9090
if (CollectionUtils.isEmpty(lines)) {
91-
throw new CloudRuntimeException("Failed to detect the user data format type as it " +
92-
"does not contain a header");
91+
LOGGER.debug("Failed to detect the user data format type as it does not contain a header");
92+
return null;
9393
}
9494
return lines.get(0);
9595
}
9696

97-
protected FormatType mapUserDataHeaderToFormatType(String header) {
98-
if (header.equalsIgnoreCase("#cloud-config")) {
97+
protected FormatType mapUserDataHeaderToFormatType(String header, FormatType defaultFormatType) {
98+
if (StringUtils.isBlank(header)) {
99+
if (defaultFormatType == null) {
100+
throw new CloudRuntimeException("Failed to detect the user data format type as it does not contain a header");
101+
}
102+
LOGGER.debug(String.format("Empty header for userdata, using the default format type: %s", defaultFormatType.name()));
103+
return defaultFormatType;
104+
} else if (header.equalsIgnoreCase("#cloud-config")) {
99105
return FormatType.CLOUD_CONFIG;
100106
} else if (header.startsWith("#!")) {
101107
return FormatType.BASH_SCRIPT;
@@ -115,17 +121,19 @@ protected FormatType mapUserDataHeaderToFormatType(String header) {
115121

116122
/**
117123
* Detect the user data type
124+
* @param userdata the userdata string to detect the type
125+
* @param defaultFormatType if not null, then use it in case the header does not exist in the userdata, otherwise fail
118126
* Reference: <a href="https://canonical-cloud-init.readthedocs-hosted.com/en/latest/explanation/format.html#user-data-formats" />
119127
*/
120-
protected FormatType getUserDataFormatType(String userdata) {
128+
protected FormatType getUserDataFormatType(String userdata, FormatType defaultFormatType) {
121129
if (StringUtils.isBlank(userdata)) {
122130
String msg = "User data expected but provided empty user data";
123131
LOGGER.error(msg);
124132
throw new CloudRuntimeException(msg);
125133
}
126134

127135
String header = extractUserDataHeader(userdata);
128-
return mapUserDataHeaderToFormatType(header);
136+
return mapUserDataHeaderToFormatType(header, defaultFormatType);
129137
}
130138

131139
private String getContentType(String userData, FormatType formatType) throws MessagingException {
@@ -234,7 +242,9 @@ private NoIdMimeMessage createMultipartMessageAddingUserdata(String userData, Fo
234242
}
235243

236244
private String simpleAppendSameFormatTypeUserData(String userData1, String userData2) {
237-
return String.format("%s\n\n%s", userData1, userData2.substring(userData2.indexOf('\n')+1));
245+
String userdata2Header = extractUserDataHeader(userData2);
246+
int beginIndex = StringUtils.isNotBlank(userdata2Header) ? userData2.indexOf('\n')+1 : 0;
247+
return String.format("%s\n\n%s", userData1, userData2.substring(beginIndex));
238248
}
239249

240250
private void checkGzipAppend(String encodedUserData1, String encodedUserData2) {
@@ -249,8 +259,8 @@ public String appendUserData(String encodedUserData1, String encodedUserData2) {
249259
checkGzipAppend(encodedUserData1, encodedUserData2);
250260
String userData1 = new String(Base64.decodeBase64(encodedUserData1));
251261
String userData2 = new String(Base64.decodeBase64(encodedUserData2));
252-
FormatType formatType1 = getUserDataFormatType(userData1);
253-
FormatType formatType2 = getUserDataFormatType(userData2);
262+
FormatType formatType1 = getUserDataFormatType(userData1, null);
263+
FormatType formatType2 = getUserDataFormatType(userData2, formatType1);
254264
if (formatType1.equals(formatType2) && List.of(FormatType.CLOUD_CONFIG, FormatType.BASH_SCRIPT).contains(formatType1)) {
255265
return simpleAppendSameFormatTypeUserData(userData1, userData2);
256266
}

engine/userdata/cloud-init/src/test/java/org/apache/cloudstack/userdata/CloudInitUserDataProviderTest.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,21 +73,28 @@ public class CloudInitUserDataProviderTest {
7373

7474
@Test
7575
public void testGetUserDataFormatType() {
76-
CloudInitUserDataProvider.FormatType type = provider.getUserDataFormatType(CLOUD_CONFIG_USERDATA);
76+
CloudInitUserDataProvider.FormatType type = provider.getUserDataFormatType(CLOUD_CONFIG_USERDATA, null);
7777
Assert.assertEquals(CloudInitUserDataProvider.FormatType.CLOUD_CONFIG, type);
7878
}
7979

8080
@Test(expected = CloudRuntimeException.class)
8181
public void testGetUserDataFormatTypeNoHeader() {
8282
String userdata = "password: password\nchpasswd: { expire: False }\nssh_pwauth: True";
83-
provider.getUserDataFormatType(userdata);
83+
provider.getUserDataFormatType(userdata, null);
84+
}
85+
86+
@Test
87+
public void testGetUserDataFormatTypeNoHeaderDefaultFormat() {
88+
String userdata = "password: password\nchpasswd: { expire: False }\nssh_pwauth: True";
89+
CloudInitUserDataProvider.FormatType defaultFormatType = CloudInitUserDataProvider.FormatType.CLOUD_CONFIG;
90+
Assert.assertEquals(defaultFormatType, provider.getUserDataFormatType(userdata, defaultFormatType));
8491
}
8592

8693
@Test(expected = CloudRuntimeException.class)
8794
public void testGetUserDataFormatTypeInvalidType() {
8895
String userdata = "#invalid-type\n" +
8996
"password: password\nchpasswd: { expire: False }\nssh_pwauth: True";
90-
provider.getUserDataFormatType(userdata);
97+
provider.getUserDataFormatType(userdata, null);
9198
}
9299

93100
private MimeMultipart getCheckedMultipartFromMultipartData(String multipartUserData, int count) {
@@ -111,6 +118,16 @@ public void testAppendUserData() {
111118
getCheckedMultipartFromMultipartData(multipartUserData, 2);
112119
}
113120

121+
@Test
122+
public void testAppendUserDataSecondWithoutHeader() {
123+
String userdataWithHeader = Base64.encodeBase64String(SHELL_SCRIPT_USERDATA1.getBytes());
124+
String bashScriptWithoutHeader = "echo \"without header\"";
125+
String userdataWithoutHeader = Base64.encodeBase64String(bashScriptWithoutHeader.getBytes());
126+
String appended = provider.appendUserData(userdataWithHeader, userdataWithoutHeader);
127+
String expected = String.format("%s\n\n%s", SHELL_SCRIPT_USERDATA1, bashScriptWithoutHeader);
128+
Assert.assertEquals(expected, appended);
129+
}
130+
114131
@Test
115132
public void testAppendSameShellScriptTypeUserData() {
116133
String result = SHELL_SCRIPT_USERDATA + "\n\n" +
@@ -129,6 +146,22 @@ public void testAppendSameCloudConfigTypeUserData() {
129146
Assert.assertEquals(result, appendUserData);
130147
}
131148

149+
@Test
150+
public void testAppendCloudConfig() {
151+
String userdata1 = "#cloud-config\n" +
152+
"chpasswd:\n" +
153+
" list: |\n" +
154+
" root:password\n" +
155+
" expire: False";
156+
String userdata2 = "write_files:\n" +
157+
"- path: /root/CLOUD_INIT_WAS_HERE";
158+
String userdataWithHeader = Base64.encodeBase64String(userdata1.getBytes());
159+
String userdataWithoutHeader = Base64.encodeBase64String(userdata2.getBytes());
160+
String appended = provider.appendUserData(userdataWithHeader, userdataWithoutHeader);
161+
String expected = String.format("%s\n\n%s", userdata1, userdata2);
162+
Assert.assertEquals(expected, appended);
163+
}
164+
132165
@Test
133166
public void testAppendUserDataMIMETemplateData() {
134167
String multipartUserData = provider.appendUserData(

ui/src/utils/plugins.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ export const genericUtilPlugin = {
506506
if (isBase64(text)) {
507507
return text
508508
}
509-
return encodeURIComponent(btoa(unescape(encodeURIComponent(text))))
509+
return encodeURI(btoa(unescape(encodeURIComponent(text))))
510510
}
511511
}
512512
}

0 commit comments

Comments
 (0)