Skip to content

Commit

Permalink
feat(cloud-init): Implement upcoming cloud-init WSL support (#506)
Browse files Browse the repository at this point in the history
Cloud-init will, at some point, be able to pick up files from specified
locations in the Windows file-system. The plan is for UP4W’s agent to
receive cloud-init files from Landscape and put them at the appropriate
location.

Furthermore, system-wide config (Pro token and Landscape) must be
written as well.

---

UDENG-2112
  • Loading branch information
CarlosNihelton authored Apr 19, 2024
2 parents f2eddb6 + d3426c8 commit a60b370
Show file tree
Hide file tree
Showing 41 changed files with 1,053 additions and 302 deletions.
126 changes: 126 additions & 0 deletions end-to-end/cloud_init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package endtoend_test

import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"

landscapeapi "github.com/canonical/landscape-hostagent-api"
"github.com/canonical/ubuntu-pro-for-wsl/common/testutils"
"github.com/stretchr/testify/require"
wsl "github.com/ubuntu/gowsl"
)

func TestCloudInitIntegration(t *testing.T) {
// TODO: Remove this line when cloud-init support for UP4W is released.
// Follow this PR for more information: https://github.com/canonical/cloud-init/pull/5116
t.Skip("This test depends on cloud-init support for UP4W being released.")
currentFuncName := t.Name()

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

testSetup(t)
defer logWindowsAgentOnError(t)

landscape := NewLandscape(t, ctx)
writeUbuntuProRegistry(t, "LandscapeConfig", landscape.ClientConfig)

serverDone := make(chan struct{})
go func() {
defer close(serverDone)
landscape.Serve()
}()
t.Cleanup(func() {
landscape.Stop()
<-serverDone
})
defer landscape.LogOnError(t)

hostname, err := os.Hostname()
require.NoError(t, err, "Setup: could not test machine's hostname")

proToken := os.Getenv(proTokenEnv)
require.NotEmptyf(t, proToken, "Setup: environment variable %q should contain a valid pro token, but is empty", proTokenEnv)
writeUbuntuProRegistry(t, "UbuntuProToken", proToken)

cleanup := startAgent(t, ctx, currentFuncName)
defer cleanup()

info := landscape.RequireReceivedInfo(t, proToken, nil, hostname)

out, err := os.ReadFile(filepath.Join(testutils.TestFixturePath(t), "user-data.yaml"))
require.NoError(t, err, "Setup: could not read cloud-init file")
cloudInitUserData := string(out)

err = landscape.service.SendCommand(ctx, info.UID, &landscapeapi.Command{
Cmd: &landscapeapi.Command_Install_{
Install: &landscapeapi.Command_Install{
Id: strings.Split(referenceDistroAppx, ".")[1], // CanonicalGroupLimited.[UbuntuPreview]
Cloudinit: &cloudInitUserData,
},
},
})
require.NoError(t, err, "Setup: could not send install command")

distro := wsl.NewDistro(ctx, referenceDistro)

//nolint:errcheck // Nothing we can do about it
defer distro.Unregister()

require.Eventually(t, func() bool {
ok, err := distro.IsRegistered()
if err != nil {
t.Logf("could not determine if distro is registered: %v", err)
return false
}
return ok
}, time.Minute, time.Second, "Distro should have been registered")

defer logWslProServiceOnError(t, ctx, distro)

runCommand(t, ctx, time.Minute, distro, "cloud-init status --wait")

require.Eventually(t, func() bool {
attached, err := distroIsProAttached(t, ctx, distro)
if err != nil {
t.Logf("could not determine if distro is attached: %v", err)
return false
}
return attached
}, 10*time.Second, time.Second, "distro should have been Pro attached")

userName := runCommand(t, ctx, 10*time.Second, distro, "whoami")
require.Equal(t, "testuser", userName, "cloud-init should have configured the default user")

landscape.RequireReceivedInfo(t, proToken, []wsl.Distro{distro}, hostname)
landscape.RequireUninstallCommand(t, ctx, distro, info)
}

