diff --git a/frontend/src/Settings/General/GeneralSettings.js b/frontend/src/Settings/General/GeneralSettings.js index e13e0f534..66ad32375 100644 --- a/frontend/src/Settings/General/GeneralSettings.js +++ b/frontend/src/Settings/General/GeneralSettings.js @@ -98,6 +98,7 @@ class GeneralSettings extends Component { isResettingApiKey, isMono, isWindows, + isDocker, mode, onInputChange, onConfirmResetApiKey, @@ -161,6 +162,7 @@ class GeneralSettings extends Component { advancedSettings={advancedSettings} settings={settings} isMono={isMono} + isDocker={isDocker} onInputChange={onInputChange} /> @@ -202,6 +204,7 @@ GeneralSettings.propTypes = { hasSettings: PropTypes.bool.isRequired, isMono: PropTypes.bool.isRequired, isWindows: PropTypes.bool.isRequired, + isDocker: PropTypes.bool.isRequired, mode: PropTypes.string.isRequired, onInputChange: PropTypes.func.isRequired, onConfirmResetApiKey: PropTypes.func.isRequired, diff --git a/frontend/src/Settings/General/GeneralSettingsConnector.js b/frontend/src/Settings/General/GeneralSettingsConnector.js index f63e70a3b..73eb63fd5 100644 --- a/frontend/src/Settings/General/GeneralSettingsConnector.js +++ b/frontend/src/Settings/General/GeneralSettingsConnector.js @@ -26,6 +26,7 @@ function createMapStateToProps() { isResettingApiKey, isMono: systemStatus.isMono, isWindows: systemStatus.isWindows, + isDocker: systemStatus.isDocker, mode: systemStatus.mode, ...sectionSettings }; diff --git a/frontend/src/Settings/General/UpdateSettings.js b/frontend/src/Settings/General/UpdateSettings.js index 71ddb0c4a..d41f95f35 100644 --- a/frontend/src/Settings/General/UpdateSettings.js +++ b/frontend/src/Settings/General/UpdateSettings.js @@ -16,6 +16,7 @@ function UpdateSettings(props) { advancedSettings, settings, isMono, + isDocker, onInputChange } = props; @@ -35,6 +36,14 @@ function UpdateSettings(props) { { key: 'script', value: 'Script' } ]; + if (isDocker) { + return ( +
+
Updating is disabled inside a docker container. Update the container image instead.
+
+ ); + } + return (
} + { + isDocker && + + } + - - Install Latest - - - { - isFetching && - - } - +
+ { + !isDocker && + + Install Latest + + } + + { + isDocker && +
+ An update is available. Please update your Docker image and re-create the container. +
+ } + + { + isFetching && + + } +
} { @@ -162,6 +173,7 @@ Updates.propTypes = { error: PropTypes.object, items: PropTypes.array.isRequired, isInstallingUpdate: PropTypes.bool.isRequired, + isDocker: PropTypes.bool.isRequired, shortDateFormat: PropTypes.string.isRequired, onInstallLatestPress: PropTypes.func.isRequired }; diff --git a/frontend/src/System/Updates/UpdatesConnector.js b/frontend/src/System/Updates/UpdatesConnector.js index 0d0aa491f..638a9790a 100644 --- a/frontend/src/System/Updates/UpdatesConnector.js +++ b/frontend/src/System/Updates/UpdatesConnector.js @@ -6,6 +6,7 @@ import { fetchUpdates } from 'Store/Actions/systemActions'; import { executeCommand } from 'Store/Actions/commandActions'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; import * as commandNames from 'Commands/commandNames'; import Updates from './Updates'; @@ -14,7 +15,8 @@ function createMapStateToProps() { (state) => state.system.updates, createUISettingsSelector(), createCommandExecutingSelector(commandNames.APPLICATION_UPDATE), - (updates, uiSettings, isInstallingUpdate) => { + createSystemStatusSelector(), + (updates, uiSettings, isInstallingUpdate, systemStatus) => { const { isFetching, isPopulated, @@ -28,6 +30,7 @@ function createMapStateToProps() { error, items, isInstallingUpdate, + isDocker: systemStatus.isDocker, shortDateFormat: uiSettings.shortDateFormat }; } diff --git a/src/Lidarr.Api.V1/System/SystemModule.cs b/src/Lidarr.Api.V1/System/SystemModule.cs index f274da77c..0c6ad0268 100644 --- a/src/Lidarr.Api.V1/System/SystemModule.cs +++ b/src/Lidarr.Api.V1/System/SystemModule.cs @@ -64,6 +64,7 @@ namespace Lidarr.Api.V1.System IsLinux = OsInfo.IsLinux, IsOsx = OsInfo.IsOsx, IsWindows = OsInfo.IsWindows, + IsDocker = _osInfo.IsDocker, Mode = _runtimeInfo.Mode, Branch = _configFileProvider.Branch, Authentication = _configFileProvider.AuthenticationMethod, diff --git a/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs index 5b26c1fb7..def6de02c 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs @@ -14,7 +14,9 @@ namespace NzbDrone.Common.EnvironmentInfo public static bool IsLinux => Os == Os.Linux; public static bool IsOsx => Os == Os.Osx; public static bool IsWindows => Os == Os.Windows; - public static bool IsDocker { get; } + + // this needs to not be static so we can mock it + public bool IsDocker { get; } public string Version { get; } public string Name { get; } @@ -45,10 +47,6 @@ namespace NzbDrone.Common.EnvironmentInfo else { Os = Os.Linux; - if (File.Exists("/proc/1/cgroup") && File.ReadAllText("/proc/1/cgroup").Contains("/docker/")) - { - IsDocker = true; - } } break; } @@ -88,8 +86,14 @@ namespace NzbDrone.Common.EnvironmentInfo FullName = Name; } + if (IsLinux && File.Exists("/proc/1/cgroup") && File.ReadAllText("/proc/1/cgroup").Contains("/docker/")) + { + IsDocker = true; + } + Environment.SetEnvironmentVariable("OS_NAME", Name); Environment.SetEnvironmentVariable("OS_VERSION", Version); + Environment.SetEnvironmentVariable("OS_IS_DOCKER", IsDocker.ToString()); } } @@ -98,6 +102,8 @@ namespace NzbDrone.Common.EnvironmentInfo string Version { get; } string Name { get; } string FullName { get; } + + bool IsDocker { get; } } public enum Os diff --git a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs index c6d861d4f..622473441 100644 --- a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs +++ b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs @@ -260,10 +260,12 @@ namespace NzbDrone.Common.Instrumentation.Sentry // populated these values yet var osName = Environment.GetEnvironmentVariable("OS_NAME"); var osVersion = Environment.GetEnvironmentVariable("OS_VERSION"); + var isDocker = Environment.GetEnvironmentVariable("OS_IS_DOCKER"); var runTimeVersion = Environment.GetEnvironmentVariable("RUNTIME_VERSION"); sentryEvent.SetTag("os_name", osName); sentryEvent.SetTag("os_version", $"{osName} {osVersion}"); + sentryEvent.SetTag("is_docker", isDocker); sentryEvent.SetTag("runtime_version", $"{PlatformInfo.PlatformName} {runTimeVersion}"); sentryEvent.SetTag("sqlite_version", $"{DatabaseVersion}"); sentryEvent.SetTag("database_migration", $"{DatabaseMigration}"); diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/RemotePathMappingCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/RemotePathMappingCheckFixture.cs index f59ff9538..d69f7a37d 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/RemotePathMappingCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/RemotePathMappingCheckFixture.cs @@ -5,6 +5,7 @@ using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients; using NzbDrone.Core.HealthCheck.Checks; @@ -86,7 +87,9 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks private void GivenDocker() { - // + Mocker.GetMock() + .Setup(x => x.IsDocker) + .Returns(true); } [Test] @@ -139,9 +142,10 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks } [Test] - [Explicit("Only works if running inside a docker container")] public void should_return_docker_path_mapping_error_if_on_docker_and_root_missing() { + GivenDocker(); + Subject.Check().ShouldBeError(wikiFragment: "docker-bad-remote-path-mapping"); } @@ -215,9 +219,10 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks } [Test] - [Explicit("Only works if running inside a docker container")] public void should_return_docker_mapping_error_on_track_import_failed_event_inside_docker_if_folder_does_not_exist() { + GivenDocker(); + clientStatus.IsLocalhost = false; var importEvent = new TrackImportFailedEvent(null, null, true, downloadItem); diff --git a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs index 2afc12fbf..29c60b8b3 100644 --- a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs +++ b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs @@ -87,6 +87,16 @@ namespace NzbDrone.Core.Test.UpdateTests .Returns(true); } + [Test] + public void should_not_update_if_inside_docker() + { + Mocker.GetMock().Setup(x => x.IsDocker).Returns(true); + + Subject.Invoking(x => x.Execute(new ApplicationUpdateCommand())) + .ShouldThrow() + .WithMessage("Updating is disabled inside a docker container. Please update the container image."); + } + [Test] public void should_delete_sandbox_before_update_if_folder_exists() { diff --git a/src/NzbDrone.Core/HealthCheck/Checks/RemotePathMappingCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/RemotePathMappingCheck.cs index 7a3782d59..be9a16e68 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/RemotePathMappingCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/RemotePathMappingCheck.cs @@ -58,7 +58,7 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, $"Remote download client {client.Definition.Name} places downloads in {folder.FullPath} but this is not a valid {_osInfo.Name} path. Review your remote path mappings and download client settings.", "#bad-remote-path-mapping"); } - else if (OsInfo.IsDocker) + else if (_osInfo.IsDocker) { return new HealthCheck(GetType(), HealthCheckResult.Error, $"You are using docker; download client {client.Definition.Name} places downloads in {folder.FullPath} but this is not a valid {_osInfo.Name} path. Review your remote path mappings and download client settings.", "#docker-bad-remote-path-mapping"); } @@ -70,7 +70,7 @@ namespace NzbDrone.Core.HealthCheck.Checks if (!_diskProvider.FolderExists(folder.FullPath)) { - if (OsInfo.IsDocker) + if (_osInfo.IsDocker) { return new HealthCheck(GetType(), HealthCheckResult.Error, $"You are using docker; download client {client.Definition.Name} places downloads in {folder.FullPath} but this directory does not appear to exist inside the container. Review your remote path mappings and container volume settings.", "#docker-bad-remote-path-mapping"); } @@ -141,7 +141,7 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, $"Remote download client {client.Definition.Name} reported files in {dlpath} but this is not a valid {_osInfo.Name} path. Review your remote path mappings and download client settings.", "#bad-remote-path-mapping"); } - else if (OsInfo.IsDocker) + else if (_osInfo.IsDocker) { return new HealthCheck(GetType(), HealthCheckResult.Error, $"You are using docker; download client {client.Definition.Name} reported files in {dlpath} but this is not a valid {_osInfo.Name} path. Review your remote path mappings and download client settings.", "#docker-bad-remote-path-mapping"); } @@ -157,7 +157,7 @@ namespace NzbDrone.Core.HealthCheck.Checks } // if it's a remote client/docker, likely missing path mappings - if (OsInfo.IsDocker) + if (_osInfo.IsDocker) { return new HealthCheck(GetType(), HealthCheckResult.Error, $"You are using docker; download client {client.Definition.Name} reported files in {dlpath} but this directory does not appear to exist inside the container. Review your remote path mappings and container volume settings.", "#docker-bad-remote-path-mapping"); } diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index 4219d7c4f..61b23c6ef 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -32,6 +32,7 @@ namespace NzbDrone.Core.Update private readonly IConfigFileProvider _configFileProvider; private readonly IRuntimeInfo _runtimeInfo; private readonly IBackupService _backupService; + private readonly IOsInfo _osInfo; public InstallUpdateService(ICheckUpdateService checkUpdateService, @@ -46,6 +47,7 @@ namespace NzbDrone.Core.Update IConfigFileProvider configFileProvider, IRuntimeInfo runtimeInfo, IBackupService backupService, + IOsInfo osInfo, Logger logger) { if (configFileProvider == null) @@ -64,6 +66,7 @@ namespace NzbDrone.Core.Update _configFileProvider = configFileProvider; _runtimeInfo = runtimeInfo; _backupService = backupService; + _osInfo = osInfo; _logger = logger; } @@ -204,6 +207,11 @@ namespace NzbDrone.Core.Update return; } + if (_osInfo.IsDocker) + { + throw new CommandFailedException("Updating is disabled inside a docker container. Please update the container image."); + } + if (OsInfo.IsNotWindows && !_configFileProvider.UpdateAutomatically && message.Trigger != CommandTrigger.Manual) { _logger.ProgressDebug("Auto-update not enabled, not installing available update");