Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid uselessly trying to migrate plugins #832

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

marckhouzam
Copy link
Contributor

@marckhouzam marckhouzam commented Nov 28, 2024

What this PR does / why we need it

The only time we need to migrate context-scope plugins is after moving from a CLI < 1.3 to a newer one. This PR makes use of the existing global initializer to do this migration once, for all contexts.

This avoids an unnecessary slow down for every CLI commands, especially shell completion which should be as fast as possible. We can see an improvement of 30% to 40% in speed for basic shell completion commands.

Note:

  1. before this PR, context-scoped plugins would be migrated only for the active context; this means that when another context becomes active, it may also need to have its plugins migrated
  2. also before this PR, context-scope plugins that have been migrated will then always be shown as standalone plugins, even if their context is made inactive
  3. with this PR, since we only run the plugin migration once, we must migrate the context-scoped plugins for all contexts immediately. This is a slight change in behaviour because context-scoped plugins for an inactive (and possibly old context) will now appear as standalone plugins right away for the new CLI. Since this new behaviour is similar to point 2 above, I feel it is acceptable

Which issue(s) this PR fixes

Fixes N/A

Describe testing done for PR

Test with a CLI 1.2 and a CLI using this PR. We install context-scope plugins and make sure they are migrated as expected.

$ tanzu1.2 version
version: v1.2.0
buildDate: 2024-02-07
sha: f3abe62e
arch: arm64

$ tz version
version: v1.6.0-dev
buildDate: 2024-11-28
sha: 05e593e30
arch: arm64

# Install the context-scope plugins
$ tanzu1.2 context use tkg1
[i] Successfully activated context 'tkg1' of type 'kubernetes'.
[i] Checking for required plugins for context 'tkg1'...
[i] The following plugins will be installed for context 'tkg1' of contextType 'kubernetes':
  NAME                TARGET      VERSION
  cluster             kubernetes  v0.31.1
  feature             kubernetes  v0.31.1
  kubernetes-release  kubernetes  v0.31.1
[i] Installed plugin 'cluster:v0.31.1' with target 'kubernetes' (from cache)
[i] Installed plugin 'feature:v0.31.1' with target 'kubernetes' (from cache)
[i] Installed plugin 'kubernetes-release:v0.31.1' with target 'kubernetes' (from cache)
[i] Successfully installed all required plugins

# Confirm they are in the Catalog under the old serverPlugins section
$ grep -A4 serverPlugin ~/.cache/tanzu/catalog.yaml
serverPlugins:
    tkg1:
        cluster_kubernetes: /Users/kmarc/Library/Application Support/tanzu-cli/cluster/v0.31.1_9d63b407fc78ad86f00342ae920589b3e0eba4282a01b547e0cee9188f338a77_kubernetes
        feature_kubernetes: /Users/kmarc/Library/Application Support/tanzu-cli/feature/v0.31.1_8c38d6db1c0cd2cd0b894cd46bbd4ffdb89e3b21df496f7df943aa57188780c7_kubernetes
        kubernetes-release_kubernetes: /Users/kmarc/Library/Application Support/tanzu-cli/kubernetes-release/v0.31.1_b4c395c7b469c048968c57314ae901e84cbd6606222da227a50a84a8e2a8aa69_kubernetes

# Run a command of the newer CLI to see the global initializer run
# Notice also that the cluster, feature, kubernetes-release plugins are shown in the list
# with the recommended column as expected; this shows they have been migrated
$ tz plugin list
Some initialization of the CLI is required.
Let's set things up for you.  This will just take a few seconds.

Refreshing the 27 installed plugins...