//nolint:revive // t always goes before ctx
func runCommand(t *testing.T, ctx context.Context, timeout time.Duration, distro wsl.Distro, comand string) string {
t.Helper()

ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

out, err := distro.Command(ctx, comand).CombinedOutput()
if err == nil {
return string(out)
}

// We check the context to see if it was a timeout.
// This makes the error message easier to understand.

select {
case <-ctx.Done():
require.Fail(t, "Timed out waiting for cloud-init to finish")
default:
}

require.NoError(t, err, "could not determine if cloud-init is done: %s. Output: %s", out, out)
return ""
}
11 changes: 7 additions & 4 deletions end-to-end/landscape_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func (l landscape) Stop() {
}

// RequireReceivedInfo checks that a connection to Landscape was made and the proper information was sent.
func (l landscape) RequireReceivedInfo(t *testing.T, wantToken string, wantDistro gowsl.Distro, wantHostname string) landscapemockservice.HostInfo {
func (l landscape) RequireReceivedInfo(t *testing.T, wantToken string, wantDistros []gowsl.Distro, wantHostname string) landscapemockservice.HostInfo {
t.Helper()

require.Eventually(t, func() bool {
Expand All @@ -122,9 +122,12 @@ func (l landscape) RequireReceivedInfo(t *testing.T, wantToken string, wantDistr
// Validate token
require.Equal(t, wantToken, info.Token, "Landscape did not receive the right pro token")

// Validate distro
require.Len(t, info.Instances, 1, "Landscape did not receive the right number of distros")
require.Equal(t, wantDistro.Name(), info.Instances[0].ID, "Landscape did not receive the right distro name from the agent")
// Validate distros
gotDistros := make([]string, 0)
for _, instance := range info.Instances {
gotDistros = append(gotDistros, instance.ID)
}
require.ElementsMatch(t, wantDistros, gotDistros, "Landscape did not receive the right distros")

// Validate hostname
require.Equal(t, wantHostname, info.Hostname, "Landscape did not receive the right hostname from the agent")
Expand Down
19 changes: 15 additions & 4 deletions end-to-end/manual_token_input_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import (
)

func TestManualTokenInput(t *testing.T) {
// TODO: Remove this line when cloud-init support for UP4W is released.
// Follow this PR for more information: https://github.com/canonical/cloud-init/pull/5116
t.Skip("This test depends on cloud-init support for UP4W being released.")

type whenToken int
const (
never whenToken = iota
Expand Down Expand Up @@ -44,9 +48,16 @@ func TestManualTokenInput(t *testing.T) {
landscape := NewLandscape(t, ctx)
writeUbuntuProRegistry(t, "LandscapeConfig", landscape.ClientConfig)

go landscape.Serve()
serverDone := make(chan struct{})
go func() {
defer close(serverDone)
landscape.Serve()
}()
t.Cleanup(func() {
landscape.Stop()
<-serverDone
})
defer landscape.LogOnError(t)
defer landscape.Stop()

hostname, err := os.Hostname()
require.NoError(t, err, "Setup: could not test machine's hostname")
Expand All @@ -63,7 +74,7 @@ func TestManualTokenInput(t *testing.T) {

defer logWslProServiceOnError(t, ctx, d)

out, err := d.Command(ctx, "exit 0").CombinedOutput()
out, err := d.Command(ctx, "cloud-init status --wait").CombinedOutput()
require.NoErrorf(t, err, "Setup: could not wake distro up: %v. %s", err, out)

// ... or after registration, but never both.
Expand Down Expand Up @@ -94,7 +105,7 @@ func TestManualTokenInput(t *testing.T) {
return attached
}, maxTimeout, time.Second, "distro should have been Pro attached")

info := landscape.RequireReceivedInfo(t, os.Getenv(proTokenEnv), d, hostname)
info := landscape.RequireReceivedInfo(t, os.Getenv(proTokenEnv), []wsl.Distro{d}, hostname)
landscape.RequireUninstallCommand(t, ctx, d, info)
})
}
Expand Down
19 changes: 15 additions & 4 deletions end-to-end/organization_token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import (
)

func TestOrganizationProvidedToken(t *testing.T) {
// TODO: Remove this line when cloud-init support for UP4W is released.
// Follow this PR for more information: https://github.com/canonical/cloud-init/pull/5116
t.Skip("This test depends on cloud-init support for UP4W being released.")

type whenToken int
const (
never whenToken = iota
Expand Down Expand Up @@ -43,9 +47,16 @@ func TestOrganizationProvidedToken(t *testing.T) {
landscape := NewLandscape(t, ctx)
writeUbuntuProRegistry(t, "LandscapeConfig", landscape.ClientConfig)

go landscape.Serve()
serverDone := make(chan struct{})
go func() {
defer close(serverDone)
landscape.Serve()
}()
t.Cleanup(func() {
landscape.Stop()
<-serverDone
})
defer landscape.LogOnError(t)
defer landscape.Stop()

hostname, err := os.Hostname()
require.NoError(t, err, "Setup: could not test machine's hostname")
Expand All @@ -65,7 +76,7 @@ func TestOrganizationProvidedToken(t *testing.T) {

defer logWslProServiceOnError(t, ctx, d)

out, err := d.Command(ctx, "exit 0").CombinedOutput()
out, err := d.Command(ctx, "cloud-init status --wait").CombinedOutput()
require.NoErrorf(t, err, "Setup: could not wake distro up: %v. %s", err, out)

if tc.whenToken == afterDistroRegistration {
Expand Down Expand Up @@ -99,7 +110,7 @@ func TestOrganizationProvidedToken(t *testing.T) {
return attached
}, maxTimeout, time.Second, "distro should have been Pro attached")

info := landscape.RequireReceivedInfo(t, proToken, d, hostname)
info := landscape.RequireReceivedInfo(t, proToken, []wsl.Distro{d}, hostname)
landscape.RequireUninstallCommand(t, ctx, d, info)
})
}
Expand Down
19 changes: 15 additions & 4 deletions end-to-end/purchase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const (
)

func TestPurchase(t *testing.T) {
// TODO: Remove this line when cloud-init support for UP4W is released.
// Follow this PR for more information: https://github.com/canonical/cloud-init/pull/5116
t.Skip("This test depends on cloud-init support for UP4W being released.")

type whenToken int
const (
never whenToken = iota
Expand Down Expand Up @@ -58,9 +62,16 @@ func TestPurchase(t *testing.T) {
landscape := NewLandscape(t, ctx)
writeUbuntuProRegistry(t, "LandscapeConfig", landscape.ClientConfig)

go landscape.Serve()
serverDone := make(chan struct{})
go func() {
defer close(serverDone)
landscape.Serve()
}()
t.Cleanup(func() {
landscape.Stop()
<-serverDone
})
defer landscape.LogOnError(t)
defer landscape.Stop()

hostname, err := os.Hostname()
require.NoError(t, err, "Setup: could not test machine's hostname")
Expand Down Expand Up @@ -121,7 +132,7 @@ func TestPurchase(t *testing.T) {

defer logWslProServiceOnError(t, ctx, d)

out, err := d.Command(ctx, "exit 0").CombinedOutput()
out, err := d.Command(ctx, "cloud-init status --wait").CombinedOutput()
require.NoErrorf(t, err, "Setup: could not wake distro up: %v. %s", err, out)

// ... or after registration, but never both.
Expand Down Expand Up @@ -153,7 +164,7 @@ func TestPurchase(t *testing.T) {
return attached
}, maxTimeout, time.Second, "distro should have been Pro attached")

landscape.RequireReceivedInfo(t, token, d, hostname)
landscape.RequireReceivedInfo(t, token, []wsl.Distro{d}, hostname)
// Skipping because we know it to be broken
// See https://warthogs.atlassian.net/browse/UDENG-1810
//
Expand Down
13 changes: 13 additions & 0 deletions end-to-end/testdata/TestCloudInitIntegration/user-data.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#cloud-config
users:
- name: testuser
gecos: Test User
sudo: ALL=(ALL) NOPASSWD:ALL
groups: [adm,dialout,cdrom,floppy,sudo,audio,dip,video,plugdev,users,netdev]
shell: /bin/bash
write_files:
- path: /etc/wsl.conf
append: true
content: |
[user]
default=testuser
1 change: 1 addition & 0 deletions gui/packages/ubuntupro/end_to_end/end_to_end_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ void main(List<String> args) {

const testCases = {
'TestOrganizationProvidedToken': testOrganizationProvidedToken,
'TestCloudInitIntegration': testOrganizationProvidedToken,
'TestManualTokenInput': testManualTokenInput,
'TestPurchase': testPurchase,
};
Expand Down
Loading

0 comments on commit a60b370

Please sign in to comment.