From 93275988bedaf89fa42a74964c3726a5439b27e9 Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Tue, 19 Sep 2023 16:06:19 -0400 Subject: [PATCH] Fallback to installing AMD64 plugins on darwin 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 --- pkg/cli/arch.go | 10 +++- pkg/pluginmanager/manager.go | 38 ++++++++----- pkg/pluginmanager/manager_helper_test.go | 44 ++++++++++++--- pkg/pluginmanager/manager_test.go | 69 +++++++++++++++++++++++- pkg/registry/helpers.go | 4 +- pkg/telemetry/sqlite_metrics_db.go | 6 +-- pkg/telemetry/sqlite_metrics_db_test.go | 7 +-- 7 files changed, 148 insertions(+), 30 deletions(-) diff --git a/pkg/cli/arch.go b/pkg/cli/arch.go index 2a59f2461..f1d0aa4a2 100644 --- a/pkg/cli/arch.go +++ b/pkg/cli/arch.go @@ -15,6 +15,9 @@ 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. @@ -22,7 +25,12 @@ 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. diff --git a/pkg/pluginmanager/manager.go b/pkg/pluginmanager/manager.go index 13028cea0..e717bd17a 100644 --- a/pkg/pluginmanager/manager.go +++ b/pkg/pluginmanager/manager.go @@ -11,7 +11,6 @@ import ( "os" "os/exec" "path/filepath" - "runtime" "strings" "github.com/pkg/errors" @@ -486,8 +485,8 @@ 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)) @@ -495,6 +494,21 @@ 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() + 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' matching version '%v' for target '%s'", pluginName, version, string(target))) @@ -689,7 +703,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 } @@ -721,13 +735,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 } @@ -782,7 +796,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) @@ -1149,8 +1163,8 @@ func getCLIPluginResourceWithLocalDistroFromPluginInfo(plugin *cli.PluginInfo, p { URI: pluginBinaryPath, Type: common.DistributionTypeLocal, - OS: runtime.GOOS, - Arch: runtime.GOARCH, + OS: cli.GOOS, + Arch: cli.GOARCH, }, }, }, @@ -1213,7 +1227,7 @@ func discoverPluginsFromLocalSourceBasedOnManifestFile(localPath string) ([]disc // Sample path: cli/[target]///tanzu--_ // 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 } @@ -1272,7 +1286,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 } @@ -1282,7 +1296,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 diff --git a/pkg/pluginmanager/manager_helper_test.go b/pkg/pluginmanager/manager_helper_test.go index 7ea329858..219a2c198 100644 --- a/pkg/pluginmanager/manager_helper_test.go +++ b/pkg/pluginmanager/manager_helper_test.go @@ -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"}, @@ -47,6 +48,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', @@ -115,6 +120,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 { @@ -185,20 +194,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) } } diff --git a/pkg/pluginmanager/manager_test.go b/pkg/pluginmanager/manager_test.go index 381acfa34..9a6880cba 100644 --- a/pkg/pluginmanager/manager_test.go +++ b/pkg/pluginmanager/manager_test.go @@ -130,6 +130,24 @@ 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, + }, + { + Name: "pluginwitharm", + Description: "Plugin pluginwitharm description", + RecommendedVersion: "v2.0.0", + SupportedVersions: []string{"v2.0.0"}, + Scope: common.PluginScopeStandalone, + ContextName: "", + Target: configtypes.TargetK8s, + }, } var expectedDiscoveredGroups = []string{"vmware-test/default:v1.6.0", "vmware-test/default:v2.1.0"} @@ -266,10 +284,37 @@ func Test_InstallStandalonePlugin(t *testing.T) { assertions.NotNil(err) assertions.Contains(err.Error(), "unable to find plugin 'feature' matching 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)) @@ -299,12 +344,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) + } } } diff --git a/pkg/registry/helpers.go b/pkg/registry/helpers.go index 6e7cde272..db70bbaa2 100644 --- a/pkg/registry/helpers.go +++ b/pkg/registry/helpers.go @@ -11,7 +11,6 @@ import ( "net" "net/http" "os" - "runtime" "strconv" "time" @@ -19,6 +18,7 @@ import ( 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" @@ -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 diff --git a/pkg/telemetry/sqlite_metrics_db.go b/pkg/telemetry/sqlite_metrics_db.go index 849e584ee..503c4a521 100644 --- a/pkg/telemetry/sqlite_metrics_db.go +++ b/pkg/telemetry/sqlite_metrics_db.go @@ -7,7 +7,6 @@ import ( "database/sql" "os" "path/filepath" - "runtime" "strconv" // Import the sqlite3 driver @@ -15,6 +14,7 @@ import ( "github.com/pkg/errors" + "github.com/vmware-tanzu/tanzu-cli/pkg/cli" "github.com/vmware-tanzu/tanzu-cli/pkg/common" ) @@ -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, diff --git a/pkg/telemetry/sqlite_metrics_db_test.go b/pkg/telemetry/sqlite_metrics_db_test.go index 73dd9c30f..c9e6e4ec3 100644 --- a/pkg/telemetry/sqlite_metrics_db_test.go +++ b/pkg/telemetry/sqlite_metrics_db_test.go @@ -7,13 +7,14 @@ import ( "database/sql" "os" "path/filepath" - "runtime" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/pkg/errors" + + "github.com/vmware-tanzu/tanzu-cli/pkg/cli" ) var _ = Describe("Inserting CLI metrics to database and verifying it by fetching the metrics from database", func() { @@ -65,8 +66,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())