Initialization done!
==
  NAME                  DESCRIPTION                                                                       TARGET      INSTALLED           RECOMMENDED  STATUS
  apply                 Apply a resource file                                                             operations  v0.6.5                           installed
  apps                  Applications on Kubernetes                                                        kubernetes  v0.13.2                          installed
  appsv2                Applications on Tanzu Platform                                                    global      v0.21.2-dirty                    installed
  build                 Generate app image from source code                                               global      v0.16.0                          installed
  builder               Build Tanzu components                                                            global      v1.5.0                           installed
  cluster               Kubernetes cluster operations                                                     kubernetes  v0.31.1             v0.31.1      installed
  cluster               Plugin for operating clusters in the organization                                 operations  v0.11.6                          installed
  clustergroup          A group of Kubernetes clusters                                                    operations  v0.7.5                           installed
  domain                tanzu-cli domain plugin                                                           global      v0.1.0                           installed
  domain-binding        tanzu-cli domain binding plugin                                                   global      v0.1.4                           installed
  egress                Provides crud operations for egresspoints                                         global      v0.0.5                           installed
  ekscluster            ekscluster plugin is used to provision and manage the tmc eks clusters.           operations  v0.6.5                           installed
  feature               Operate on features and featuregates                                              kubernetes  v0.31.1             v0.31.1      installed
  kubectl-plugin        Provides the kubectl functionality                                                global      v0.0.0                           installed
  kubernetes-release    Kubernetes release operations                                                     kubernetes  v0.31.1             v0.31.1      installed
  kubetool              Invokes any binary after setting KUBECONFIG to match the tanzu CLI                global      v0.0.0                           installed
  management-cluster    A registered Management cluster                                                   operations  v0.6.5                           installed
  package               tanzu package management                                                          kubernetes  v0.35.8                          installed
  policy                Policy management for resources                                                   operations  v0.6.5                           installed
  project               Create, view, list, use and delete Tanzu projects.                                global      v0.0.0-dev-7741e5e               installed
  provider-eks-cluster  provider-eks-cluster plugin is used to manage and unmanage tmc provider eks       operations  v0.6.5                           installed
                        clusters
  rbac                  Tanzu Platform Role-Based Access Control                                          global      v0.0.0-dev-cde9be5               installed
  resource              manage resources in a Kubernetes cluster                                          global      v0.5.1                           installed
  secret                Tanzu secret management                                                           kubernetes  v0.33.5                          installed
  services              Commands for working with services, classes and claims                            kubernetes  v0.14.0                          installed
  space                 Tanzu space lifecycle management                                                  global      v0.4.0                           installed
  telemetry             configure cluster-wide settings for vmware tanzu telemetry                        global      v1.1.0                           installed

# Confirm the old serverPlugin section is gone
$ grep -A4 serverPlugin ~/.cache/tanzu/catalog.yaml
$

# Recreate the old serverPlugins section by reinstalling the context-scope
# plugins with the old CLI
$ tanzu1.2 context use tkg1
[i] Successfully activated context 'tkg1' of type 'kubernetes'.
[i] Checking for required plugins for context 'tkg1'...
[i] The following plugins will be installed for context 'tkg1' of contextType 'kubernetes':
  NAME                TARGET      VERSION
  cluster             kubernetes  v0.31.1
  feature             kubernetes  v0.31.1
  kubernetes-release  kubernetes  v0.31.1
[i] Installed plugin 'cluster:v0.31.1' with target 'kubernetes' (from cache)
[i] Installed plugin 'feature:v0.31.1' with target 'kubernetes' (from cache)
[i] Installed plugin 'kubernetes-release:v0.31.1' with target 'kubernetes' (from cache)
[i] Successfully installed all required plugins
$
$ grep -A4 serverPlugin ~/.cache/tanzu/catalog.yaml
serverPlugins:
    tkg1:
        cluster_kubernetes: /Users/kmarc/Library/Application Support/tanzu-cli/cluster/v0.31.1_9d63b407fc78ad86f00342ae920589b3e0eba4282a01b547e0cee9188f338a77_kubernetes
        feature_kubernetes: /Users/kmarc/Library/Application Support/tanzu-cli/feature/v0.31.1_8c38d6db1c0cd2cd0b894cd46bbd4ffdb89e3b21df496f7df943aa57188780c7_kubernetes
        kubernetes-release_kubernetes: /Users/kmarc/Library/Application Support/tanzu-cli/kubernetes-release/v0.31.1_b4c395c7b469c048968c57314ae901e84cbd6606222da227a50a84a8e2a8aa69_kubernetes

