Skip to content

Commit

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

Thanks to Apple's Rosetta emulator available on Darwin ARM64 machines,
it is possible to run AMD64 binaries.  This is currently how the CLI
and its plugins are being run.

This commit teaches a Darwin ARM64 CLI that if a plugin is not available
for Darwin ARM64, it should instead install the Darwin AMD64 version.
With this approach, it now become possible to use a Darwin 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.

Unit tests have been added for this new feature.

Signed-off-by: Marc Khouzam <[email protected]>
  • Loading branch information
marckhouzam committed Sep 21, 2023
1 parent c0e6210 commit e1d776d
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 30 deletions.
10 changes: 9 additions & 1 deletion pkg/cli/arch.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,22 @@ var (

// AllOSArch defines all OS/ARCH combination for which plugin can be built
AllOSArch = []Arch{LinuxAMD64, DarwinAMD64, WinAMD64, DarwinARM64, LinuxARM64}

GOOS = runtime.GOOS
GOARCH = runtime.GOARCH
)

// Arch represents a system architecture.
type Arch string

// BuildArch returns compile time build arch or locates it.
func BuildArch() Arch {
return Arch(fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH))
return Arch(fmt.Sprintf("%s_%s", GOOS, GOARCH))
}

func SetArch(a Arch) {
GOOS = a.OS()
GOARCH = a.Arch()
}

// IsWindows tells if an arch is windows.
Expand Down
38 changes: 26 additions & 12 deletions pkg/pluginmanager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"

"github.com/pkg/errors"
Expand Down Expand Up @@ -459,15 +458,30 @@ func installPlugin(pluginName, version string, target configtypes.Target, contex
Name: pluginName,
Target: target,
Version: version,
OS: runtime.GOOS,
Arch: runtime.GOARCH,
OS: cli.GOOS,
Arch: cli.GOARCH,
}
errorList := make([]error, 0)
availablePlugins, err := discoverSpecificPlugins(discoveries, discovery.WithPluginDiscoveryCriteria(criteria))
if err != nil {
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()
availablePlugins, err = discoverSpecificPlugins(discoveries, discovery.WithPluginDiscoveryCriteria(criteria))
if err != nil {
errorList = append(errorList, err)
}
}

