Skip to content

Commit

Permalink
Setup global initializer for missing central config file (vmware-tanz…
Browse files Browse the repository at this point in the history
…u#723)

The concept of global initializers is added to allow the CLI to prepare
its data on certain conditions (triggers).

The commit also uses one such initializer to fix a plugin cache that is
missing the central_config.yaml file.
  • Loading branch information
marckhouzam authored and vuil committed Apr 19, 2024
1 parent 1f579b3 commit 28a3418
Show file tree
Hide file tree
Showing 20 changed files with 791 additions and 28 deletions.
7 changes: 7 additions & 0 deletions docs/dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ that is valid yaml. To read the Central Configuration the `CentralConfig` inter
err = reader.GetCentralConfigEntry("myStringKey", &myValue)
```

## Global Initializers

The CLI has a concept of global initializers accessible from the `globalinit` package. Such initializers can
be used by CLI features to initialize/update certain data required for the proper functioning of the CLI itself.
Such initializers are often useful after an older CLI version was executed and a new CLI version now needs to
properly setup its data.

## Deprecation of existing functionality

Any changes aimed to remove functionality in the CLI (e.g. commands, command
Expand Down
8 changes: 8 additions & 0 deletions docs/full/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,11 @@ variable to `0` will turn off such notifications.

Note that special consideration must be given for this feature to work in an internet-restricted environment.
Please refer to [this section](../quickstart/install.md#updating-the-central-configuration) of the documentation.

## Initialization upon the execution of a new version

When a new version of the Tanzu CLI is executed for the first time it may need to be globally initialized.
If needed, this allows the new CLI to update things like its cache or configuration depending on the needs of
features being introduced in the new version. This initialization is normally only done once for a new
version of the CLI, however, if a user goes back and forth between versions of the CLI (which can affect the
format of the CLI data), the initialization could be performed more than once, as required.
6 changes: 2 additions & 4 deletions pkg/centralconfig/central_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ import (
"path/filepath"

"github.com/vmware-tanzu/tanzu-cli/pkg/common"
"github.com/vmware-tanzu/tanzu-cli/pkg/constants"
"github.com/vmware-tanzu/tanzu-plugin-runtime/config/types"
)

// CentralConfigFileName is the name of the central config file
const CentralConfigFileName = "central_config.yaml"

// CentralConfig is used to interact with the central configuration.
type CentralConfig interface {
// GetCentralConfigEntry reads the central configuration and
Expand All @@ -28,7 +26,7 @@ type CentralConfig interface {
// be used to read central configuration values.
func NewCentralConfigReader(pd *types.PluginDiscovery) CentralConfig {
// The central config is stored in the cache
centralConfigFile := filepath.Join(common.DefaultCacheDir, common.PluginInventoryDirName, pd.OCI.Name, CentralConfigFileName)
centralConfigFile := filepath.Join(common.DefaultCacheDir, common.PluginInventoryDirName, pd.OCI.Name, constants.CentralConfigFileName)

return &centralConfigYamlReader{configFile: centralConfigFile}
}
79 changes: 79 additions & 0 deletions pkg/centralconfig/central_config_cache_init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2024 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package centralconfig

import (
"io"
"os"
"path/filepath"

kerrors "k8s.io/apimachinery/pkg/util/errors"

"github.com/vmware-tanzu/tanzu-cli/pkg/common"
"github.com/vmware-tanzu/tanzu-cli/pkg/constants"
"github.com/vmware-tanzu/tanzu-cli/pkg/discovery"
"github.com/vmware-tanzu/tanzu-cli/pkg/globalinit"
"github.com/vmware-tanzu/tanzu-plugin-runtime/config"
)

// The Central Configuration feature uses a central_config.yaml file that gets downloaded
// along with the plugin inventory cache and is stored in the cache. Older versions of the
// CLI (< 1.3.0) do not include this file in the cache. It is therefore possible that
// the plugin inventory cache was setup by an older version of the CLI and is missing
// the central_config.yaml file. In such a case, the digest of the cache will still indicate
// that the latest plugin inventory is present and the content of the cache will not be refreshed
// until the plugin inventory data changes in the central repo itself. In such a case, to be able
// to benefit from the central configuration feature once the CLI is upgraded to >= 1.3.0
// we need to fix the cache. This initializer checks if the central_config.yaml file is
// present in the cache and if not, it invalidates the cache.

func init() {
globalinit.RegisterInitializer("Central Config Initializer", triggerForInventoryCacheInvalidation, invalidateInventoryCache)
}

// triggerForInventoryCacheInvalidation returns true if the central_config.yaml file is missing
// in the plugin inventory cache.
func triggerForInventoryCacheInvalidation() bool {
sources, err := config.GetCLIDiscoverySources()
if err != nil {
// No discovery source
return false
}

for _, source := range sources {
centralConfigFile := filepath.Join(common.DefaultCacheDir, common.PluginInventoryDirName, source.OCI.Name, constants.CentralConfigFileName)

if _, err := os.Stat(centralConfigFile); os.IsNotExist(err) {
// As soon as we find a source that doesn't have a central_config.yaml file,
// we need to perform some initialization.
return true
}
}
// If we get here, then all sources have a central_config.yaml file
return false
}

// invalidateInventoryCache performs the required actions
func invalidateInventoryCache(_ io.Writer) error {
sources, err := config.GetCLIDiscoverySources()
if err != nil {
// No discovery source
return nil
}

var errorList []error
for _, source := range sources {
centralConfigFile := filepath.Join(common.DefaultCacheDir, common.PluginInventoryDirName, source.OCI.Name, constants.CentralConfigFileName)

if _, err := os.Stat(centralConfigFile); os.IsNotExist(err) {
// This source doesn't have a central_config.yaml file,
// we need to invalidate its plugin inventory cache.
err = discovery.RefreshDiscoveryDatabaseForSource(source, discovery.WithForceInvalidation())
if err != nil {
errorList = append(errorList, err)
}
}
}
return kerrors.NewAggregate(errorList)
}
114 changes: 114 additions & 0 deletions pkg/centralconfig/central_config_cache_init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright 2024 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package centralconfig

import (
"fmt"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"

"github.com/vmware-tanzu/tanzu-cli/pkg/common"
"github.com/vmware-tanzu/tanzu-cli/pkg/constants"
"github.com/vmware-tanzu/tanzu-plugin-runtime/config"
"github.com/vmware-tanzu/tanzu-plugin-runtime/config/types"
)

func TestTriggerForInventoryCacheInvalidation(t *testing.T) {
tcs := []struct {
name string
numDiscoveries int
missingCentralConfig []bool
expectedToTrigger bool
}{
{
name: "No discovery sources",
numDiscoveries: 0,
expectedToTrigger: false,
},
{
name: "One discovery source with central config",
numDiscoveries: 1,
missingCentralConfig: []bool{false},
expectedToTrigger: false,
},
{
name: "One discovery source with missing central config",
numDiscoveries: 1,
missingCentralConfig: []bool{true},
expectedToTrigger: true,
},
{
name: "Two discovery sources both with central config",
numDiscoveries: 2,
missingCentralConfig: []bool{false, false},
expectedToTrigger: false,
},
{
name: "Two discovery sources with only one missing central config",
numDiscoveries: 2,
missingCentralConfig: []bool{false, true},
expectedToTrigger: true,
},
}

configFile, err := os.CreateTemp("", "config")
assert.Nil(t, err)
os.Setenv("TANZU_CONFIG", configFile.Name())

configFileNG, err := os.CreateTemp("", "config_ng")
assert.Nil(t, err)
os.Setenv("TANZU_CONFIG_NEXT_GEN", configFileNG.Name())

defer func() {
os.Unsetenv("TANZU_CONFIG")
os.Unsetenv("TANZU_CONFIG_NEXT_GEN")
os.RemoveAll(configFile.Name())
os.RemoveAll(configFileNG.Name())
}()

for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
// Create a cache directory for the plugin inventory and the central config file
cacheDir, err := os.MkdirTemp("", "test-cache-dir")
assert.Nil(t, err)

common.DefaultCacheDir = cacheDir

// Create the discovery sources
var discoveries []types.PluginDiscovery
for i := 0; i < tc.numDiscoveries; i++ {
discName := fmt.Sprintf("discovery%d", i)
discoveries = append(discoveries, types.PluginDiscovery{
OCI: &types.OCIDiscovery{
Name: discName,
},
})

// Create the directory for this discovery source
centralCfgDir := filepath.Join(common.DefaultCacheDir, common.PluginInventoryDirName, discName)
err = os.MkdirAll(centralCfgDir, 0755)
assert.Nil(t, err)

// Create the central config file if needed by the test
if !tc.missingCentralConfig[i] {
centralCfgFile := filepath.Join(centralCfgDir, constants.CentralConfigFileName)
file, err := os.Create(centralCfgFile)
assert.Nil(t, err)
assert.NotNil(t, file)
file.Close()
}
}

err = config.SetCLIDiscoverySources(discoveries)
assert.Nil(t, err)

assert.Equal(t, triggerForInventoryCacheInvalidation(), tc.expectedToTrigger)

os.RemoveAll(cacheDir)
})
}
}
3 changes: 2 additions & 1 deletion pkg/centralconfig/central_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"

"github.com/vmware-tanzu/tanzu-cli/pkg/common"
"github.com/vmware-tanzu/tanzu-cli/pkg/constants"
"github.com/vmware-tanzu/tanzu-plugin-runtime/config/types"
)

Expand All @@ -24,7 +25,7 @@ func TestNewCentralConfigReader(t *testing.T) {
})

path := reader.(*centralConfigYamlReader).configFile
expectedPath := filepath.Join(common.DefaultCacheDir, common.PluginInventoryDirName, discoveryName, CentralConfigFileName)
expectedPath := filepath.Join(common.DefaultCacheDir, common.PluginInventoryDirName, discoveryName, constants.CentralConfigFileName)

assert.Equal(t, expectedPath, path)
}
7 changes: 1 addition & 6 deletions pkg/command/discovery_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,7 @@ func checkDiscoverySource(source configtypes.PluginDiscovery) error {
// the WithForceRefresh() option to ensure we refresh the DB no matter if the TTL has expired or not.
// This provides a way for the user to force a refresh of the DB by running "tanzu plugin source init/update"
// without waiting for the TTL to expire.
discObject, err := discovery.CreateDiscoveryFromV1alpha1(source, discovery.WithForceRefresh())
if err != nil {
return err
}
_, err = discObject.List()
return err
return discovery.RefreshDiscoveryDatabaseForSource(source, discovery.WithForceRefresh())
}

// ====================================
Expand Down
7 changes: 7 additions & 0 deletions pkg/command/plugin_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/vmware-tanzu/tanzu-cli/pkg/cli"
"github.com/vmware-tanzu/tanzu-cli/pkg/common"
"github.com/vmware-tanzu/tanzu-cli/pkg/config"
"github.com/vmware-tanzu/tanzu-cli/pkg/constants"
"github.com/vmware-tanzu/tanzu-cli/pkg/plugininventory"
)

Expand Down Expand Up @@ -234,6 +235,12 @@ func setupTestPluginInventory(t *testing.T) {
// Add plugin group entries to the DB
_, err = db.Exec(createGroupsStmt)
assert.Nil(t, err)

// Create an empty central_config.yaml file to avoid
// triggering the global initializer that would invalidate the cache
// and add extra printouts to the test output
_, err = os.Create(filepath.Join(inventoryDir, constants.CentralConfigFileName))
assert.Nil(t, err)
}

func setupTestPluginCatalog(t *testing.T) {
Expand Down
40 changes: 40 additions & 0 deletions pkg/command/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
cliconfig "github.com/vmware-tanzu/tanzu-cli/pkg/config"
"github.com/vmware-tanzu/tanzu-cli/pkg/constants"
"github.com/vmware-tanzu/tanzu-cli/pkg/discovery"
"github.com/vmware-tanzu/tanzu-cli/pkg/globalinit"
"github.com/vmware-tanzu/tanzu-cli/pkg/pluginmanager"
"github.com/vmware-tanzu/tanzu-cli/pkg/pluginsupplier"
"github.com/vmware-tanzu/tanzu-cli/pkg/recommendedversion"
Expand Down Expand Up @@ -265,6 +266,13 @@ func newRootCmd() *cobra.Command {
// Sets the verbosity of the logger if TANZU_CLI_LOG_LEVEL is set
setLoggerVerbosity()

// Perform some global initialization of the CLI if necessary
// We do this as early as possible to make sure the CLI is ready for use
// for any other logic below.
if !shouldSkipGlobalInit(cmd) {
checkGlobalInit(cmd)
}

// Ensure mutual exclusion in current contexts just in case if any plugins with old
// plugin-runtime sets k8s context as current when tanzu context is already set as current
if err := utils.EnsureMutualExclusiveCurrentContexts(); err != nil {
Expand Down Expand Up @@ -328,6 +336,24 @@ func setLoggerVerbosity() {
}
}

func checkGlobalInit(cmd *cobra.Command) {
if globalinit.InitializationRequired() {
outStream := cmd.OutOrStderr()

fmt.Fprintf(outStream, "Some initialization of the CLI is required.\n")
fmt.Fprintf(outStream, "Let's set things up for you. This will just take a few seconds.\n\n")

err := globalinit.PerformInitializations(outStream)
if err != nil {
log.Warningf("The initialization encountered the following error: %v", err)
}

fmt.Fprintln(outStream)
fmt.Fprintln(outStream, "Initialization done!")
fmt.Fprintln(outStream, "==")
}
}

func installEssentialPlugins() {
_ = discovery.RefreshDatabase()

Expand Down Expand Up @@ -581,6 +607,20 @@ func shouldSkipVersionCheck(cmd *cobra.Command) bool {
return isSkipCommand(skipVersionCheckCommands, cmd.CommandPath())
}

// shouldSkipGlobalInit checks if the initialization of a new CLI version should be skipped
// for the specified command
func shouldSkipGlobalInit(cmd *cobra.Command) bool {
skipGlobalInitCommands := []string{
// The shell completion logic is not interactive, so it should not trigger
// the global initialization of the CLI
"tanzu __complete",
"tanzu completion",
// Common first command to run, let's not perform extra tasks
"tanzu version",
}
return isSkipCommand(skipGlobalInitCommands, cmd.CommandPath())
}

// Execute executes the CLI.
func Execute() error {
root, err := NewRootCmd()
Expand Down
Loading

0 comments on commit 28a3418

Please sign in to comment.