# Now uninstall the 3 context-scope plugins to get an empty serverPlugins section
$ tanzu1.2 plugin uninstall cluster -t k8s
Uninstalling plugin 'cluster' for target 'kubernetes'. Are you sure? [y/N]: y
[i] Uninstalling plugin 'cluster' for target 'kubernetes'
[i] Uninstalling plugin 'cluster' for target 'kubernetes'
[ok] successfully uninstalled plugin 'cluster'
$ tanzu1.2 plugin uninstall kubernetes-release -y -t k8s
[i] Uninstalling plugin 'kubernetes-release' for target 'kubernetes'
[i] Uninstalling plugin 'kubernetes-release' for target 'kubernetes'
[ok] successfully uninstalled plugin 'kubernetes-release'
$ tanzu1.2 plugin uninstall feature -y -t k8s
[i] Uninstalling plugin 'feature' for target 'kubernetes'
[i] Uninstalling plugin 'feature' for target 'kubernetes'
[ok] successfully uninstalled plugin 'feature'
$
$ grep -A4 serverPlugin ~/.cache/tanzu/catalog.yaml
serverPlugins:
    tkg1: {}

# Use the new CLI and notice the cluster, feature, kubernetes-release plugins
# are listed as uninstalled as expected
$ tz plugin list
Some initialization of the CLI is required.
Let's set things up for you.  This will just take a few seconds.

Refreshing the 24 installed plugins...

Initialization done!
==
  NAME                  DESCRIPTION                                                                       TARGET      INSTALLED           RECOMMENDED  STATUS
  apply                 Apply a resource file                                                             operations  v0.6.5                           installed
  apps                  Applications on Kubernetes                                                        kubernetes  v0.13.2                          installed
  appsv2                Applications on Tanzu Platform                                                    global      v0.21.2-dirty                    installed
  build                 Generate app image from source code                                               global      v0.16.0                          installed
  builder               Build Tanzu components                                                            global      v1.5.0                           installed
  cluster               Kubernetes cluster operations                                                     kubernetes                      v0.31.1      not installed
  cluster               Plugin for operating clusters in the organization                                 operations  v0.11.6                          installed
  clustergroup          A group of Kubernetes clusters                                                    operations  v0.7.5                           installed
  domain                tanzu-cli domain plugin                                                           global      v0.1.0                           installed
  domain-binding        tanzu-cli domain binding plugin                                                   global      v0.1.4                           installed
  egress                Provides crud operations for egresspoints                                         global      v0.0.5                           installed
  ekscluster            ekscluster plugin is used to provision and manage the tmc eks clusters.           operations  v0.6.5                           installed
  feature               Operate on features and featuregates                                              kubernetes                      v0.31.1      not installed
  kubectl-plugin        Provides the kubectl functionality                                                global      v0.0.0                           installed
  kubernetes-release    Kubernetes release operations                                                     kubernetes                      v0.31.1      not installed
  kubetool              Invokes any binary after setting KUBECONFIG to match the tanzu CLI                global      v0.0.0                           installed
  management-cluster    A registered Management cluster                                                   operations  v0.6.5                           installed
  package               tanzu package management                                                          kubernetes  v0.35.8                          installed
  policy                Policy management for resources                                                   operations  v0.6.5                           installed
  project               Create, view, list, use and delete Tanzu projects.                                global      v0.0.0-dev-7741e5e               installed
  provider-eks-cluster  provider-eks-cluster plugin is used to manage and unmanage tmc provider eks       operations  v0.6.5                           installed
                        clusters
  rbac                  Tanzu Platform Role-Based Access Control                                          global      v0.0.0-dev-cde9be5               installed
  resource              manage resources in a Kubernetes cluster                                          global      v0.5.1                           installed
  secret                Tanzu secret management                                                           kubernetes  v0.33.5                          installed
  services              Commands for working with services, classes and claims                            kubernetes  v0.14.0                          installed
  space                 Tanzu space lifecycle management                                                  global      v0.4.0                           installed
  telemetry             configure cluster-wide settings for vmware tanzu telemetry                        global      v1.1.0                           installed