if len(availablePlugins) == 0 {
if target != configtypes.TargetUnknown {
errorList = append(errorList, errors.Errorf("unable to find plugin '%v' with version '%v' for target '%s'", pluginName, version, string(target)))
Expand Down Expand Up @@ -666,7 +680,7 @@ func installOrUpgradePlugin(p *discovery.Discovered, version string, installTest
}

func getPluginFromCache(p *discovery.Discovered, version string) *cli.PluginInfo {
pluginArtifact, err := p.Distribution.DescribeArtifact(version, runtime.GOOS, runtime.GOARCH)
pluginArtifact, err := p.Distribution.DescribeArtifact(version, cli.GOOS, cli.GOARCH)
if err != nil {
return nil
}
Expand Down Expand Up @@ -698,13 +712,13 @@ func fetchAndVerifyPlugin(p *discovery.Discovered, version string) ([]byte, erro
return nil, errors.Wrapf(err, "%q plugin pre-download verification failed", p.Name)
}

b, err := p.Distribution.Fetch(version, runtime.GOOS, runtime.GOARCH)
b, err := p.Distribution.Fetch(version, cli.GOOS, cli.GOARCH)
if err != nil {
return nil, errors.Wrapf(err, "unable to fetch the plugin metadata for plugin %q", p.Name)
}

// verify plugin after download but before installation
d, err := p.Distribution.GetDigest(version, runtime.GOOS, runtime.GOARCH)
d, err := p.Distribution.GetDigest(version, cli.GOOS, cli.GOARCH)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -759,7 +773,7 @@ func describePlugin(p *discovery.Discovered, pluginPath string) (*cli.PluginInfo

func doInstallTestPlugin(p *discovery.Discovered, pluginPath, version string) error {
log.Infof("Installing test plugin for '%v:%v'", p.Name, version)
binary, err := p.Distribution.FetchTest(version, runtime.GOOS, runtime.GOARCH)
binary, err := p.Distribution.FetchTest(version, cli.GOOS, cli.GOARCH)
if err != nil {
if os.Getenv("TZ_ENFORCE_TEST_PLUGIN") == "1" {
return errors.Wrapf(err, "unable to install test plugin for '%v:%v'", p.Name, version)
Expand Down Expand Up @@ -1126,8 +1140,8 @@ func getCLIPluginResourceWithLocalDistroFromPluginInfo(plugin *cli.PluginInfo, p
{
URI: pluginBinaryPath,
Type: common.DistributionTypeLocal,
OS: runtime.GOOS,
Arch: runtime.GOARCH,
OS: cli.GOOS,
Arch: cli.GOARCH,
},
},
},
Expand Down Expand Up @@ -1190,7 +1204,7 @@ func discoverPluginsFromLocalSourceBasedOnManifestFile(localPath string) ([]disc
// Sample path: cli/[target]/<plugin-name>/<plugin-version>/tanzu-<plugin-name>-<os>_<arch>
// cli/[target]/v0.14.0/tanzu-login-darwin_amd64
// As mentioned above, we expect the binary for user's OS-ARCH is present and hence creating path accordingly
pluginBinaryPath := filepath.Join(absLocalPath, string(pluginInfo.Target), p.Name, pluginInfo.Version, fmt.Sprintf("tanzu-%s-%s_%s", p.Name, runtime.GOOS, runtime.GOARCH))
pluginBinaryPath := filepath.Join(absLocalPath, string(pluginInfo.Target), p.Name, pluginInfo.Version, fmt.Sprintf("tanzu-%s-%s_%s", p.Name, cli.GOOS, cli.GOARCH))
if cli.BuildArch().IsWindows() {
pluginBinaryPath += exe
}
Expand Down Expand Up @@ -1249,7 +1263,7 @@ func getPluginInfoResource(pluginFilePath string) (*cli.PluginInfo, error) {
// verifyPluginPreDownload verifies that the plugin distribution repo is trusted
// and returns error if the verification fails.
func verifyPluginPreDownload(p *discovery.Discovered, version string) error {
artifactInfo, err := p.Distribution.DescribeArtifact(version, runtime.GOOS, runtime.GOARCH)
artifactInfo, err := p.Distribution.DescribeArtifact(version, cli.GOOS, cli.GOARCH)
if err != nil {
return err
}
Expand All @@ -1259,7 +1273,7 @@ func verifyPluginPreDownload(p *discovery.Discovered, version string) error {
if artifactInfo.URI != "" {
return verifyArtifactLocation(artifactInfo.URI)
}
return errors.Errorf("no download information available for artifact \"%s:%s:%s:%s\"", p.Name, p.RecommendedVersion, runtime.GOOS, runtime.GOARCH)
return errors.Errorf("no download information available for artifact \"%s:%s:%s:%s\"", p.Name, p.RecommendedVersion, cli.GOOS, cli.GOARCH)
}

// verifyRegistry verifies the authenticity of the registry from where cli is
Expand Down
44 changes: 36 additions & 8 deletions pkg/pluginmanager/manager_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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: "isolated-cluster", Target: configtypes.TargetGlobal, Version: "v1.2.3"},
{Name: "isolated-cluster", Target: configtypes.TargetGlobal, Version: "v1.3.0"},
Expand All @@ -45,6 +46,10 @@ var testPlugins = []plugininventory.PluginIdentifier{
{Name: "myplugin", Target: configtypes.TargetTMC, Version: "v0.2.0"},
}

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

const createGroupsStmt = `
INSERT INTO PluginGroups VALUES(
'vmware',
Expand Down Expand Up @@ -113,6 +118,10 @@ INSERT INTO PluginGroups VALUES(
'true',
'false');
`
const (
digestForAMD64 = "0000000000"
digestForARM64 = "1111111111"
)

func findDiscoveredPlugin(discovered []discovery.Discovered, pluginName string, target configtypes.Target) *discovery.Discovered {
for i := range discovered {
Expand Down Expand Up @@ -183,20 +192,39 @@ func setupPluginBinaryInCache(name, version string, target configtypes.Target, d
}
}

func createPluginEntry(db *sql.DB, plugin plugininventory.PluginIdentifier, arch cli.Arch, digest string) {
uri := fmt.Sprintf("vmware/test/%s/%s/%s/%s:%s", arch.OS(), arch.Arch(), plugin.Target, plugin.Name, plugin.Version)
desc := fmt.Sprintf("Plugin %s description", plugin.Name)

_, err := db.Exec("INSERT INTO PluginBinaries (PluginName,Target,RecommendedVersion,Version,Hidden,Description,Publisher,Vendor,OS,Architecture,Digest,URI) VALUES(?,?,'',?,'false',?,'test','vmware',?,?,?,?);", plugin.Name, plugin.Target, plugin.Version, desc, arch.OS(), arch.Arch(), digest, uri)

if err != nil {
log.Fatal(err, fmt.Sprintf("failed to create %s:%s for target %s for testing", plugin.Name, plugin.Version, plugin.Target))
}
}

func setupPluginEntriesAndBinaries(db *sql.DB) {
digest := "0000000000"
// Setup DB entries and plugin binaries for all os/architecture combinations
for _, plugin := range testPlugins {
desc := fmt.Sprintf("Plugin %s description", plugin.Name)
for _, osArch := range cli.AllOSArch {
uri := fmt.Sprintf("vmware/test/%s/%s/%s/%s:%s", osArch.OS(), osArch.Arch(), plugin.Target, plugin.Name, plugin.Version)

_, err := db.Exec("INSERT INTO PluginBinaries (PluginName,Target,RecommendedVersion,Version,Hidden,Description,Publisher,Vendor,OS,Architecture,Digest,URI) VALUES(?,?,'',?,'false',?,'test','vmware',?,?,?,?);", plugin.Name, plugin.Target, plugin.Version, desc, osArch.OS(), osArch.Arch(), digest, uri)
digest := digestForAMD64
if osArch.Arch() == cli.DarwinARM64.Arch() {
digest = digestForARM64
}
createPluginEntry(db, plugin, osArch, digest)
setupPluginBinaryInCache(plugin.Name, plugin.Version, plugin.Target, digest)
}
}

if err != nil {
log.Fatal(err, fmt.Sprintf("failed to create %s:%s for target %s for testing", plugin.Name, plugin.Version, plugin.Target))
// Setup DB entries and plugin binaries but skip ARM64
digest := digestForAMD64
for _, plugin := range testPluginsNoARM64 {
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, digest)
}
}

Expand Down
60 changes: 59 additions & 1 deletion pkg/pluginmanager/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ var expectedDiscoveredStandalonePlugins = []discovery.Discovered{
ContextName: "",
Target: configtypes.TargetTMC,
},
{
Name: "pluginnoarm",
Description: "Plugin pluginnoarm description",
RecommendedVersion: "v1.0.0",
SupportedVersions: []string{"v1.0.0"},
Scope: common.PluginScopeStandalone,
ContextName: "",
Target: configtypes.TargetK8s,
},
}

var expectedDiscoveredGroups = []string{"vmware-test/default:v1.6.0", "vmware-test/default:v2.1.0"}
Expand Down Expand Up @@ -240,10 +249,37 @@ func Test_InstallStandalonePlugin(t *testing.T) {
assertions.NotNil(err)
assertions.Contains(err.Error(), "unable to find plugin 'feature' with version 'v0.2.0' for target 'mission-control'")

// When on Darwin ARM64, try installing a plugin that is only available for Darwin AMD64
// and see that it still gets installed (it will use AMD64)
//
// 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.
realArch := cli.BuildArch()
cli.SetArch(cli.DarwinARM64)
err = InstallStandalonePlugin("pluginnoarm", "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())
// Now reset to the real machine architecture
cli.SetArch(realArch)

// When on Darwin ARM64, try installing a plugin that IS available for Darwin ARM64
// and make sure it is the ARM64 one that gets installed (not AMD64)
//
// 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)
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()
assertions.Nil(err)
assertions.Equal(4, len(installedStandalonePlugins))
assertions.Equal(6, len(installedStandalonePlugins))
installedServerPlugins, err := pluginsupplier.GetInstalledServerPlugins()
assertions.Nil(err)
assertions.Equal(0, len(installedServerPlugins))
Expand Down Expand Up @@ -273,12 +309,34 @@ func Test_InstallStandalonePlugin(t *testing.T) {
Scope: common.PluginScopeStandalone,
Target: configtypes.TargetTMC,
},
{
Name: "pluginnoarm",
Version: "v1.0.0",
Scope: common.PluginScopeStandalone,
Target: configtypes.TargetK8s,
},
{
Name: "pluginwitharm",
Version: "v2.0.0",
Scope: common.PluginScopeStandalone,
Target: configtypes.TargetK8s,
},
}

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
assertions.Equal(digestForAMD64, pd.Digest)
}

if pd.Name == "pluginwitharm" {
// Make sure this plugin is always installed as ARM64
assertions.Equal(digestForARM64, pd.Digest)
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/registry/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import (
"net"
"net/http"
"os"
"runtime"
"strconv"
"time"

regname "github.com/google/go-containerregistry/pkg/name"
gocontainerregistry "github.com/google/go-containerregistry/pkg/registry"
"github.com/pkg/errors"

"github.com/vmware-tanzu/tanzu-cli/pkg/cli"
"github.com/vmware-tanzu/tanzu-cli/pkg/configpaths"
"github.com/vmware-tanzu/tanzu-cli/pkg/constants"
configlib "github.com/vmware-tanzu/tanzu-plugin-runtime/config"
Expand All @@ -38,7 +38,7 @@ func GetRegistryCertOptions(registryHost string) (*CertOptions, error) {
Insecure: false,
}

if runtime.GOOS == "windows" {
if cli.GOOS == "windows" {
err := AddRegistryTrustedRootCertsFileForWindows(registryCertOpts)
if err != nil {
return nil, err
Expand Down
6 changes: 3 additions & 3 deletions pkg/telemetry/sqlite_metrics_db.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import (
"database/sql"
"os"
"path/filepath"
"runtime"
"strconv"

// Import the sqlite3 driver
_ "modernc.org/sqlite"

"github.com/pkg/errors"

"github.com/vmware-tanzu/tanzu-cli/pkg/cli"
"github.com/vmware-tanzu/tanzu-cli/pkg/common"
)

Expand Down Expand Up @@ -110,8 +110,8 @@ func (b *sqliteMetricsDB) SaveOperationMetric(entry *OperationMetricsPayload) er

row := cliOperationsRow{
cliVersion: entry.CliVersion,
osName: runtime.GOOS,
osArch: runtime.GOARCH,
osName: cli.GOOS,
osArch: cli.GOARCH,
pluginName: entry.PluginName,
pluginVersion: entry.PluginVersion,
command: entry.CommandName,
Expand Down
6 changes: 3 additions & 3 deletions pkg/telemetry/sqlite_metrics_db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import (
"database/sql"
"os"
"path/filepath"
"runtime"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/vmware-tanzu/tanzu-cli/pkg/cli"

"github.com/pkg/errors"
)
Expand Down Expand Up @@ -65,8 +65,8 @@ var _ = Describe("Inserting CLI metrics to database and verifying it by fetching
Expect(len(metricsRows)).To(Equal(1))
Expect(metricsRows[0].cliID).To(Equal("fake-cli-cliID"))
Expect(metricsRows[0].cliVersion).To(Equal("v1.0.0"))
Expect(metricsRows[0].osName).To(Equal(runtime.GOOS))
Expect(metricsRows[0].osArch).To(Equal(runtime.GOARCH))
Expect(metricsRows[0].osName).To(Equal(cli.GOOS))
Expect(metricsRows[0].osArch).To(Equal(cli.GOARCH))
Expect(metricsRows[0].nameArg).To(Equal("fake-name-arg"))
Expect(metricsRows[0].command).To(Equal("fake-cmd-name"))
Expect(metricsRows[0].commandStartTSMsec).ToNot(BeEmpty())
Expand Down

0 comments on commit e1d776d

Please sign in to comment.