Preinstall: Replace auto update feature flag with a permanent one (#113586)
This commit is contained in:
committed by
GitHub
parent
9a542489a7
commit
1a2beae38a
@@ -1934,6 +1934,9 @@ preinstall_disabled = false
|
||||
# Update strategy for plugins.
|
||||
# Available options: "latest", "minor"
|
||||
update_strategy = minor
|
||||
# Enable automatic updates for preinstalled plugins on startup.
|
||||
# When enabled, preinstalled plugins without a pinned version will be updated to the latest version.
|
||||
preinstall_auto_update = true
|
||||
|
||||
#################################### Grafana Live ##########################################
|
||||
[live]
|
||||
|
||||
@@ -2622,6 +2622,15 @@ These will be installed before starting Grafana. Useful when used with provision
|
||||
|
||||
This option disables all preinstalled plugins. The default is `false`. To disable a specific plugin from being preinstalled, use the `disable_plugins` option.
|
||||
|
||||
#### `preinstall_auto_update`
|
||||
|
||||
Enable automatic updates for preinstalled plugins on start-up.
|
||||
When enabled, preinstalled plugins without a pinned version are automatically updated to the latest version when Grafana starts.
|
||||
|
||||
The default is `true`.
|
||||
|
||||
To prevent automatic updates for specific plugins, pin them to a specific version using the format `plugin_id@version` in the `preinstall` setting.
|
||||
|
||||
<hr>
|
||||
|
||||
### `[live]`
|
||||
|
||||
@@ -66,7 +66,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
|
||||
| `useSessionStorageForRedirection` | Use session storage for handling the redirection after login | Yes |
|
||||
| `pluginsSriChecks` | Enables SRI checks for plugin assets | |
|
||||
| `azureMonitorDisableLogLimit` | Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. | |
|
||||
| `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins | Yes |
|
||||
| `alertingUIOptimizeReducer` | Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query | Yes |
|
||||
| `azureMonitorEnableUserAuth` | Enables user auth for Azure Monitor datasource only | Yes |
|
||||
| `alertingNotificationsStepMode` | Enables simplified step mode in the notifications section | Yes |
|
||||
|
||||
@@ -311,6 +311,7 @@ export interface GrafanaConfig {
|
||||
pluginCatalogHiddenPlugins: string[];
|
||||
pluginCatalogManagedPlugins: string[];
|
||||
pluginCatalogPreinstalledPlugins: PreinstalledPlugin[];
|
||||
pluginCatalogPreinstalledAutoUpdate?: boolean;
|
||||
pluginsCDNBaseURL: string;
|
||||
tokenExpirationDayLimit: number;
|
||||
listDashboardScopesEndpoint: string;
|
||||
|
||||
@@ -733,11 +733,6 @@ export interface FeatureToggles {
|
||||
*/
|
||||
azureMonitorDisableLogLimit?: boolean;
|
||||
/**
|
||||
* Enables automatic updates for pre-installed plugins
|
||||
* @default true
|
||||
*/
|
||||
preinstallAutoUpdate?: boolean;
|
||||
/**
|
||||
* Enables experimental reconciler for playlists
|
||||
*/
|
||||
playlistsReconciler?: boolean;
|
||||
|
||||
@@ -169,6 +169,7 @@ export class GrafanaBootConfig {
|
||||
pluginCatalogHiddenPlugins: string[] = [];
|
||||
pluginCatalogManagedPlugins: string[] = [];
|
||||
pluginCatalogPreinstalledPlugins: PreinstalledPluginGrafanaData[] = [];
|
||||
pluginCatalogPreinstalledAutoUpdate?: boolean;
|
||||
pluginsCDNBaseURL = '';
|
||||
expressionsEnabled = false;
|
||||
awsAllowedAuthProviders: string[] = [];
|
||||
|
||||
@@ -235,29 +235,30 @@ type FrontendSettingsDTO struct {
|
||||
|
||||
LicenseInfo FrontendSettingsLicenseInfoDTO `json:"licenseInfo"`
|
||||
|
||||
FeatureToggles map[string]bool `json:"featureToggles"`
|
||||
AnonymousEnabled bool `json:"anonymousEnabled"`
|
||||
AnonymousDeviceLimit int64 `json:"anonymousDeviceLimit"`
|
||||
RendererAvailable bool `json:"rendererAvailable"`
|
||||
RendererVersion string `json:"rendererVersion"`
|
||||
RendererDefaultImageWidth int `json:"rendererDefaultImageWidth"`
|
||||
RendererDefaultImageHeight int `json:"rendererDefaultImageHeight"`
|
||||
RendererDefaultImageScale float64 `json:"rendererDefaultImageScale"`
|
||||
Http2Enabled bool `json:"http2Enabled"`
|
||||
GrafanaJavascriptAgent setting.GrafanaJavascriptAgent `json:"grafanaJavascriptAgent"`
|
||||
PluginCatalogURL string `json:"pluginCatalogURL"`
|
||||
PluginAdminEnabled bool `json:"pluginAdminEnabled"`
|
||||
PluginAdminExternalManageEnabled bool `json:"pluginAdminExternalManageEnabled"`
|
||||
PluginCatalogHiddenPlugins []string `json:"pluginCatalogHiddenPlugins"`
|
||||
PluginCatalogManagedPlugins []string `json:"pluginCatalogManagedPlugins"`
|
||||
PluginCatalogPreinstalledPlugins []setting.InstallPlugin `json:"pluginCatalogPreinstalledPlugins"`
|
||||
ExpressionsEnabled bool `json:"expressionsEnabled"`
|
||||
AwsAllowedAuthProviders []string `json:"awsAllowedAuthProviders"`
|
||||
AwsAssumeRoleEnabled bool `json:"awsAssumeRoleEnabled"`
|
||||
SupportBundlesEnabled bool `json:"supportBundlesEnabled"`
|
||||
SnapshotEnabled bool `json:"snapshotEnabled"`
|
||||
SecureSocksDSProxyEnabled bool `json:"secureSocksDSProxyEnabled"`
|
||||
ReportingStaticContext map[string]string `json:"reportingStaticContext"`
|
||||
FeatureToggles map[string]bool `json:"featureToggles"`
|
||||
AnonymousEnabled bool `json:"anonymousEnabled"`
|
||||
AnonymousDeviceLimit int64 `json:"anonymousDeviceLimit"`
|
||||
RendererAvailable bool `json:"rendererAvailable"`
|
||||
RendererVersion string `json:"rendererVersion"`
|
||||
RendererDefaultImageWidth int `json:"rendererDefaultImageWidth"`
|
||||
RendererDefaultImageHeight int `json:"rendererDefaultImageHeight"`
|
||||
RendererDefaultImageScale float64 `json:"rendererDefaultImageScale"`
|
||||
Http2Enabled bool `json:"http2Enabled"`
|
||||
GrafanaJavascriptAgent setting.GrafanaJavascriptAgent `json:"grafanaJavascriptAgent"`
|
||||
PluginCatalogURL string `json:"pluginCatalogURL"`
|
||||
PluginAdminEnabled bool `json:"pluginAdminEnabled"`
|
||||
PluginAdminExternalManageEnabled bool `json:"pluginAdminExternalManageEnabled"`
|
||||
PluginCatalogHiddenPlugins []string `json:"pluginCatalogHiddenPlugins"`
|
||||
PluginCatalogManagedPlugins []string `json:"pluginCatalogManagedPlugins"`
|
||||
PluginCatalogPreinstalledPlugins []setting.InstallPlugin `json:"pluginCatalogPreinstalledPlugins"`
|
||||
PluginCatalogPreinstalledAutoUpdate bool `json:"pluginCatalogPreinstalledAutoUpdate"`
|
||||
ExpressionsEnabled bool `json:"expressionsEnabled"`
|
||||
AwsAllowedAuthProviders []string `json:"awsAllowedAuthProviders"`
|
||||
AwsAssumeRoleEnabled bool `json:"awsAssumeRoleEnabled"`
|
||||
SupportBundlesEnabled bool `json:"supportBundlesEnabled"`
|
||||
SnapshotEnabled bool `json:"snapshotEnabled"`
|
||||
SecureSocksDSProxyEnabled bool `json:"secureSocksDSProxyEnabled"`
|
||||
ReportingStaticContext map[string]string `json:"reportingStaticContext"`
|
||||
|
||||
Azure FrontendSettingsAzureDTO `json:"azure"`
|
||||
|
||||
|
||||
+21
-20
@@ -290,26 +290,27 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
EnabledFeatures: hs.License.EnabledFeatures(),
|
||||
},
|
||||
|
||||
FeatureToggles: featureToggles,
|
||||
AnonymousEnabled: hs.Cfg.Anonymous.Enabled,
|
||||
AnonymousDeviceLimit: hs.Cfg.Anonymous.DeviceLimit,
|
||||
RendererAvailable: hs.RenderService.IsAvailable(c.Req.Context()),
|
||||
RendererVersion: hs.RenderService.Version(),
|
||||
RendererDefaultImageWidth: hs.Cfg.RendererDefaultImageWidth,
|
||||
RendererDefaultImageHeight: hs.Cfg.RendererDefaultImageHeight,
|
||||
RendererDefaultImageScale: hs.Cfg.RendererDefaultImageScale,
|
||||
Http2Enabled: hs.Cfg.Protocol == setting.HTTP2Scheme,
|
||||
GrafanaJavascriptAgent: hs.Cfg.GrafanaJavascriptAgent,
|
||||
PluginCatalogURL: hs.Cfg.PluginCatalogURL,
|
||||
PluginAdminEnabled: hs.Cfg.PluginAdminEnabled,
|
||||
PluginAdminExternalManageEnabled: hs.Cfg.PluginAdminEnabled && hs.Cfg.PluginAdminExternalManageEnabled,
|
||||
PluginCatalogHiddenPlugins: hs.Cfg.PluginCatalogHiddenPlugins,
|
||||
PluginCatalogManagedPlugins: hs.managedPluginsService.ManagedPlugins(c.Req.Context()),
|
||||
PluginCatalogPreinstalledPlugins: append(hs.Cfg.PreinstallPluginsAsync, hs.Cfg.PreinstallPluginsSync...),
|
||||
ExpressionsEnabled: hs.Cfg.ExpressionsEnabled,
|
||||
AwsAllowedAuthProviders: hs.Cfg.AWSAllowedAuthProviders,
|
||||
AwsAssumeRoleEnabled: hs.Cfg.AWSAssumeRoleEnabled,
|
||||
SupportBundlesEnabled: isSupportBundlesEnabled(hs),
|
||||
FeatureToggles: featureToggles,
|
||||
AnonymousEnabled: hs.Cfg.Anonymous.Enabled,
|
||||
AnonymousDeviceLimit: hs.Cfg.Anonymous.DeviceLimit,
|
||||
RendererAvailable: hs.RenderService.IsAvailable(c.Req.Context()),
|
||||
RendererVersion: hs.RenderService.Version(),
|
||||
RendererDefaultImageWidth: hs.Cfg.RendererDefaultImageWidth,
|
||||
RendererDefaultImageHeight: hs.Cfg.RendererDefaultImageHeight,
|
||||
RendererDefaultImageScale: hs.Cfg.RendererDefaultImageScale,
|
||||
Http2Enabled: hs.Cfg.Protocol == setting.HTTP2Scheme,
|
||||
GrafanaJavascriptAgent: hs.Cfg.GrafanaJavascriptAgent,
|
||||
PluginCatalogURL: hs.Cfg.PluginCatalogURL,
|
||||
PluginAdminEnabled: hs.Cfg.PluginAdminEnabled,
|
||||
PluginAdminExternalManageEnabled: hs.Cfg.PluginAdminEnabled && hs.Cfg.PluginAdminExternalManageEnabled,
|
||||
PluginCatalogHiddenPlugins: hs.Cfg.PluginCatalogHiddenPlugins,
|
||||
PluginCatalogManagedPlugins: hs.managedPluginsService.ManagedPlugins(c.Req.Context()),
|
||||
PluginCatalogPreinstalledPlugins: append(hs.Cfg.PreinstallPluginsAsync, hs.Cfg.PreinstallPluginsSync...),
|
||||
PluginCatalogPreinstalledAutoUpdate: hs.Cfg.PreinstallAutoUpdate,
|
||||
ExpressionsEnabled: hs.Cfg.ExpressionsEnabled,
|
||||
AwsAllowedAuthProviders: hs.Cfg.AWSAllowedAuthProviders,
|
||||
AwsAssumeRoleEnabled: hs.Cfg.AWSAssumeRoleEnabled,
|
||||
SupportBundlesEnabled: isSupportBundlesEnabled(hs),
|
||||
|
||||
Azure: dtos.FrontendSettingsAzureDTO{
|
||||
Cloud: hs.Cfg.Azure.Cloud,
|
||||
|
||||
Generated
+2
-2
@@ -817,7 +817,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plugininstallerService, err := plugininstaller.ProvideService(cfg, pluginstoreService, pluginInstaller, registerer, repoManager, featureToggles, plugincheckerService)
|
||||
plugininstallerService, err := plugininstaller.ProvideService(cfg, pluginstoreService, pluginInstaller, registerer, repoManager, plugincheckerService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1455,7 +1455,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plugininstallerService, err := plugininstaller.ProvideService(cfg, pluginstoreService, pluginInstaller, registerer, repoManager, featureToggles, plugincheckerService)
|
||||
plugininstallerService, err := plugininstaller.ProvideService(cfg, pluginstoreService, pluginInstaller, registerer, repoManager, plugincheckerService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1261,13 +1261,6 @@ var (
|
||||
Owner: grafanaPartnerPluginsSquad,
|
||||
Expression: "false",
|
||||
},
|
||||
{
|
||||
Name: "preinstallAutoUpdate",
|
||||
Description: "Enables automatic updates for pre-installed plugins",
|
||||
Stage: FeatureStageGeneralAvailability,
|
||||
Owner: grafanaPluginsPlatformSquad,
|
||||
Expression: "true", // enabled by default
|
||||
},
|
||||
{
|
||||
Name: "playlistsReconciler",
|
||||
Description: "Enables experimental reconciler for playlists",
|
||||
|
||||
@@ -165,7 +165,6 @@ unifiedStorageBigObjectsSupport,experimental,@grafana/search-and-storage,false,f
|
||||
timeRangeProvider,experimental,@grafana/grafana-frontend-platform,false,false,false
|
||||
timeRangePan,experimental,@grafana/dataviz-squad,false,false,true
|
||||
azureMonitorDisableLogLimit,GA,@grafana/partner-datasources,false,false,false
|
||||
preinstallAutoUpdate,GA,@grafana/plugins-platform-backend,false,false,false
|
||||
playlistsReconciler,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
passwordlessMagicLinkAuthentication,experimental,@grafana/identity-access-team,false,false,false
|
||||
exploreMetricsRelatedLogs,experimental,@grafana/observability-metrics,false,false,true
|
||||
|
||||
|
Generated
-4
@@ -671,10 +671,6 @@ const (
|
||||
// Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default.
|
||||
FlagAzureMonitorDisableLogLimit = "azureMonitorDisableLogLimit"
|
||||
|
||||
// FlagPreinstallAutoUpdate
|
||||
// Enables automatic updates for pre-installed plugins
|
||||
FlagPreinstallAutoUpdate = "preinstallAutoUpdate"
|
||||
|
||||
// FlagPlaylistsReconciler
|
||||
// Enables experimental reconciler for playlists
|
||||
FlagPlaylistsReconciler = "playlistsReconciler"
|
||||
|
||||
@@ -3158,7 +3158,8 @@
|
||||
"metadata": {
|
||||
"name": "preinstallAutoUpdate",
|
||||
"resourceVersion": "1753448760331",
|
||||
"creationTimestamp": "2024-11-07T12:14:25Z"
|
||||
"creationTimestamp": "2024-11-07T12:14:25Z",
|
||||
"deletionTimestamp": "2025-11-07T10:10:01Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables automatic updates for pre-installed plugins",
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/repo"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -45,7 +44,6 @@ type Service struct {
|
||||
pluginInstaller plugins.Installer
|
||||
pluginStore pluginstore.Store
|
||||
pluginRepo repo.Service
|
||||
features featuremgmt.FeatureToggles
|
||||
updateChecker pluginchecker.PluginUpdateChecker
|
||||
installComplete chan struct{} // closed when all plugins are installed (used for testing)
|
||||
}
|
||||
@@ -56,7 +54,6 @@ func ProvideService(
|
||||
pluginInstaller plugins.Installer,
|
||||
promReg prometheus.Registerer,
|
||||
pluginRepo repo.Service,
|
||||
features featuremgmt.FeatureToggles,
|
||||
updateChecker pluginchecker.PluginUpdateChecker,
|
||||
) (*Service, error) {
|
||||
once.Do(func() {
|
||||
@@ -70,7 +67,6 @@ func ProvideService(
|
||||
pluginInstaller: pluginInstaller,
|
||||
pluginStore: pluginStore,
|
||||
pluginRepo: pluginRepo,
|
||||
features: features,
|
||||
updateChecker: updateChecker,
|
||||
installComplete: make(chan struct{}),
|
||||
}
|
||||
@@ -111,8 +107,8 @@ func (s *Service) installPlugins(ctx context.Context, pluginsToInstall []setting
|
||||
continue
|
||||
}
|
||||
if installPlugin.Version == "" {
|
||||
if !s.features.IsEnabled(ctx, featuremgmt.FlagPreinstallAutoUpdate) {
|
||||
// Skip updating the plugin if the feature flag is disabled
|
||||
if !s.cfg.PreinstallAutoUpdate {
|
||||
// Skip updating the plugin if auto-update is disabled
|
||||
continue
|
||||
}
|
||||
// The plugin is installed but it's not pinned to a specific version
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/plugins/repo"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/installsync/installsyncfakes"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
|
||||
@@ -31,7 +30,6 @@ func TestService_IsDisabled(t *testing.T) {
|
||||
&pluginfakes.FakePluginInstaller{},
|
||||
prometheus.NewRegistry(),
|
||||
&pluginfakes.FakePluginRepo{},
|
||||
featuremgmt.WithFeatures(),
|
||||
&pluginchecker.FakePluginUpdateChecker{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -167,6 +165,7 @@ func TestService_Run(t *testing.T) {
|
||||
&setting.Cfg{
|
||||
PreinstallPluginsAsync: tt.pluginsToInstall,
|
||||
PreinstallPluginsSync: tt.pluginsToInstallSync,
|
||||
PreinstallAutoUpdate: true,
|
||||
},
|
||||
store,
|
||||
&pluginfakes.FakePluginInstaller{
|
||||
@@ -199,7 +198,6 @@ func TestService_Run(t *testing.T) {
|
||||
return tt.latestPlugin, nil
|
||||
},
|
||||
},
|
||||
featuremgmt.WithFeatures(featuremgmt.FlagPreinstallAutoUpdate),
|
||||
pluginchecker.ProvideService(
|
||||
managedplugins.NewNoop(),
|
||||
provisionedplugins.NewNoop(),
|
||||
|
||||
@@ -215,6 +215,7 @@ type Cfg struct {
|
||||
ForwardHostEnvVars []string
|
||||
PreinstallPluginsAsync []InstallPlugin
|
||||
PreinstallPluginsSync []InstallPlugin
|
||||
PreinstallAutoUpdate bool
|
||||
|
||||
PluginsCDNURLTemplate string
|
||||
PluginLogBackendRequests bool
|
||||
|
||||
@@ -185,6 +185,8 @@ func (cfg *Cfg) readPluginSettings(iniFile *ini.File) error {
|
||||
}
|
||||
cfg.PreinstallPluginsAsync = nil
|
||||
}
|
||||
|
||||
cfg.PreinstallAutoUpdate = pluginsSection.Key("preinstall_auto_update").MustBool(true)
|
||||
}
|
||||
|
||||
cfg.PluginCatalogURL = pluginsSection.Key("plugin_catalog_url").MustString("https://grafana.com/grafana/plugins/")
|
||||
|
||||
@@ -15,6 +15,7 @@ describe('VersionInstallButton', () => {
|
||||
...originalConfig.featureToggles,
|
||||
};
|
||||
config.pluginCatalogPreinstalledPlugins = originalConfig.pluginCatalogPreinstalledPlugins;
|
||||
config.pluginCatalogPreinstalledAutoUpdate = originalConfig.pluginCatalogPreinstalledAutoUpdate;
|
||||
});
|
||||
it('should show install when no version is installed', () => {
|
||||
const version: Version = {
|
||||
@@ -120,7 +121,7 @@ describe('VersionInstallButton', () => {
|
||||
grafanaDependency: null,
|
||||
};
|
||||
const installedVersion = '1.0.0';
|
||||
config.featureToggles.preinstallAutoUpdate = true;
|
||||
config.pluginCatalogPreinstalledAutoUpdate = true;
|
||||
config.pluginCatalogPreinstalledPlugins = [{ id: 'test', version: '1.0.0' }];
|
||||
renderWithStore(
|
||||
<VersionInstallButton
|
||||
@@ -142,7 +143,7 @@ describe('VersionInstallButton', () => {
|
||||
grafanaDependency: null,
|
||||
};
|
||||
const installedVersion = '1.0.1';
|
||||
config.featureToggles.preinstallAutoUpdate = true;
|
||||
config.pluginCatalogPreinstalledAutoUpdate = true;
|
||||
config.pluginCatalogPreinstalledPlugins = [{ id: 'test', version: '1.0.1' }];
|
||||
renderWithStore(
|
||||
<VersionInstallButton
|
||||
@@ -164,7 +165,7 @@ describe('VersionInstallButton', () => {
|
||||
grafanaDependency: null,
|
||||
};
|
||||
const installedVersion = '1.0.1';
|
||||
config.featureToggles.preinstallAutoUpdate = true;
|
||||
config.pluginCatalogPreinstalledAutoUpdate = true;
|
||||
config.pluginCatalogPreinstalledPlugins = [{ id: 'test', version: '' }];
|
||||
renderWithStore(
|
||||
<VersionInstallButton
|
||||
|
||||
@@ -174,7 +174,7 @@ function getButtonHiddenState(installState: PluginStatus, isPreinstalled: { foun
|
||||
|
||||
// Handle downgrade case
|
||||
if (installState === PluginStatus.DOWNGRADE) {
|
||||
return isPreinstalled.found && Boolean(config.featureToggles.preinstallAutoUpdate);
|
||||
return isPreinstalled.found && Boolean(config.pluginCatalogPreinstalledAutoUpdate);
|
||||
}
|
||||
|
||||
// Handle upgrade case
|
||||
|
||||
Reference in New Issue
Block a user