Note: As shown above, some recommended plugins have not been installed or are outdated. To install them please run 'tanzu plugin sync'.

# Confirm the serverPlugin section is properly removed
$ grep -A4 serverPlugin ~/.cache/tanzu/catalog.yaml
$

# Install the context-scope plugins again but using plugin sync with the old CLI
$ tanzu plugin sync
[i] Plugin sync will be performed for context: 'tkg1'
[i] Checking for required plugins for context 'tkg1'...
[i] The following plugins will be installed for context 'tkg1' of contextType 'kubernetes':
  NAME                TARGET      VERSION
  cluster             kubernetes  v0.31.1
  feature             kubernetes  v0.31.1
  kubernetes-release  kubernetes  v0.31.1
[i] Installed plugin 'cluster:v0.31.1' with target 'kubernetes' (from cache)
[i] Installed plugin 'feature:v0.31.1' with target 'kubernetes' (from cache)
[i] Installed plugin 'kubernetes-release:v0.31.1' with target 'kubernetes' (from cache)
[i] Successfully installed all required plugins
[ok] Done

# Now change context so that the tkg context which specifies the
# context-scope plugins is no longer active
$ tanzu context use Tanzu_Platform_for_K8s_SaaS_-_Internal_1
[i] Successfully activated context 'Tanzu_Platform_for_K8s_SaaS_-_Internal_1' of type 'tanzu' (Project: bagreda).
[i] Checking for required plugins for context 'Tanzu_Platform_for_K8s_SaaS_-_Internal_1'...
[i] All required plugins are already installed and up-to-date

# Run the new CLI and notice that the cluster, feature, kubernetes-release plugins
# are properly migrated even though their context was not active.
# This is important because the migration will not be run again.
$ tz plugin list
Some initialization of the CLI is required.
Let's set things up for you.  This will just take a few seconds.

Refreshing the 27 installed plugins...

Initialization done!
==
  NAME                  DESCRIPTION                                                                       TARGET      INSTALLED           STATUS
  apply                 Apply a resource file                                                             operations  v0.6.5              installed
  apps                  Applications on Kubernetes                                                        kubernetes  v0.13.2             installed
  appsv2                Applications on Tanzu Platform                                                    global      v0.21.2-dirty       installed
  build                 Generate app image from source code                                               global      v0.16.0             installed
  builder               Build Tanzu components                                                            global      v1.5.0              installed
  cluster               Kubernetes cluster operations                                                     kubernetes  v0.31.1             installed
  cluster               Plugin for operating clusters in the organization                                 operations  v0.11.6             installed
  clustergroup          A group of Kubernetes clusters                                                    operations  v0.7.5              installed
  domain                tanzu-cli domain plugin                                                           global      v0.1.0              installed
  domain-binding        tanzu-cli domain binding plugin                                                   global      v0.1.4              installed
  egress                Provides crud operations for egresspoints                                         global      v0.0.5              installed
  ekscluster            ekscluster plugin is used to provision and manage the tmc eks clusters.           operations  v0.6.5              installed
  feature               Operate on features and featuregates                                              kubernetes  v0.31.1             installed
  kubectl-plugin        Provides the kubectl functionality                                                global      v0.0.0              installed
  kubernetes-release    Kubernetes release operations                                                     kubernetes  v0.31.1             installed
  kubetool              Invokes any binary after setting KUBECONFIG to match the tanzu CLI                global      v0.0.0              installed
  management-cluster    A registered Management cluster                                                   operations  v0.6.5              installed
  package               tanzu package management                                                          kubernetes  v0.35.8             installed
  policy                Policy management for resources                                                   operations  v0.6.5              installed
  project               Create, view, list, use and delete Tanzu projects.                                global      v0.0.0-dev-7741e5e  installed
  provider-eks-cluster  provider-eks-cluster plugin is used to manage and unmanage tmc provider eks       operations  v0.6.5              installed
                        clusters
  rbac                  Tanzu Platform Role-Based Access Control                                          global      v0.0.0-dev-cde9be5  installed
  resource              manage resources in a Kubernetes cluster                                          global      v0.5.1              installed
  secret                Tanzu secret management                                                           kubernetes  v0.33.5             installed
  services              Commands for working with services, classes and claims                            kubernetes  v0.14.0             installed
  space                 Tanzu space lifecycle management                                                  global      v0.4.0              installed
  telemetry             configure cluster-wide settings for vmware tanzu telemetry                        global      v1.1.0              installed
