diff --git a/.github/actions/download-rootfs/action.yaml b/.github/actions/download-rootfs/action.yaml index ac58e364..d6ed8125 100644 --- a/.github/actions/download-rootfs/action.yaml +++ b/.github/actions/download-rootfs/action.yaml @@ -75,17 +75,31 @@ runs: foreach ($release in $releaseList) { $data = $release -split "\s+" + if ($data.Count -lt 2) { + Write-Output "Error: not enough fields in release info: $release" + $allSuccess = $false + continue + } + # $name is the one in inputs.distros $name = $data[0] - $file = ".\${name}.tar.gz" $url = $data[1] + if ($name.Length -eq 0 -or $url.Length -eq 0) { + Write-Output "Error: empty fields in release info: $release" + $allSuccess = $false + continue + } + + $file = ".\${name}.tar.gz" + & ${{ github.action_path }}\download-rootfs.ps1 -Path "${file}" -URL "$url" if ( ! $? ) { $allSuccess = $false Write-Output "Error: Could not download ${name}" } + $allSuccess = $true Write-Output "::endgroup::" } diff --git a/.github/workflows/build-pr.yaml b/.github/workflows/build-pr.yaml index 35d9e080..7ba01725 100644 --- a/.github/workflows/build-pr.yaml +++ b/.github/workflows/build-pr.yaml @@ -25,7 +25,9 @@ jobs: path: repo - name: Copy under workDir (which has more space to build the package) shell: bash + # Bash.exe wouldn't know how to handle the symlinks inside docs/ without the export below. run: | + export MSYS=winsymlinks:nativestrict mkdir -p $(dirname ${{ env.workDir }}) mv ${GITHUB_WORKSPACE}/repo ${{ env.workDir }} cd ${{ env.workDir }} diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index bc43edba..d5163700 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -4,7 +4,7 @@ on: pull_request: paths-ignore: - docs/** - - *.md + - "*.md" concurrency: azure-vm env: @@ -28,9 +28,10 @@ jobs: needs: vm-setup env: rootFsCache: "${env:USERPROFILE}\\Downloads\\rootfs" - distroName: Ubuntu-Preview - appID: UbuntuPreview - launcher: ubuntupreview.exe + # TODO: Move this to "Ubuntu" once we have backported everything and Ubuntu is transitionned to 24.04 + distroName: Ubuntu-24.04 + appID: Ubuntu24.04LTS + launcher: ubuntu2404.exe steps: - name: Checkout uses: actions/checkout@v3 @@ -88,7 +89,7 @@ jobs: Write-Output "::endgroup::" Write-Output "::group::Tests" - go test .\launchertester\ -timeout 15m -run TestBasicSetup --distro-name '${{ env.distroName }}' --launcher-name '${{ env.launcher }}' + go test .\launchertester\ -timeout 30m --distro-name '${{ env.distroName }}' --launcher-name '${{ env.launcher }}' $exitStatus=$? Write-Output "::endgroup::" if ( ! $exitStatus ) { Exit(1) } diff --git a/DistroLauncher/DistroLauncher.cpp b/DistroLauncher/DistroLauncher.cpp index 4b0da554..4c04ab2e 100644 --- a/DistroLauncher/DistroLauncher.cpp +++ b/DistroLauncher/DistroLauncher.cpp @@ -37,6 +37,10 @@ HRESULT InstallDistribution(bool createUser) return hr; } + if (Ubuntu::CheckInitTasks(g_wslApi, createUser)) { + return ERROR_SUCCESS; + } + // Create a user account. if (createUser) { Helpers::PrintMessage(MSG_CREATE_USER_PROMPT); diff --git a/DistroLauncher/DistroLauncher.vcxproj b/DistroLauncher/DistroLauncher.vcxproj index 299a19f0..616ff9bc 100644 --- a/DistroLauncher/DistroLauncher.vcxproj +++ b/DistroLauncher/DistroLauncher.vcxproj @@ -140,6 +140,7 @@ + @@ -149,6 +150,9 @@ + + $(ProjectDir);%(AdditionalIncludeDirectories) + Create diff --git a/DistroLauncher/DistroLauncher.vcxproj.filters b/DistroLauncher/DistroLauncher.vcxproj.filters index 4aefa801..990ad3c3 100644 --- a/DistroLauncher/DistroLauncher.vcxproj.filters +++ b/DistroLauncher/DistroLauncher.vcxproj.filters @@ -33,6 +33,9 @@ Header Files + + Header Files + @@ -50,6 +53,9 @@ Source Files + + Source Files + diff --git a/DistroLauncher/Ubuntu/.clang-format b/DistroLauncher/Ubuntu/.clang-format new file mode 100644 index 00000000..f1776104 --- /dev/null +++ b/DistroLauncher/Ubuntu/.clang-format @@ -0,0 +1,11 @@ +BasedOnStyle: Google +--- +Language: Cpp +AllowShortBlocksOnASingleLine: Empty +AllowShortFunctionsOnASingleLine: All +ColumnLimit: 100 +DerivePointerAlignment: false +InsertNewlineAtEOF: true +PointerAlignment: Left +# We need to keep stdafx.h as the first include of any cpp file. +SortIncludes: false diff --git a/DistroLauncher/Ubuntu/InitTasks.cpp b/DistroLauncher/Ubuntu/InitTasks.cpp new file mode 100644 index 00000000..01095286 --- /dev/null +++ b/DistroLauncher/Ubuntu/InitTasks.cpp @@ -0,0 +1,521 @@ +#include +#include "InitTasks.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace Ubuntu { + +namespace { +// Blocks the current thread until all initialization tasks finish. +void waitForInitTasks(WslApiLoader& api); + +// Enforces the existence of a default WSL user either: +// - defined in /etc/wsl.conf (which might not be in effect yet) +// - defined in WSL API/registry +// - or the lowest non-system account with UID >= 1000 in the NSS passwd database +// Returns false if a default user couldn't be set. +bool enforceDefaultUser(WslApiLoader& api); +} // namespace + +bool CheckInitTasks(WslApiLoader& api, bool checkDefaultUser) { + waitForInitTasks(api); + + if (!checkDefaultUser) { + return true; + } + + return enforceDefaultUser(api); +} + +namespace { +void waitForInitTasks(WslApiLoader& api) { + _putws(L"Checking for initialization tasks...\n"); + + DWORD exitCode = -1; + // Try running cloud-init unconditionally, but avoid printing to console if it doesn't exist. + auto hr = api.WslLaunchInteractive( + L"function command_not_found_handle() { return 127; }; cloud-init status --wait", FALSE, + &exitCode); + if (FAILED(hr)) { + Helpers::PrintErrorMessage(hr); + return; + } + + // 127 for command not found, 0 for "success" or 2 for "recoverable error" - sometimes we get that + // while status shows "done". + // https://cloudinit.readthedocs.io/en/latest/explanation/failure_states.html#cloud-init-error-codes + // If that's the case we should inspect the system to check whether we should skip or proceed with + // user creation. + switch (exitCode) { + case 0: { + return; + } + case 127: { + _putws(L"INFO: this release doesn't support initialization tasks.\n"); + return; + } + case 2: { + _putws(L"WARNING: initialization tasks partially succeeded, see below:"); + break; + } + default: { + wprintf(L"ERROR: initialization failed with exit code: %u\n", exitCode); + break; + } + } + // We don't really care if the command below fails, it's just informative. + api.WslLaunchInteractive(L"cloud-init status --long", FALSE, &exitCode); +} + +namespace fs = std::filesystem; +fs::path wslConfPath() { + // init-once, lazily. + static fs::path etcWslConf = + fs::path{L"\\\\wsl.localhost"} / DistributionInfo::Name / "etc\\wsl.conf"; + return etcWslConf; +} + +bool setDefaultUserViaWslApi(WslApiLoader& api, unsigned long uid) { + if (auto hr = api.WslConfigureDistribution(uid, WSL_DISTRIBUTION_FLAGS_DEFAULT); FAILED(hr)) { + _putws(L"ERROR: failed to set default user: "); + Helpers::PrintErrorMessage(hr); + return false; + } + return true; +} +// Groups the pieces of information from a single user entry in the passwd database we care about. +struct UserEntry { + std::string name; + ULONG uid = -1; + bool hasLogin = false; +}; + +// Collects all users found in the NSS passwd database, sorted by UID. +std::vector getAllUsers(WslApiLoader& api); + +// Returns the defaultUser set in /etc/wsl.conf or the empty string if none is set. +std::string defaultUserInWslConf(); + +// Converts a multi-byte null-terminated string into a wide string. +std::wstring str2wide(std::string_view str, UINT codePage = CP_THREAD_ACP); + +bool enforceDefaultUser(WslApiLoader& api) try { + auto users = getAllUsers(api); + + if (users.empty()) { + // unexpectedly nothing to do + _putws(L"ERROR: couldn't find any users in NSS database\n"); + return false; + } + // 1. We read the default user name from /etc/wsl.conf + if (auto name = defaultUserInWslConf(); !name.empty()) { + // We still need the UID to be able to call the WSL API. + auto found = std::find_if(users.begin(), users.end(), + [&name](const UserEntry& u) { return u.name == name; }); + if (found == users.end()) { + // no UID, nothing to do, the system is in a bad state where the user requested in wsl.conf + // doesn't exist. We won't fix that. + return true; + } + return setDefaultUserViaWslApi(api, found->uid); + } + // 2. Check for the Windows registry + // This call returns the UID of the current default user, most likely root, unless someone set a + // different UID via the registry editor or WSL API, for which case we are done. + if (auto uid = DistributionInfo::QueryUid(L""); uid != 0) { + return true; + } + + // 3. Finally, search for the first non-system user. + auto found = std::find_if(users.begin(), users.end(), + [](const UserEntry& u) { return u.uid > 999 && u.hasLogin; }); + if (found != users.end()) { + return setDefaultUserViaWslApi(api, found->uid); + } + + _putws(L"ERROR: no candidate default user was found\n"); + return false; +} catch (const std::exception& err) { + _putws(L"ERROR: Unexpected failure when enforcing the default user: "); + _putws(str2wide(err.what()).c_str()); + return false; +} + +// A truly temporary file copy. +// +// Invariant: while this object exists, so does the underlying file, +// being auto-deleted by the OS when this object goes out of scope. +class TempFileCopy { + fs::path p; + HANDLE h = nullptr; + + public: + ~TempFileCopy() { + if (h) { + CloseHandle(h); + } + } + const fs::path& path() const { return p; } + + // Creates a temporary copy of the source file configured for auto removal by the OS when this + // object goes out of scope under the system's preferred temporary directory. + explicit TempFileCopy(const fs::path& source) { + // When debugging fs::temp_directory_path() returns a path like LOCALAPPDATA\Temp, but once + // packaged it should point inside LOCALAPPDATA\Packages\\... + auto path = fs::temp_directory_path(); + + wchar_t rawDestination[MAX_PATH] = {'\0'}; + if (auto uniqueCode = GetTempFileNameW(path.native().c_str(), L"wsl", 0, rawDestination); + uniqueCode == 0) { + std::string msg{"couldn't create a temporary file inside "}; + msg += path.string(); + throw std::system_error{static_cast(GetLastError()), std::system_category(), msg}; + } + fs::path destination{rawDestination}; + + // GetTempFileNameA would have created a file for us thus we need to overwrite it. + fs::copy_file(source, destination, fs::copy_options::overwrite_existing); + fs::permissions(destination, fs::perms::owner_read | fs::perms::owner_write); + + // Allow others to read but not write it, and configure it to be deleted automatically when the + // handle is closed (what's the destructor does automatically no matter what). + HANDLE file = CreateFileW( + rawDestination, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_TEMPORARY | FILE_ATTRIBUTE_READONLY | FILE_FLAG_DELETE_ON_CLOSE, nullptr); + if (file == INVALID_HANDLE_VALUE) { + std::string msg{"couldn't open file in auto-delete mode: "}; + msg += path.string(); + throw std::system_error{static_cast(GetLastError()), std::system_category(), msg}; + } + + // fully initializes the object. + p = destination; + h = file; + } +}; + +// Reads [user].default from an existing wsl.conf file located at the ini path provided. +std::string readIniDefaultUser(const fs::path& ini) { + // `getconf LOGIN_NAME_MAX` returns 256, but glibc restricts user login names to 32 chars long. + static constexpr auto utNameSize = 33; // With room for the NULL terminator just in case. + char uname[utNameSize] = {'\0'}; + auto len = + GetPrivateProfileStringA("user", "default", nullptr, uname, utNameSize, ini.string().c_str()); + + // GetPrivateProfileStringA set errno to ERROR_FILE_NOT_FOUND if the file + // or section or key are not found or if the file is ill-formed. We know the file exists. + // Absent user.default entry is a common case, we don't want to spam users for that. + // I don't see what other errors that could raise. + if (auto e = GetLastError(); len == 0 && e == ERROR_FILE_NOT_FOUND) { + return {}; + } + + return uname; +} + +std::string defaultUserInWslConf() try { + auto etcWslConf = wslConfPath(); + if (!fs::exists(etcWslConf)) { + return {}; + } + // Copy /etc/wsl.conf to a temporary local path (guaranteed by the OS to be auto-deleted) from + // where GetPrivateProfileStringA can read it. + TempFileCopy copy(etcWslConf); + return readIniDefaultUser(copy.path()); + +} catch (std::system_error const& err) { + // std::filesystem_error is child of std::system_error + std::wcout << L"ERROR: failed to read /etc/wsl.conf: " << err.code() << ": " + << str2wide(err.what()); + return {}; +} + +std::wstring str2wide(std::string_view str, UINT codePage) { + if (str.empty() || str.size() >= INT_MAX) return {}; + + int inputSize = static_cast(str.size()); + int required = ::MultiByteToWideChar(codePage, 0, str.data(), inputSize, NULL, 0); + if (0 == required) return {}; + + std::wstring str2; + str2.resize(required); + + int converted = ::MultiByteToWideChar(codePage, 0, str.data(), inputSize, &str2[0], required); + if (0 == converted) return {}; + + return str2; +} + +// A non-interactive WSL process, turned into a class so we don't have to worry about closing +// the process and pipe's handles. +class WslProcess { + private: + HANDLE process_ = nullptr; + HANDLE readPipe_ = nullptr; + HANDLE writePipe_ = nullptr; + std::wstring command_; + + static constexpr auto MaxOutputSize = 4096; + + public: + ~WslProcess() { + if (process_) { + CloseHandle(process_); + } + if (writePipe_) { + CloseHandle(writePipe_); + } + if (readPipe_) { + CloseHandle(readPipe_); + } + } + + struct Result { + std::wstring error; + std::size_t exitCode = static_cast(-1); + std::string stdOut; + }; + + // Runs the process via WSL api and wait for timeout milliseconds. + Result run(WslApiLoader& api, DWORD timeout); + + explicit WslProcess(std::wstring command_) : command_{command_} {}; +}; + +// Views a string as a collection of (most likely non-null terminated) substring slices split by the +// provided delimiter, visited unidirectionally. The backing string is required to outlive this for +// safe usage. Useful for lazy iteration. +class SplitView { + private: + std::string_view parent; + char delimiter; + std::string_view::const_iterator start; + + public: + SplitView(std::string_view str, char delimiter) + : parent(str), delimiter(delimiter), start(parent.begin()) {} + + std::optional next() { + if (start == parent.end()) { + return std::nullopt; + } + + auto end = std::find(start, parent.end(), delimiter); + std::string_view token = parent.substr(start - parent.begin(), end - start); + + if (end != parent.end()) { + start = end + 1; + } else { + start = end; + } + + return token; + } + + // This allows plugging the SplitView into std algorithms and range-for loops. + auto begin() { return iterator(this); } + auto end() { return iterator::sentinel(this); } + + class iterator { + private: + SplitView* splitView; + std::optional current; + + iterator(SplitView* splitView, const std::optional& current) + : splitView(splitView), current(current) {} + + public: + // Creates a new iterator pointing to the next value of the SplitView, i.e. the + // begin-iterator. + iterator(SplitView* splitView) : splitView{splitView}, current{splitView->next()} {} + // Creates a new sentinel iterator for the provided SplitView, i.e. the end-iterator. + static iterator sentinel(SplitView* splitView) { return iterator{splitView, std::nullopt}; } + + // boiler-plate to define a standard-compliant iterator interface. + using value_type = std::string_view; + using difference_type = std::ptrdiff_t; + using pointer = const std::string_view*; + using reference = const std::string_view&; + using iterator_category = std::input_iterator_tag; + + reference operator*() const { return *current; } + pointer operator->() const { return &(*current); } + + iterator& operator++() { + current = splitView->next(); + return *this; + } + + iterator operator++(int) { + iterator temp = *this; + ++(*this); + return temp; + } + + friend bool operator==(const iterator& a, const iterator& b) { + return a.splitView == b.splitView && a.current == b.current; + } + + friend bool operator!=(const iterator& a, const iterator& b) { return !(a == b); } + }; +}; + +// Parses a string modelling a line of passwd into a UserEntry object. +// We only care about login name, UID and the login shell, although the lines should have 7 fields: +// ^NAME:ENCRYPTION:UID:...3 fields...:SHELL\n$ +// Returns std::nullopt on parse failure, the exact error for ill-formed lines is not needed. +std::optional userEntryFromString(std::string_view line); + +// Assuming UnaryOperation returns a std::optional, applies it to each element of the range +// [first,last[ and stores the results that actually hold a value into the output range. +// It's like std::transform, but skips the results of UnaryOperation that returns std::nullopt, +// thus the output range size is going to be smaller than or equal to the input range size. +template +OutputIt transform_maybe(InputIt first, InputIt last, OutputIt d_first, UnaryOperation unary_op) { + while (first != last) { + auto&& in = *first; + std::optional maybe = unary_op(in); + if (maybe) { + *d_first++ = *maybe; + } + ++first; + } + return d_first; +} + +std::vector getAllUsers(WslApiLoader& api) { + WslProcess getent{L"getent passwd"}; + auto [error, exitCode, output] = getent.run(api, 10'000); + if (!error.empty()) { + _putws(L"failed to read passwd database: "); + _putws(error.c_str()); + if (exitCode != 0) { + wprintf(L"%lld", exitCode); + } + return {}; + } + + if (output.empty()) { + return {}; + } + + std::vector users; + // Where the boilerplate pays-off: splits the getent output by lines + SplitView lines{output, '\n'}; + // and store the parsed results in the users vector. + // NOTE about ill-formed lines in passwd: this algorithm just skips them. + // Broken lines in /etc/passwd won't prevent the effects of the good lines. + // getent itself reports errors for broken lines but still output the good ones. + // The system behaves as if they don't exist. So we can ignore them as well. + transform_maybe(lines.begin(), lines.end(), std::back_inserter(users), userEntryFromString); + // Finally sort that vector by UID. + std::sort(users.begin(), users.end(), + [](const UserEntry& a, const UserEntry& b) { return a.uid < b.uid; }); + return users; +} + +std::optional userEntryFromString(std::string_view line) { + SplitView fields{line, ':'}; + // Field 0: name + auto name = fields.next(); + if (!name || name->empty()) { + return std::nullopt; + } + // Field 1: encryption flag, unused. + auto unused = fields.next(); + if (!unused) { + return std::nullopt; + } + // Field 2: UID + auto u = fields.next(); + if (!u) { + return std::nullopt; + } + ULONG uid = -1; + auto ud = std::from_chars(u->data(), u->data() + u->length(), uid); + if (ud.ec != std::errc{}) { + // cannot convert UID to an integer + return std::nullopt; + } + // Fields 3, 4 and 5: unused in this context, but still must be checked, otherwise the line is + // ill-formed. + for (int i = 0; i < 3; ++i) { + if (unused = fields.next(); !unused) { + return std::nullopt; + } + } + // Field 6: the login shell. + auto shell = fields.next(); + if (!shell || shell->empty()) { + return std::nullopt; + } + + // For this particular case it seems that an exclusion list is easier than a + // positive list of what shells are valid as there are more valid shell choices (sh, bash, csh, + // dash, ksh, tcsh, zsh, fish, ...). + bool hasLogin = + (shell->find("/sync") == std::string::npos && shell->find("/nologin") == std::string::npos && + shell->find("/false") == std::string::npos); + + return UserEntry{std::string{name->begin(), name->end()}, uid, hasLogin}; +} + +WslProcess::Result WslProcess::run(WslApiLoader& api, DWORD timeout) { + // Create a pipe to read the output of the launched process. + HANDLE read, write, process; + SECURITY_ATTRIBUTES sa{sizeof(sa), nullptr, true}; + ULONG uid = -1; + if (CreatePipe(&read, &write, &sa, 0) == FALSE) { + return {L"failed to create the stdio pipe"}; + } + // We have to remember to close the pipe handles. + readPipe_ = read; + writePipe_ = write; + + DWORD exitCode = -1; + auto hr = api.WslLaunch(command_.c_str(), FALSE, GetStdHandle(STD_INPUT_HANDLE), writePipe_, + GetStdHandle(STD_ERROR_HANDLE), &process); + if (FAILED(hr)) { + return {L"failed to launch process"}; + } + // Also need to remember to close the process handle. + process_ = process; + + if (auto wait = WaitForSingleObject(process_, timeout); wait == WAIT_TIMEOUT) { + return {L"terminated due timed out"}; + } + + exitCode = -1; + if ((GetExitCodeProcess(process_, &exitCode) == false) || (exitCode != 0)) { + return {L"exited with error", exitCode}; + } + + // Check how many bytes we need to allocate to pump all contents out the pipe. + DWORD unreadBytes; + if (FALSE == PeekNamedPipe(readPipe_, 0, 0, 0, &unreadBytes, 0) || unreadBytes == 0) { + return {L"could not read the process output", 0}; + } + std::size_t size = 1ULL + unreadBytes; + if (size > MaxOutputSize) { + return {L"process output is too big", 0}; + } + std::string contents(size, '\0'); + + // Finally we do the read. + DWORD readCount = 0, avail = 0; + if (FALSE == ReadFile(readPipe_, contents.data(), unreadBytes, &readCount, nullptr) || + read == 0) { + return {L"could not read the process output", 0}; + } + + return {{}, 0, contents}; +} + +} // namespace +} // namespace Ubuntu diff --git a/DistroLauncher/Ubuntu/InitTasks.h b/DistroLauncher/Ubuntu/InitTasks.h new file mode 100644 index 00000000..71ca2234 --- /dev/null +++ b/DistroLauncher/Ubuntu/InitTasks.h @@ -0,0 +1,8 @@ +#pragma once +namespace Ubuntu +{ + // Returns true if system initialization tasks are complete. + // If [checkDefaultUser] is true, we consider creating the default user part of such tasks. + bool CheckInitTasks(WslApiLoader& api, bool checkDefaultUser); +}; + diff --git a/DistroLauncher/stdafx.h b/DistroLauncher/stdafx.h index 54123d4b..bd5117c4 100644 --- a/DistroLauncher/stdafx.h +++ b/DistroLauncher/stdafx.h @@ -32,3 +32,6 @@ // Message strings compiled from .MC file. #include "messages.h" + +// Ubuntu extensions +#include "Ubuntu/InitTasks.h" diff --git a/e2e/go.work b/e2e/go.work index 5ee38570..a6385f3f 100644 --- a/e2e/go.work +++ b/e2e/go.work @@ -1,3 +1,5 @@ -go 1.21.5 +go 1.22.0 + +toolchain go1.22.5 use ./launchertester diff --git a/e2e/go.work.sum b/e2e/go.work.sum new file mode 100644 index 00000000..594145dc --- /dev/null +++ b/e2e/go.work.sum @@ -0,0 +1,6 @@ +github.com/0xrawsec/golang-utils v1.3.2/go.mod h1:m7AzHXgdSAkFCD9tWWsApxNVxMlyy7anpPVOyT/yM7E= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/e2e/launchertester/basic_setup_test.go b/e2e/launchertester/basic_setup_test.go index 2d11772a..e310972c 100644 --- a/e2e/launchertester/basic_setup_test.go +++ b/e2e/launchertester/basic_setup_test.go @@ -3,10 +3,16 @@ package launchertester import ( "context" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" "testing" "time" "github.com/stretchr/testify/require" + "github.com/ubuntu/gowsl" ) // TestBasicSetup runs a battery of assertions after installing with the distro launcher. @@ -18,20 +24,145 @@ func TestBasicSetup(t *testing.T) { // TODO: try to inject user/password to stdin to avoid --root arg. out, err := launcherCommand(ctx, "install", "--root").CombinedOutput() // Installing as root to avoid Stdin require.NoErrorf(t, err, "Unexpected error installing: %s\n%v", out, err) - // TODO: check with Carlos if this is necessary - require.NotEmpty(t, out, "Failed to install the distro: No output produced.") testCases := map[string]func(t *testing.T){ - // TODO: Re-enable those tests once the latest wsl-setup with distro patching land. - // "SystemdEnabled": testSystemdEnabled, - // "SystemdUnits": testSystemdUnits, - // "CorrectUpgradePolicy": testCorrectUpgradePolicy, - // "UpgradePolicyIdempotent": testUpgradePolicyIdempotent, - "InteropIsEnabled": testInteropIsEnabled, - "HelpFlag": testHelpFlag, + "SystemdEnabled": testSystemdEnabled, + "SystemdUnits": testSystemdUnits, + "CorrectUpgradePolicy": testCorrectUpgradePolicy, + "UpgradePolicyIdempotent": testUpgradePolicyIdempotent, + "InteropIsEnabled": testInteropIsEnabled, + "HelpFlag": testHelpFlag, } for name, tc := range testCases { t.Run(name, tc) } } + +// TestSetupWithCloudInit runs a battery of assertions after installing with the distro launcher and cloud-init. +func TestSetupWithCloudInit(t *testing.T) { + testCases := map[string]struct { + install_root bool + withRegistryUser string + wantUser string + wantFile string + }{ + "With default user in conf": {wantUser: "testuser", wantFile: "/etc/with_default_user.done"}, + "With default user in registry": {withRegistryUser: "testuser", wantUser: "testuser", wantFile: "/etc/with_default_user.done"}, + "With default user in both": {withRegistryUser: "anotheruser", wantUser: "testuser"}, + "With default user in none": {wantUser: "testuser", wantFile: "/home/testuser/with_default_user.done"}, + + "With only remote users": {wantUser: "testmail"}, + "With broken passwd file": {wantUser: "testmail"}, + "Without checking user": {install_root: true, wantUser: "root", wantFile: "/home/testuser/with_default_user.done"}, + } + + home, err := os.UserHomeDir() + require.NoError(t, err, "Setup: Cannot get user home directory") + cloudinitdir := filepath.Join(home, ".cloud-init") + backupDir := filepath.Join(t.TempDir(), "cloud-init-backup") + if _, err = os.Stat(cloudinitdir); err == nil { + require.NoError(t, os.Rename(cloudinitdir, backupDir), "Failed to backup cloud-init directory") + } + require.NoError(t, os.MkdirAll(cloudinitdir, 0755), "Setup: Cannot create cloud-init directory") + t.Cleanup(func() { + if err := os.RemoveAll(cloudinitdir); err != nil { + t.Logf("Setup: Failed to remove user-data file after test: %v", err) + } + if err = os.Rename(backupDir, cloudinitdir); err != nil { + t.Logf("Setup: Failed to restore cloud-init directory after test: %v", err) + } + }) + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + wslSetup(t) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + userdata, err := os.ReadFile(TestFixturePath(t)) + require.NoError(t, err, "Setup: Cannot read test user-data file") + err = os.WriteFile(filepath.Join(cloudinitdir, "default.user-data"), userdata, 0755) + require.NoError(t, err, "Setup: Cannot write user-data file to the right location") + + var args []string + if tc.install_root { + args = append(args, "--root") + } + + registrySet := make(chan error) + if len(tc.withRegistryUser) == 0 { + close(registrySet) + } else { + go func() { + defer close(registrySet) + + d := gowsl.NewDistro(ctx, *distroName) + for { + select { + case <-ctx.Done(): + registrySet <- ctx.Err() + return + default: + } + + state, err := d.State() + if err != nil { + // WSL is not ready for concurrent access. Errors are very likely when registering or unregistering. + t.Logf("Setup: Couldn't to get distro state this time: %v", err) + time.Sleep(10 * time.Second) + continue + } + + if state == gowsl.Uninstalling { + registrySet <- fmt.Errorf("Setup: too late to set the registry: distro is uninstalling") + return + } + + if state == gowsl.NonRegistered || state == gowsl.Installing { + t.Logf("Setup: Waiting for distro to be registered to set default user via registry") + time.Sleep(10 * time.Second) + continue + } + // if running or stopped + id := wslCommand(ctx, "id", "-u", tc.withRegistryUser) + out, err := id.CombinedOutput() + if err != nil { + t.Logf("Setup: Failed to get uid for %s: %v", tc.withRegistryUser, err) + time.Sleep(300 * time.Millisecond) + continue + } + uid, err := strconv.Atoi(strings.TrimSpace(string(out))) + if err != nil { + t.Logf("Setup: Failed to parse %s: %v", out, err) + time.Sleep(300 * time.Millisecond) + continue + } + // We use cloud-init to create the user with cloud-config data we control, so we expect it to be in the range of normal users. + if uid < 1000 || uid > 60000 { + registrySet <- fmt.Errorf("Setup: unexpected uid: %d", uid) + return + } + registrySet <- d.DefaultUID(uint32(uid)) + return + } + }() + } + + out, err := launcherCommand(ctx, "install", args...).CombinedOutput() // Using the "install" command to avoid the shell after installation. + require.NoErrorf(t, err, "Unexpected error installing: %s\n%v", out, err) + + // Seems out of order but setting the registry is concurrent with the installation, hopefully it will be set before the + // launcher checks for the default user. Either way the user assertion in the end of this test case will work as exoected. + require.NoError(t, <-registrySet, "Setup: Failed to set default user via GoWSL/registry") + + testSystemdEnabled(t) + testInteropIsEnabled(t) + if len(tc.wantFile) > 0 { + testFileExists(t, tc.wantFile) + } + testDefaultUser(t, tc.wantUser) + }) + } +} diff --git a/e2e/launchertester/default_experience_test.go b/e2e/launchertester/default_experience_test.go deleted file mode 100644 index 435fd268..00000000 --- a/e2e/launchertester/default_experience_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package launchertester - -import ( - "context" - "errors" - "os/exec" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -// TestDefaultExperience tests the experience that most users have: -// opening WSL from the store or from the Start menu. -func TestDefaultExperience(t *testing.T) { - t.Skip("Skipped: fails in Azure") // TODO: Fix - wslSetup(t) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // TODO: redirect powershell.exe output to a file and read from it. - commandText := []string{"-Command", "wt.exe", "--window", "0", "-d", ".", - "powershell.exe", "-noExit", *launcherName, "--hide-console"} - cmd := exec.CommandContext(ctx, "powershell", commandText...) - - err := cmd.Start() - require.NoErrorf(t, err, "Failed to start new WSL command") - - waitStateTransition(t, "DistroNotFound", "Installing") - waitStateTransition(t, "Installing", "Running") - waitForInstaller(t) - - state := distroState(t) - require.Equal(t, "Running", state, "Unexpected state for after installation") - - time.Sleep(15 * time.Second) // If tty did not start, it should stop after 8 seconds - state = distroState(t) - require.Equal(t, "Running", state, "Distro should still be running after 15 seconds, because a shell should still be open") - - cancel() - err = cmd.Wait() - require.NoError(t, err, "Unexpected error after finishing command") - - testCases := map[string]func(t *testing.T){ - "UserNotRoot": testUserNotRoot, - "SystemdEnabled": testSystemdEnabled, - "SystemdUnits": testSystemdUnits, - "CorrectUpgradePolicy": testCorrectUpgradePolicy, - "UpgradePolicyIdempotent": testUpgradePolicyIdempotent, - "InteropIsEnabled": testInteropIsEnabled, - } - - for name, tc := range testCases { - t.Run(name, tc) - } -} - -// waitStateTransition waits until the current state (fromState) transitions into toState. -// Fails if any other state is reached. -// Fails if the state cannot be parsed. -func waitStateTransition(t *testing.T, fromState string, toState string) { - t.Helper() - - t.Logf("Awaiting state transition: %s -> %s", fromState, toState) - state := distroState(t) - require.Containsf(t, []string{fromState, toState}, state, "In transition from '%s' to '%s': Unexpected state '%s'", fromState, toState, state) - - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - - timeout := time.After(60 * time.Second) - - for { - select { - case <-timeout: - t.Fatalf("Didn't reach expected state in time. Last state: %s", state) - case <-ticker.C: - state = distroState(t) - if state == fromState { - continue - } - } - break - } - - require.Equalf(t, toState, state, "After transition '%s' -> '%s': Unexpected final state '%s'", fromState, toState, state) -} - -// waitForInstaller waits until the subiquity server log indicates that the installation -// has finsihed. -// Considerations: -// - The server may still run for a small amount of time after the log -// says it has finished. Experimentally, it seems to be less than a second. -// - The State of the distro must be either Running or Stopped when called (i.e. installation must have been started). -func waitForInstaller(t *testing.T) { - t.Helper() - - require.Contains(t, []string{"Running", "Stopped"}, distroState(t)) - t.Logf("Waiting for installer to finish") - defer t.Logf("Installation finished") - - ticker := time.NewTicker(time.Second) - defer ticker.Stop() - - timeout := time.After(2 * time.Minute) - - for { - select { - case <-timeout: - t.Fatal("Timed out waiting for installer to finish") - case <-ticker.C: - out, err := wslCommand(context.Background(), "cat", serverLogPath).CombinedOutput() - - var target *exec.ExitError - if errors.As(err, &target) { - require.Equal(t, target.ProcessState.ExitCode(), 1, "Unexpected error reading subiquity server log: %s", out) - continue // Log not yet available - } - require.NoErrorf(t, err, "Unexpected error reading subiquity server log: %s", out) - - if strings.Contains(string(out), "finish: subiquity/SetupShutdown/shutdown: SUCCESS") { - time.Sleep(time.Second) // Offering the server some time to finish - return - } - } - } -} diff --git a/e2e/launchertester/go.mod b/e2e/launchertester/go.mod index eb5b52c8..298ae6ed 100644 --- a/e2e/launchertester/go.mod +++ b/e2e/launchertester/go.mod @@ -1,14 +1,21 @@ module github.com/ubuntu/wsl/e2e/launchertester -go 1.21.5 +go 1.22.0 + +toolchain go1.22.3 require ( - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.9.0 gopkg.in/ini.v1 v1.67.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/ubuntu/decorate v0.0.0-20230125165522-2d5b0a9bb117 // indirect + github.com/ubuntu/gowsl v0.0.0-20240621022151-0ca2385d9153 // indirect + golang.org/x/sys v0.20.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/e2e/launchertester/go.sum b/e2e/launchertester/go.sum index 2f9868ae..c8af9cdc 100644 --- a/e2e/launchertester/go.sum +++ b/e2e/launchertester/go.sum @@ -1,17 +1,31 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ubuntu/decorate v0.0.0-20230125165522-2d5b0a9bb117 h1:XQpsQG5lqRJlx4mUVHcJvyyc1rdTI9nHvwrdfcuy8aM= +github.com/ubuntu/decorate v0.0.0-20230125165522-2d5b0a9bb117/go.mod h1:mx0TjbqsaDD9DUT5gA1s3hw47U6RIbbIBfvGzR85K0g= +github.com/ubuntu/gowsl v0.0.0-20240621022151-0ca2385d9153 h1:WEZ1t/PjXdeyyxTFQwS7SO2wKC8czBf2R6/vcWonD0g= +github.com/ubuntu/gowsl v0.0.0-20240621022151-0ca2385d9153/go.mod h1:t74qqYMKvTnMyTHEDhVlbouDpk5BaIk1TyejRtyTSQ8= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/e2e/launchertester/test_cases.go b/e2e/launchertester/test_cases.go index ac40342c..a733d894 100644 --- a/e2e/launchertester/test_cases.go +++ b/e2e/launchertester/test_cases.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "os/exec" + "regexp" "strings" "testing" "time" @@ -14,23 +15,19 @@ import ( "gopkg.in/ini.v1" ) -// testUserNotRoot ensures the default user is not root. -func testUserNotRoot(t *testing.T) { //nolint: thelper, this is a test - t.Parallel() - +// testDefaultUser ensures the default user matches the expected. +func testDefaultUser(t *testing.T, expected string) { //nolint: thelper, this is a test ctx, cancel := context.WithTimeout(context.Background(), systemdBootTimeout) defer cancel() out, err := wslCommand(ctx, "whoami").CombinedOutput() require.NoErrorf(t, err, "Unexpected failure executing whoami: %s", out) - - require.NotContains(t, string(out), "root", "Default user should not be root.") + got := strings.TrimSpace(string(out)) + require.Equal(t, expected, got, "Default user should be %s, got %s", expected, got) } // testSystemdEnabled ensures systemd was enabled. func testSystemdEnabled(t *testing.T) { //nolint: thelper, this is a test - t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), systemdBootTimeout) defer cancel() @@ -90,11 +87,10 @@ func testSystemdUnits(t *testing.T) { //nolint: thelper, this is a test // testCorrectUpgradePolicy ensures upgrade policy matches the one expected for the app. func testCorrectUpgradePolicy(t *testing.T) { //nolint: thelper, this is a test - t.Parallel() - /* Ubuntu always upgrade to next lts */ wantPolicy := "lts" - if strings.HasPrefix(*distroName, "Ubuntu") && strings.HasSuffix(*distroName, "LTS") { + ltsRegex := regexp.MustCompile(`^Ubuntu-\d{2}\.\d{2}$`) // Ubuntu-WX.YZ + if ltsRegex.MatchString(*distroName) { wantPolicy = "never" } else if *distroName != "Ubuntu" { // Preview and Dev @@ -146,7 +142,6 @@ func testUpgradePolicyIdempotent(t *testing.T) { //nolint: thelper, this is a te // testInteropIsEnabled ensures interop works fine. // See related issue: https://github.com/ubuntu/WSL/issues/334 func testInteropIsEnabled(t *testing.T) { //nolint: thelper, this is a test - t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), systemdBootTimeout) defer cancel() @@ -175,3 +170,11 @@ func testHelpFlag(t *testing.T) { require.NoError(t, err, "could not run '%s help': %v, %s", *launcherName, err, out) require.Contains(t, string(out), usageFirstLine, "help command should have been picked up by the launcher") } + +func testFileExists(t *testing.T, linuxPath string) { + ctx, cancel := context.WithTimeout(context.Background(), systemdBootTimeout) + defer cancel() + + out, err := launcherCommand(ctx, "run", "test", "-e", linuxPath).CombinedOutput() + require.NoError(t, err, "Unexpected error checking file existence: %s", out) +} diff --git a/e2e/launchertester/test_utils.go b/e2e/launchertester/test_utils.go index 2e66e799..6657b1cd 100644 --- a/e2e/launchertester/test_utils.go +++ b/e2e/launchertester/test_utils.go @@ -6,6 +6,7 @@ import ( "context" "flag" "os/exec" + "path/filepath" "regexp" "strings" "testing" @@ -14,12 +15,6 @@ import ( "github.com/stretchr/testify/require" ) -const ( - serverLogPath = "/var/log/installer/systemsetup-server-debug.log" - clientLogPath = "ubuntu-desktop-installer/packages/ubuntu_wsl_setup/build/windows/runner/Debug/.ubuntu_wsl_setup.exe/ubuntu_wsl_setup.exe.log" - subiquityAnswerFile = "/var/log/prefill-system-setup.yaml" -) - // These timeouts have been decided on experimentally, according to the time each action // could reasonably need on an Azure runner. const ( @@ -77,7 +72,7 @@ func checkValidTestbed(t *testing.T) { func terminateDistro(t *testing.T) { t.Helper() out, err := exec.Command("wsl.exe", "--terminate", *distroName).CombinedOutput() - require.NoError(t, err, "Failed to shut down WLS: %s", out) + require.NoError(t, err, "Failed to shut down WSL: %s", out) } // distroState parses the output of "wsl -l -v" to find the state of the current distro. @@ -122,3 +117,24 @@ func distroState(t *testing.T) string { return distroNotFoundMsg } + +// TestFixturePath returns the path of the dir or file for storing fixture specific to the subtest name. +func TestFixturePath(t *testing.T) string { + t.Helper() + + // Ensures that only the name of the parent test is used. + familyName, subtestName, _ := strings.Cut(t.Name(), "/") + + return filepath.Join("testdata", familyName, normalizeName(t, subtestName)) +} + +// normalizeName returns a path from name with illegal Windows +// characters replaced or removed. +func normalizeName(t *testing.T, name string) string { + t.Helper() + + name = strings.ReplaceAll(name, `\`, "_") + name = strings.ReplaceAll(name, ":", "") + name = strings.ToLower(name) + return name +} diff --git a/e2e/launchertester/testdata/TestSetupWithCloudInit/with_broken_passwd_file b/e2e/launchertester/testdata/TestSetupWithCloudInit/with_broken_passwd_file new file mode 100644 index 00000000..7abf8bfc --- /dev/null +++ b/e2e/launchertester/testdata/TestSetupWithCloudInit/with_broken_passwd_file @@ -0,0 +1,35 @@ +#cloud-config +locale: pt_BR.UTF-8 + +# This not become the default user because the shell is not a login shell. +users: + - gecos: Invalid user + name: invalid + groups: users, admin + lock_passwd: true + shell: /bin/false + +# This should become the default user in the end. +runcmd: + - useradd --uid 71000 --create-home --shell /bin/sh --password '*' testmail + +# We're using cloud-init as double-agent, doing it's regular job but also breaking the system for tests. +# Here we'll have a /etc/passwd file with: +# - ubuntu user with UID 2000 but nologin +# - u user with UID 2001 but nologin and an extra non-sense metadata field +# - someuser with just a name and nothing else. +# - UID 1001 without a name +# - break_the_world user with UID 1002 missing fields (home and shell are not present) +# - lxd user with UID 998 which shouldn't interfere with the results +# No one of those lines are suitable for being the default user +write_files: + - path: /etc/passwd + append: true + defer: true # So we don't interfere with the runcmd above + content: | + ubuntu:x:2000:2000:Ubuntu:/home/ubuntu:/usr/bin/nologin + u:x:2001:2001:Ubuntu:/home/ubuntu:/usr/bin/nologin:metadata + someuser: + :x:1001::Another User:/home/anotheruser:/bin/bash + break_the_world::1002:1003:Break The World + lxd:x:998:998:LXD User:/home/lxd:/bin/bash diff --git a/e2e/launchertester/testdata/TestSetupWithCloudInit/with_default_user_in_both b/e2e/launchertester/testdata/TestSetupWithCloudInit/with_default_user_in_both new file mode 100644 index 00000000..19f88dc3 --- /dev/null +++ b/e2e/launchertester/testdata/TestSetupWithCloudInit/with_default_user_in_both @@ -0,0 +1,30 @@ +#cloud-config +apt: + preserve_sources_list: false + disable_suites: + - backports + primary: + - arches: + - default + uri: http://br.archive.ubuntu.com/ubuntu +locale: pt_BR.UTF-8 +users: + - name: nontestuser + groups: sudo + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + - name: anotheruser + groups: sudo + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + - name: testuser + groups: sudo + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + +write_files: + - path: /etc/wsl.conf + append: true + content: | + [user] + default=testuser diff --git a/e2e/launchertester/testdata/TestSetupWithCloudInit/with_default_user_in_conf b/e2e/launchertester/testdata/TestSetupWithCloudInit/with_default_user_in_conf new file mode 100644 index 00000000..9128e70f --- /dev/null +++ b/e2e/launchertester/testdata/TestSetupWithCloudInit/with_default_user_in_conf @@ -0,0 +1,20 @@ +#cloud-config +users: + - name: nontestuser + groups: sudo + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + - name: testuser + groups: sudo + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + +write_files: + - path: /etc/wsl.conf + append: true + content: "[user]\r\ndefault=testuser\r\n" + +runcmd: + # Make sure to take longer than WSL boot timeout (10s). + - sleep 30s + - touch /etc/with_default_user.done diff --git a/e2e/launchertester/testdata/TestSetupWithCloudInit/with_default_user_in_none b/e2e/launchertester/testdata/TestSetupWithCloudInit/with_default_user_in_none new file mode 100644 index 00000000..634f459d --- /dev/null +++ b/e2e/launchertester/testdata/TestSetupWithCloudInit/with_default_user_in_none @@ -0,0 +1,13 @@ +#cloud-config +users: + - name: testuser + groups: sudo + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + - name: nontestuser + groups: sudo + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + +runcmd: + - touch /home/testuser/with_default_user.done diff --git a/e2e/launchertester/testdata/TestSetupWithCloudInit/with_default_user_in_registry b/e2e/launchertester/testdata/TestSetupWithCloudInit/with_default_user_in_registry new file mode 100644 index 00000000..2841acf0 --- /dev/null +++ b/e2e/launchertester/testdata/TestSetupWithCloudInit/with_default_user_in_registry @@ -0,0 +1,17 @@ +#cloud-config +users: + - name: nontestuser + groups: sudo + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + - name: testuser + groups: sudo + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + - name: anotheruser + groups: sudo + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + +runcmd: + - touch /etc/with_default_user.done diff --git a/e2e/launchertester/testdata/TestSetupWithCloudInit/with_only_remote_users b/e2e/launchertester/testdata/TestSetupWithCloudInit/with_only_remote_users new file mode 100644 index 00000000..62f1b978 --- /dev/null +++ b/e2e/launchertester/testdata/TestSetupWithCloudInit/with_only_remote_users @@ -0,0 +1,16 @@ +#cloud-config +apt: + preserve_sources_list: false + disable_suites: + - backports + primary: + - arches: + - default + uri: http://br.archive.ubuntu.com/ubuntu +locale: pt_BR.UTF-8 +users: [] +# Here we're using cloud-init to pretend that the system has remote accounts (LDAP for example) +# Hopefully the system won't have any non-system account with a lower UID. :) +runcmd: + - useradd --uid 73101 --create-home --shell /bin/bash --password '*' somemail + - useradd --uid 71000 --create-home --shell /bin/bash --password '*' testmail diff --git a/e2e/launchertester/testdata/TestSetupWithCloudInit/without_checking_user b/e2e/launchertester/testdata/TestSetupWithCloudInit/without_checking_user new file mode 100644 index 00000000..fff82286 --- /dev/null +++ b/e2e/launchertester/testdata/TestSetupWithCloudInit/without_checking_user @@ -0,0 +1,20 @@ +#cloud-config +users: + - name: nontestuser + groups: sudo + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + - name: testuser + groups: sudo + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + +write_files: + - path: /etc/wsl.conf + append: true + content: | + [user] + default=testuser + +runcmd: + - touch /home/testuser/with_default_user.done