Skip to content

Commit

Permalink
Fallback to installing AMD64 plugins on Windows (#615)
Browse files Browse the repository at this point in the history
Although the CLI is available for Windows ARM64, the vast majority of
plugins are not, and therefore an ARM64 CLI will not be able to install
most plugins.

Thanks to Windows 11's emulator available on Windows ARM64 machines,
it is possible to run AMD64 binaries.

This commit teaches a Windows ARM64 CLI that if a plugin is not
available for Windows ARM64, it should instead install the Windows AMD64
version. With this approach, it now becomes possible to use a Windows
ARM64 CLI to its full potential.

Note that this approach cannot be used for Linux as there is no standard
emulator for AMD64 binaries.  For Linux, plugins will need to be
published for ARM64.

Note also that if running on Windows 10 ARM64, the AMD64 plugins will
get installed but will not work.  This is not much worse than simply not
installing a plugin that is not available for ARM64.  Also, this limitation only
impacts Windows 10 ARM64 users, which should be a very limited amount
of users.  We will communicate this limitation through release notes.

Signed-off-by: Marc Khouzam <[email protected]>
  • Loading branch information
marckhouzam authored Dec 21, 2023
1 parent ac8d95e commit 65c0ae0
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 31 deletions.
27 changes: 18 additions & 9 deletions pkg/pluginmanager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -505,15 +505,24 @@ func installPlugin(pluginName, version string, target configtypes.Target, contex
errorList = append(errorList, err)
}

// If we cannot find the plugin for DarwinARM64, let's fallback to DarwinAMD64.
// This leverages Apples Rosetta emulator and helps until plugins are all available
// for ARM64. Note that this cannot be used on Linux since there is no such emulator.
if len(availablePlugins) == 0 && cli.BuildArch() == cli.DarwinARM64 {
// Pretend we are on a AMD64 machine
cli.SetArch(cli.DarwinAMD64)
defer cli.SetArch(cli.DarwinARM64) // Go back to ARM64 once the plugin is installed

criteria.Arch = cli.DarwinAMD64.Arch()
// If we cannot find the plugin for ARM64, let's fallback to AMD64 for Darwin and Windows.
// This leverages Apples Rosetta emulator and Windows 11 emulator until plugins
// are all available for ARM64. Note that this approach cannot be used on Linux since there
// is no such emulator.
if len(availablePlugins) == 0 &&
(cli.BuildArch() == cli.DarwinARM64 || cli.BuildArch() == cli.WinARM64) {
// Pretend we are on a AMD64 machine so that we can find the plugin.
switch cli.BuildArch() {
case cli.DarwinARM64:
cli.SetArch(cli.DarwinAMD64)
criteria.Arch = cli.DarwinAMD64.Arch()
defer cli.SetArch(cli.DarwinARM64) // Go back to ARM64 once the plugin is installed
case cli.WinARM64:
cli.SetArch(cli.WinAMD64)
criteria.Arch = cli.WinAMD64.Arch()
defer cli.SetArch(cli.WinARM64) // Go back to ARM64 once the plugin is installed
}

availablePlugins, err = discoverSpecificPlugins(discoveries, discovery.WithPluginDiscoveryCriteria(criteria))
if err != nil {
errorList = append(errorList, err)
Expand Down
16 changes: 11 additions & 5 deletions pkg/pluginmanager/manager_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ var testPlugins = []plugininventory.PluginIdentifier{
{Name: "cluster", Target: configtypes.TargetK8s, Version: "v1.6.0"},
{Name: "myplugin", Target: configtypes.TargetK8s, Version: "v1.6.0"},
{Name: "feature", Target: configtypes.TargetK8s, Version: "v0.2.0"},
{Name: "pluginwitharm", Target: configtypes.TargetK8s, Version: "v2.0.0"},
{Name: "pluginwitharmdarwin", Target: configtypes.TargetK8s, Version: "v2.0.0"},

{Name: "isolated-cluster", Target: configtypes.TargetGlobal, Version: "v1.2.3"},
{Name: "isolated-cluster", Target: configtypes.TargetGlobal, Version: "v1.3.0"},
Expand All @@ -47,10 +47,12 @@ var testPlugins = []plugininventory.PluginIdentifier{
{Name: "management-cluster", Target: configtypes.TargetTMC, Version: "v0.2.0"},
{Name: "cluster", Target: configtypes.TargetTMC, Version: "v0.2.0"},
{Name: "myplugin", Target: configtypes.TargetTMC, Version: "v0.2.0"},
{Name: "pluginwitharmwindows", Target: configtypes.TargetTMC, Version: "v4.0.0"},
}

var testPluginsNoARM64 = []plugininventory.PluginIdentifier{
{Name: "pluginnoarm", Target: configtypes.TargetK8s, Version: "v1.0.0"},
{Name: "pluginnoarmdarwin", Target: configtypes.TargetK8s, Version: "v1.0.0"},
{Name: "pluginnoarmwindows", Target: configtypes.TargetTMC, Version: "v3.0.0"},
}

var installedStandalonePlugins = []plugininventory.PluginIdentifier{
Expand Down Expand Up @@ -199,14 +201,18 @@ func findGroupVersion(allGroups []*plugininventory.PluginGroup, id string) bool
return false
}

func setupPluginBinaryInCache(name, version string, target configtypes.Target, digest string) {
func setupPluginBinaryInCache(name, version string, target configtypes.Target, arch cli.Arch, digest string) {
dir := filepath.Join(common.DefaultPluginRoot, name)
err := os.MkdirAll(dir, 0755)
if err != nil {
log.Fatal(err, "unable to create temporary directory for plugin binary")
}

pluginBinary := filepath.Join(dir, fmt.Sprintf("%s_%s_%s", version, digest, target))
if arch.IsWindows() {
pluginBinary += exe
}

f, err := os.OpenFile(pluginBinary, os.O_CREATE|os.O_WRONLY, 0755)
if err != nil {
log.Fatal(err, "unable to create temporary plugin binary")
Expand Down Expand Up @@ -254,7 +260,7 @@ func setupPluginEntriesAndBinaries(db *sql.DB) {
digest = digestForARM64
}
createPluginEntry(db, plugin, osArch, digest)
setupPluginBinaryInCache(plugin.Name, plugin.Version, plugin.Target, digest)
setupPluginBinaryInCache(plugin.Name, plugin.Version, plugin.Target, osArch, digest)
}
}

Expand All @@ -264,7 +270,7 @@ func setupPluginEntriesAndBinaries(db *sql.DB) {
for _, osArch := range cli.AllOSArch {
if osArch.Arch() != cli.DarwinARM64.Arch() {
createPluginEntry(db, plugin, osArch, digest)
setupPluginBinaryInCache(plugin.Name, plugin.Version, plugin.Target, digest)
setupPluginBinaryInCache(plugin.Name, plugin.Version, plugin.Target, osArch, digest)
}
}
}
Expand Down
91 changes: 74 additions & 17 deletions pkg/pluginmanager/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

"github.com/pkg/errors"
Expand Down Expand Up @@ -131,22 +132,39 @@ var expectedDiscoveredStandalonePlugins = []discovery.Discovered{
Target: configtypes.TargetTMC,
},
{
Name: "pluginnoarm",
Description: "Plugin pluginnoarm description",
Name: "pluginnoarmdarwin",
Description: "Plugin pluginnoarmdarwin description",
RecommendedVersion: "v1.0.0",
SupportedVersions: []string{"v1.0.0"},
Scope: common.PluginScopeStandalone,
ContextName: "",
Target: configtypes.TargetK8s,
},
{
Name: "pluginwitharm",
Description: "Plugin pluginwitharm description",
Name: "pluginwitharmdarwin",
Description: "Plugin pluginwitharmdarwin description",
RecommendedVersion: "v2.0.0",
SupportedVersions: []string{"v2.0.0"},
Scope: common.PluginScopeStandalone,
ContextName: "",
Target: configtypes.TargetK8s,
}, {
Name: "pluginnoarmwindows",
Description: "Plugin pluginnoarmwindows description",
RecommendedVersion: "v3.0.0",
SupportedVersions: []string{"v3.0.0"},
Scope: common.PluginScopeStandalone,
ContextName: "",
Target: configtypes.TargetTMC,
},
{
Name: "pluginwitharmwindows",
Description: "Plugin pluginwitharmwindows description",
RecommendedVersion: "v4.0.0",
SupportedVersions: []string{"v4.0.0"},
Scope: common.PluginScopeStandalone,
ContextName: "",
Target: configtypes.TargetTMC,
},
}

Expand Down Expand Up @@ -291,7 +309,7 @@ func Test_InstallStandalonePlugin(t *testing.T) {
// the unit tests are run on Linux for example.
realArch := cli.BuildArch()
cli.SetArch(cli.DarwinARM64)
err = InstallStandalonePlugin("pluginnoarm", "v1.0.0", configtypes.TargetUnknown)
err = InstallStandalonePlugin("pluginnoarmdarwin", "v1.0.0", configtypes.TargetUnknown)
assertions.Nil(err)
// Make sure that after the plugin is installed (using AMD64), the arch is back to ARM64
assertions.Equal(cli.DarwinARM64, cli.BuildArch())
Expand All @@ -304,20 +322,39 @@ func Test_InstallStandalonePlugin(t *testing.T) {
// First make the CLI believe we are running on Darwin ARM64. We need this for when
// the unit tests are run on Linux for example.
cli.SetArch(cli.DarwinARM64)
err = InstallStandalonePlugin("pluginwitharm", "v2.0.0", configtypes.TargetUnknown)
err = InstallStandalonePlugin("pluginwitharmdarwin", "v2.0.0", configtypes.TargetUnknown)
assertions.Nil(err)
// Make sure that after the plugin is installed (using AMD64), the arch is back to ARM64
assertions.Equal(cli.DarwinARM64, cli.BuildArch())
// Now reset to the real machine architecture
cli.SetArch(realArch)

// Verify installed plugins
installedStandalonePlugins, err := pluginsupplier.GetInstalledStandalonePlugins()
// When on Windows ARM64, try installing a plugin that is only available for Windows AMD64
// and see that it still gets installed (it will use AMD64)
//
// First make the CLI believe we are running on Windows ARM64. We need this for when
// the unit tests are run on Linux for example.
realArch = cli.BuildArch()
cli.SetArch(cli.WinARM64)
err = InstallStandalonePlugin("pluginnoarmwindows", "v3.0.0", configtypes.TargetUnknown)
assertions.Nil(err)
assertions.Equal(6, len(installedStandalonePlugins))
installedServerPlugins, err := pluginsupplier.GetInstalledServerPlugins()
// Make sure that after the plugin is installed (using AMD64), the arch is back to ARM64
assertions.Equal(cli.WinARM64, cli.BuildArch())
// Now reset to the real machine architecture
cli.SetArch(realArch)

// When on Windows ARM64, try installing a plugin that IS available for Windows ARM64
// and make sure it is the ARM64 one that gets installed (not AMD64)
//
// First make the CLI believe we are running on Windows ARM64. We need this for when
// the unit tests are run on Linux for example.
cli.SetArch(cli.WinARM64)
err = InstallStandalonePlugin("pluginwitharmwindows", "v4.0.0", configtypes.TargetUnknown)
assertions.Nil(err)
assertions.Equal(0, len(installedServerPlugins))
// Make sure that after the plugin is installed (using AMD64), the arch is back to ARM64
assertions.Equal(cli.WinARM64, cli.BuildArch())
// Now reset to the real machine architecture
cli.SetArch(realArch)

expectedInstalledStandalonePlugins := []cli.PluginInfo{
{
Expand Down Expand Up @@ -345,31 +382,51 @@ func Test_InstallStandalonePlugin(t *testing.T) {
Target: configtypes.TargetTMC,
},
{
Name: "pluginnoarm",
Name: "pluginnoarmdarwin",
Version: "v1.0.0",
Scope: common.PluginScopeStandalone,
Target: configtypes.TargetK8s,
},
{
Name: "pluginwitharm",
Name: "pluginwitharmdarwin",
Version: "v2.0.0",
Scope: common.PluginScopeStandalone,
Target: configtypes.TargetK8s,
},
{
Name: "pluginnoarmwindows",
Version: "v3.0.0",
Scope: common.PluginScopeStandalone,
Target: configtypes.TargetTMC,
},
{
Name: "pluginwitharmwindows",
Version: "v4.0.0",
Scope: common.PluginScopeStandalone,
Target: configtypes.TargetTMC,
},
}

// Verify installed plugins
installedStandalonePlugins, err := pluginsupplier.GetInstalledStandalonePlugins()
assertions.Nil(err)
assertions.Equal(len(expectedInstalledStandalonePlugins), len(installedStandalonePlugins))
installedServerPlugins, err := pluginsupplier.GetInstalledServerPlugins()
assertions.Nil(err)
assertions.Equal(0, len(installedServerPlugins))

for i := 0; i < len(expectedInstalledStandalonePlugins); i++ {
pd := findPluginInfo(installedStandalonePlugins, expectedInstalledStandalonePlugins[i].Name, expectedInstalledStandalonePlugins[i].Target)
assertions.NotNil(pd)
assertions.Equal(expectedInstalledStandalonePlugins[i].Version, pd.Version)

if pd.Name == "pluginnoarm" {
// Make sure this plugin is always installed as AMD64
if strings.HasPrefix(pd.Name, "pluginnoarm") {
// Make sure these plugins are always installed as AMD64
assertions.Equal(digestForAMD64, pd.Digest)
}

if pd.Name == "pluginwitharm" {
// Make sure this plugin is always installed as ARM64
if strings.HasPrefix(pd.Name, "pluginwitharm") {
// Make sure these plugins are always installed as ARM64
assertions.Equal(digestForARM64, pd.Digest)
}
}
Expand Down

0 comments on commit 65c0ae0

Please sign in to comment.