$ grep -A4 serverPlugin ~/.cache/tanzu/catalog.yaml

Now, confirm that CLI is faster.
Before the PR we see a speed normally just above 0.20 seconds.

$ time tz version>/dev/null
/Users/kmarc/bin/tanzu version > /dev/null  0.24s user 0.03s system 137% cpu 0.199 total
$ time tz version>/dev/null
/Users/kmarc/bin/tanzu version > /dev/null  0.26s user 0.03s system 136% cpu 0.214 total
$ time tz version>/dev/null
/Users/kmarc/bin/tanzu version > /dev/null  0.25s user 0.03s system 141% cpu 0.195 total
$ time tz version>/dev/null
/Users/kmarc/bin/tanzu version > /dev/null  0.26s user 0.03s system 130% cpu 0.227 total
$ time tz version>/dev/null
/Users/kmarc/bin/tanzu version > /dev/null  0.26s user 0.03s system 140% cpu 0.208 total
$ time tz version>/dev/null
/Users/kmarc/bin/tanzu version > /dev/null  0.24s user 0.02s system 137% cpu 0.189 total
$ time tz version>/dev/null
/Users/kmarc/bin/tanzu version > /dev/null  0.25s user 0.03s system 130% cpu 0.211 total

With the PR we see a time closer to 0.14 which is a 30% improvement

$ time tz version>/dev/null
/Users/kmarc/bin/tanzu version > /dev/null  0.12s user 0.02s system 116% cpu 0.123 total
$ time tz version>/dev/null
/Users/kmarc/bin/tanzu version > /dev/null  0.12s user 0.02s system 77% cpu 0.185 total
$ time tz version>/dev/null
/Users/kmarc/bin/tanzu version > /dev/null  0.12s user 0.02s system 115% cpu 0.120 total
$ time tz version>/dev/null
/Users/kmarc/bin/tanzu version > /dev/null  0.12s user 0.02s system 101% cpu 0.141 total
$ time tz version>/dev/null
/Users/kmarc/bin/tanzu version > /dev/null  0.13s user 0.02s system 111% cpu 0.133 total
$ time tz version>/dev/null
/Users/kmarc/bin/tanzu version > /dev/null  0.12s user 0.02s system 108% cpu 0.134 total
$ time tz version>/dev/null
/Users/kmarc/bin/tanzu version > /dev/null  0.12s user 0.02s system 120% cpu 0.121 total

Release note

Speed up CLI by avoiding unnecessary writing to the Catalog of plugins

Additional information

Special notes for your reviewers

On my M1 machine I see these speed results:

I repeatedly run time tz __complete '' > /dev/null to test shell completion responsiveness.

With 1 context:
v1.2.0 -> ~0.18 secs
v1.3.0 -> ~0.18 secs
v1.4.1 -> ~0.16 secs
v1.5.1 -> ~0.17 secs
main (92c98e8) -> ~0.17 secs
with this PR -> ~0.10 secs

So we see a significant improvement. It makes basic shell completion blazingly fast.

@marckhouzam marckhouzam requested a review from a team as a code owner November 28, 2024 03:11
@marckhouzam marckhouzam force-pushed the marck/fastStartupCatalog branch from 05e593e to 4317f5d Compare November 28, 2024 04:07
@@ -141,222 +130,6 @@ var _ = Describe("GetInstalledPlugins (standalone plugins)", func() {
Expect(isInstalled).To(BeFalse())
})
})

Context("when a standalone and server plugin for k8s target installed", func() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the MigrateContextPluginsAsStandaloneIfNeeded() function is no longer called by pluginsupplier.GetInstalledPlugins(), these tests were no longer applicable.

I however moved them to a new cleanup_test.go file to directly test MigrateContextPluginsAsStandaloneIfNeeded()

The only time we need to migrate context-scope plugins is after moving
from a CLI < 1.3 to a newer one.  This commit makes use of the existing
global initializer to do this migration once, for all contexts.

This avoids an unnecessary slow down for every CLI commands, especially
shell completion which should be as fast as possible.

Signed-off-by: Marc Khouzam <[email protected]>
@marckhouzam marckhouzam force-pushed the marck/fastStartupCatalog branch from 4317f5d to 92fa8ed Compare November 28, 2024 12:14
@marckhouzam marckhouzam requested a review from anujc25 November 28, 2024 14:40
@vuil
Copy link
Contributor

vuil commented Dec 3, 2024

Thanks for the clear explanation. Looks like a good bit of optimization.

with this PR, since we only run the plugin migration once, we must migrate the context-scoped plugins for all contexts immediately. This is a slight change in behaviour because context-scoped plugins for an inactive (and possibly old context) will now appear as standalone plugins right away for the new CLI. Since this new behaviour is similar to point 2 above, I feel it is acceptable

On point 3, I am not sure whether there was some nuance behind the way it was done before this change.
Interested to hear @anujc25's take on this.

Copy link
Contributor

@anujc25 anujc25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good. But need clarification on one comment that I added.

Comment on lines 53 to 68
func MigrateContextPluginsAsStandaloneIfNeeded() {
activeContexts, err := configlib.GetAllActiveContextsList()
if err != nil || len(activeContexts) == 0 {
return
}

c, lockedFile, err := getCatalogCache(true)
if err != nil {
return
}
defer lockedFile.Close()

for _, ac := range activeContexts {
for pluginKey, installPath := range c.ServerPlugins[ac] {
for _, association := range c.ServerPlugins {
for pluginKey, installPath := range association {
c.StandAlonePlugins.Add(pluginKey, installPath)
}
delete(c.ServerPlugins, ac)
}
// Delete all entries by reassigning to a new empty map
c.ServerPlugins = make(map[string]PluginAssociation)

_ = saveCatalogCache(c, lockedFile)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I understand is with this change now we are converting all the ServerPlugins as StandalonePlugins instead of just doing that for the ActiveContexts.
Is this change intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. See point 3 in the PR description:

with this PR, since we only run the plugin migration once, we must migrate the context-scoped plugins for all contexts immediately. This is a slight change in behaviour because context-scoped plugins for an inactive (and possibly old context) will now appear as standalone plugins right away for the new CLI. Since this new behaviour is similar to point 2 above, I feel it is acceptable

Originally, I started by keeping the original solution of checking this migration at every command, but not blindly updating the catalog if there was nothing to migrate. However this still has a cost compared to running the migration only once, since we don't have to check the catalog for every command, just in case.

But I wanted your opinion if there may be side-effects I missed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of just doing that for the ActiveContexts

To be very clear, just in case, with the previous approach, when the active context would change, we would migrate the plugins for the new active context. So, assuming every context eventually became active, then all ServerPlugins would eventually be migrated as StandalonePlugins.

With this PR we do it all at once and be done with it.

Do your recall if "only doing it for the active contexts" was a kind of optimization? Maybe because it was run for every command?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do your recall if "only doing it for the active contexts" was a kind of optimization? Maybe because it was run for every command?

It was not for optimization but to reliably do the migration as different contexts can recommend different versions of a same plugin. So when we migrate the context-scoped plugins for all available contexts at once we can loose the data as the last migrated plugin version will win. I think we need to be careful and think this through.

@anujc25
Copy link
Contributor

anujc25 commented Dec 4, 2024

On point 3, I am not sure whether there was some nuance behind the way it was done before this change. Interested to hear @anujc25's take on this.

Yes. This seems like the correct thing to do. I added a TODO to update the implementation this way because the globalinitializer approach was not available when I implemented this context-scoped plugin to standalone plugin migration. See the existing TODO below.

// Migrate context-scoped plugins as standalone plugin if required
// TODO(anujc): Think on how to invoke this function just once after the newer version
// of the CLI gets installed as we just need to do this migration once

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants