diff --git a/source/Octopus.Client.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress..NETCore.approved.txt b/source/Octopus.Client.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress..NETCore.approved.txt index c0299011..f17a4868 100644 --- a/source/Octopus.Client.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress..NETCore.approved.txt +++ b/source/Octopus.Client.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress..NETCore.approved.txt @@ -30,6 +30,7 @@ Octopus.Client Boolean IsUsingSecureConnection { get; } Octopus.Client.IOctopusAsyncRepository Repository { get; } Octopus.Client.Model.RootResource RootDocument { get; } + Task AsUser(String, Octopus.Client.OctopusClientOptions) Task Create(String, Octopus.Client.TResource, Object) Task Create(String, Octopus.Client.TResource, CancellationToken) Task Create(String, Octopus.Client.TResource, Object, CancellationToken) @@ -98,6 +99,7 @@ Octopus.Client Boolean IsUsingSecureConnection { get; } Octopus.Client.IOctopusRepository Repository { get; } Octopus.Client.Model.RootResource RootDocument { get; } + Octopus.Client.IOctopusClient AsUser(String, Octopus.Client.OctopusClientOptions) Octopus.Client.TResource Create(String, Octopus.Client.TResource, Object) Octopus.Client.TResponse Create(String, Octopus.Client.TCommand, Object) void Delete(String, Object, Object) @@ -169,6 +171,10 @@ Octopus.Client Octopus.Client.IOctopusSystemRepository { } + interface IOctopusServerRootResourceCache + { + Octopus.Client.Model.RootResource CachedRootResource { get; set; } + } interface IOctopusSpaceAsyncRepository Octopus.Client.IOctopusCommonAsyncRepository { @@ -303,6 +309,7 @@ Octopus.Client class OctopusAsyncClient Octopus.Client.IOctopusAsyncClient IDisposable + Octopus.Client.IOctopusServerRootResourceCache { event Action AfterReceivedHttpResponse event Action BeforeSendingHttpRequest @@ -311,6 +318,7 @@ Octopus.Client Boolean IsUsingSecureConnection { get; } Octopus.Client.IOctopusAsyncRepository Repository { get; } Octopus.Client.Model.RootResource RootDocument { get; } + Task AsUser(String, Octopus.Client.OctopusClientOptions) static Task Create(Octopus.Client.OctopusServerEndpoint, Octopus.Client.OctopusClientOptions) Task Create(String, Octopus.Client.TResource, Object) Task Create(String, Octopus.Client.TResource, CancellationToken) @@ -451,6 +459,7 @@ Octopus.Client Octopus.Client.IHttpOctopusClient Octopus.Client.IOctopusClient IDisposable + Octopus.Client.IOctopusServerRootResourceCache { event Action AfterReceivingHttpResponse event Action BeforeSendingHttpRequest @@ -460,6 +469,7 @@ Octopus.Client Boolean IsUsingSecureConnection { get; } Octopus.Client.IOctopusRepository Repository { get; } Octopus.Client.Model.RootResource RootDocument { get; } + Octopus.Client.IOctopusClient AsUser(String, Octopus.Client.OctopusClientOptions) Octopus.Client.TResource Create(String, Octopus.Client.TResource, Object) Octopus.Client.TResponse Create(String, Octopus.Client.TCommand, Object) void Delete(String, Object, Object) @@ -618,6 +628,7 @@ Octopus.Client Octopus.Client.TResponseResource ResponseResource { get; } } class OctopusServerEndpoint + Octopus.Client.IOctopusServerRootResourceCache { .ctor(String) .ctor(String, String) @@ -625,6 +636,7 @@ Octopus.Client .ctor(Octopus.Client.ILinkResolver, String, ICredentials) String ApiKey { get; } String BearerToken { get; } + Octopus.Client.Model.RootResource CachedRootResource { get; set; } ICredentials Credentials { get; } Boolean IsUsingSecureConnection { get; } Octopus.Client.ILinkResolver OctopusServer { get; } diff --git a/source/Octopus.Client.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress..NETFramework.approved.txt b/source/Octopus.Client.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress..NETFramework.approved.txt index 104f9d15..01340768 100644 --- a/source/Octopus.Client.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress..NETFramework.approved.txt +++ b/source/Octopus.Client.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress..NETFramework.approved.txt @@ -30,6 +30,7 @@ Octopus.Client Boolean IsUsingSecureConnection { get; } Octopus.Client.IOctopusAsyncRepository Repository { get; } Octopus.Client.Model.RootResource RootDocument { get; } + Task AsUser(String, Octopus.Client.OctopusClientOptions) Task Create(String, Octopus.Client.TResource, Object) Task Create(String, Octopus.Client.TResource, CancellationToken) Task Create(String, Octopus.Client.TResource, Object, CancellationToken) @@ -98,6 +99,7 @@ Octopus.Client Boolean IsUsingSecureConnection { get; } Octopus.Client.IOctopusRepository Repository { get; } Octopus.Client.Model.RootResource RootDocument { get; } + Octopus.Client.IOctopusClient AsUser(String, Octopus.Client.OctopusClientOptions) Octopus.Client.TResource Create(String, Octopus.Client.TResource, Object) Octopus.Client.TResponse Create(String, Octopus.Client.TCommand, Object) void Delete(String, Object, Object) @@ -169,6 +171,10 @@ Octopus.Client Octopus.Client.IOctopusSystemRepository { } + interface IOctopusServerRootResourceCache + { + Octopus.Client.Model.RootResource CachedRootResource { get; set; } + } interface IOctopusSpaceAsyncRepository Octopus.Client.IOctopusCommonAsyncRepository { @@ -303,6 +309,7 @@ Octopus.Client class OctopusAsyncClient Octopus.Client.IOctopusAsyncClient IDisposable + Octopus.Client.IOctopusServerRootResourceCache { event Action AfterReceivedHttpResponse event Action BeforeSendingHttpRequest @@ -311,6 +318,7 @@ Octopus.Client Boolean IsUsingSecureConnection { get; } Octopus.Client.IOctopusAsyncRepository Repository { get; } Octopus.Client.Model.RootResource RootDocument { get; } + Task AsUser(String, Octopus.Client.OctopusClientOptions) static Task Create(Octopus.Client.OctopusServerEndpoint, Octopus.Client.OctopusClientOptions) Task Create(String, Octopus.Client.TResource, Object) Task Create(String, Octopus.Client.TResource, CancellationToken) @@ -451,6 +459,7 @@ Octopus.Client Octopus.Client.IHttpOctopusClient Octopus.Client.IOctopusClient IDisposable + Octopus.Client.IOctopusServerRootResourceCache { event Action AfterReceivingHttpResponse event Action BeforeSendingHttpRequest @@ -460,6 +469,7 @@ Octopus.Client Boolean IsUsingSecureConnection { get; } Octopus.Client.IOctopusRepository Repository { get; } Octopus.Client.Model.RootResource RootDocument { get; } + Octopus.Client.IOctopusClient AsUser(String, Octopus.Client.OctopusClientOptions) Octopus.Client.TResource Create(String, Octopus.Client.TResource, Object) Octopus.Client.TResponse Create(String, Octopus.Client.TCommand, Object) void Delete(String, Object, Object) @@ -616,6 +626,7 @@ Octopus.Client Octopus.Client.TResponseResource ResponseResource { get; } } class OctopusServerEndpoint + Octopus.Client.IOctopusServerRootResourceCache { .ctor(String) .ctor(String, String) @@ -623,6 +634,7 @@ Octopus.Client .ctor(Octopus.Client.ILinkResolver, String, ICredentials) String ApiKey { get; } String BearerToken { get; } + Octopus.Client.Model.RootResource CachedRootResource { get; set; } ICredentials Credentials { get; } Boolean IsUsingSecureConnection { get; } Octopus.Client.ILinkResolver OctopusServer { get; } diff --git a/source/Octopus.Server.Client/IOctopusAsyncClient.cs b/source/Octopus.Server.Client/IOctopusAsyncClient.cs index 7edacaa2..81dab541 100644 --- a/source/Octopus.Server.Client/IOctopusAsyncClient.cs +++ b/source/Octopus.Server.Client/IOctopusAsyncClient.cs @@ -938,6 +938,8 @@ Task Create(string path, TCommand command, objec /// Task SignOut(CancellationToken cancellationToken); + Task AsUser(string apiKey, OctopusClientOptions options = null); + /// /// Get a repository for the given space /// diff --git a/source/Octopus.Server.Client/IOctopusClient.cs b/source/Octopus.Server.Client/IOctopusClient.cs index dcdd222f..f4904f43 100644 --- a/source/Octopus.Server.Client/IOctopusClient.cs +++ b/source/Octopus.Server.Client/IOctopusClient.cs @@ -410,6 +410,7 @@ public interface IOctopusClient : IDisposable /// Sign out /// void SignOut(); + IOctopusClient AsUser(string apiKey, OctopusClientOptions options = null); IOctopusSpaceRepository ForSpace(SpaceResource space); IOctopusSystemRepository ForSystem(); diff --git a/source/Octopus.Server.Client/IOctopusServerRootResourceCache.cs b/source/Octopus.Server.Client/IOctopusServerRootResourceCache.cs new file mode 100644 index 00000000..60367630 --- /dev/null +++ b/source/Octopus.Server.Client/IOctopusServerRootResourceCache.cs @@ -0,0 +1,8 @@ +using Octopus.Client.Model; + +namespace Octopus.Client; + +public interface IOctopusServerRootResourceCache +{ + RootResource CachedRootResource { get; set; } +} \ No newline at end of file diff --git a/source/Octopus.Server.Client/OctopusAsyncClient.cs b/source/Octopus.Server.Client/OctopusAsyncClient.cs index 64e2773f..08fc5934 100644 --- a/source/Octopus.Server.Client/OctopusAsyncClient.cs +++ b/source/Octopus.Server.Client/OctopusAsyncClient.cs @@ -24,7 +24,7 @@ namespace Octopus.Client /// /// The Octopus Deploy RESTful HTTP API client. /// - public class OctopusAsyncClient : IOctopusAsyncClient + public class OctopusAsyncClient : IOctopusAsyncClient, IOctopusServerRootResourceCache { private static readonly ILog Logger = LogProvider.For(); @@ -125,6 +125,8 @@ private bool IgnoreServerCertificateCallback(HttpRequestMessage message, X509Cer return false; } + public async Task AsUser(string apiKey, OctopusClientOptions options = null) => await Create(serverEndpoint.AsUser(apiKey), options); + public IOctopusSpaceAsyncRepository ForSpace(SpaceResource space) { ValidateSpaceId(space); @@ -271,7 +273,13 @@ public async Task SignOut(CancellationToken cancellationToken) /// that it is only requested once for /// the current . /// - public RootResource RootDocument => Repository.LoadRootDocument().GetAwaiter().GetResult(); + public RootResource RootDocument => (this as IOctopusServerRootResourceCache)?.CachedRootResource ?? Repository.LoadRootDocument().GetAwaiter().GetResult(); + + RootResource IOctopusServerRootResourceCache.CachedRootResource + { + get => serverEndpoint.CachedRootResource; + set => serverEndpoint.CachedRootResource = value; + } /// /// Occurs when a request is about to be sent. diff --git a/source/Octopus.Server.Client/OctopusAsyncRepository.cs b/source/Octopus.Server.Client/OctopusAsyncRepository.cs index efc77daf..36dd070f 100644 --- a/source/Octopus.Server.Client/OctopusAsyncRepository.cs +++ b/source/Octopus.Server.Client/OctopusAsyncRepository.cs @@ -235,6 +235,11 @@ public async Task Link(string name) private async Task LoadRootDocumentInner(CancellationToken cancellationToken) { + if (Client is IOctopusServerRootResourceCache { CachedRootResource: { } rootResource }) + { + return rootResource; + } + var watch = Stopwatch.StartNew(); Exception lastError = null; @@ -285,7 +290,12 @@ private async Task LoadRootDocumentInner(CancellationToken cancell if (current < min || current > max) throw new UnsupportedApiVersionException($"This Octopus Deploy server uses a newer API specification ({rootDocument.ApiVersion}) than this tool can handle ({ApiConstants.SupportedApiSchemaVersionMin} to {ApiConstants.SupportedApiSchemaVersionMax}). Please check for updates to this tool."); - + + if (Client is IOctopusServerRootResourceCache cache) + { + cache.CachedRootResource = rootDocument; + } + return rootDocument; } diff --git a/source/Octopus.Server.Client/OctopusClient.cs b/source/Octopus.Server.Client/OctopusClient.cs index aa33e29b..2afc8e43 100644 --- a/source/Octopus.Server.Client/OctopusClient.cs +++ b/source/Octopus.Server.Client/OctopusClient.cs @@ -20,7 +20,7 @@ namespace Octopus.Client /// /// The Octopus Deploy RESTful HTTP API client. /// - public class OctopusClient : IHttpOctopusClient + public class OctopusClient : IHttpOctopusClient, IOctopusServerRootResourceCache { private static readonly ILog Logger = LogProvider.For(); @@ -53,7 +53,14 @@ internal OctopusClient(OctopusServerEndpoint serverEndpoint, string requestingTo Repository = new OctopusRepository(this); } - public RootResource RootDocument => Repository.LoadRootDocument(); + public RootResource RootDocument => (this as IOctopusServerRootResourceCache)?.CachedRootResource ?? Repository.LoadRootDocument(); + + RootResource IOctopusServerRootResourceCache.CachedRootResource + { + get => serverEndpoint.CachedRootResource; + set => serverEndpoint.CachedRootResource = value; + } + public IOctopusRepository Repository { get; private set; } /// @@ -61,6 +68,8 @@ internal OctopusClient(OctopusServerEndpoint serverEndpoint, string requestingTo /// public bool IsUsingSecureConnection => serverEndpoint.IsUsingSecureConnection; + public IOctopusClient AsUser(string apiKey, OctopusClientOptions options = null) => new OctopusClient(serverEndpoint.AsUser(apiKey), options); + public IOctopusSpaceRepository ForSpace(SpaceResource space) { ValidateSpaceId(space); diff --git a/source/Octopus.Server.Client/OctopusRepository.cs b/source/Octopus.Server.Client/OctopusRepository.cs index 6d011de1..5961536f 100644 --- a/source/Octopus.Server.Client/OctopusRepository.cs +++ b/source/Octopus.Server.Client/OctopusRepository.cs @@ -217,6 +217,11 @@ public string Link(string name) RootResource LoadRootDocumentInner() { + if (Client is IOctopusServerRootResourceCache { CachedRootResource: { } rootResource }) + { + return rootResource; + } + var watch = Stopwatch.StartNew(); Exception lastError = null; @@ -270,6 +275,11 @@ RootResource LoadRootDocumentInner() if (current < min || current > max) throw new UnsupportedApiVersionException(string.Format("This Octopus Deploy server uses a newer API specification ({0}) than this tool can handle ({1} to {2}). Please check for updates to this tool.", rootDocument.ApiVersion, ApiConstants.SupportedApiSchemaVersionMin, ApiConstants.SupportedApiSchemaVersionMax)); + if (Client is IOctopusServerRootResourceCache cache) + { + cache.CachedRootResource = rootDocument; + } + return rootDocument; } diff --git a/source/Octopus.Server.Client/OctopusServerEndpoint.cs b/source/Octopus.Server.Client/OctopusServerEndpoint.cs index 52aba366..27960559 100644 --- a/source/Octopus.Server.Client/OctopusServerEndpoint.cs +++ b/source/Octopus.Server.Client/OctopusServerEndpoint.cs @@ -1,12 +1,13 @@ using System; using System.Net; +using Octopus.Client.Model; namespace Octopus.Client { /// /// Specifies the location and credentials to use when communicating with an Octopus Deploy server. /// - public class OctopusServerEndpoint + public class OctopusServerEndpoint : IOctopusServerRootResourceCache { /// /// Create an instance with a Token to authenticate. @@ -148,7 +149,11 @@ public OctopusServerEndpoint(ILinkResolver octopusServer, string apiKey, ICreden /// An endpoint with a new user. public OctopusServerEndpoint AsUser(string newUserApiKey) { - return new OctopusServerEndpoint(OctopusServer, newUserApiKey, Credentials); + return new OctopusServerEndpoint(OctopusServer, newUserApiKey, Credentials) + { + // Preserve the cached root resource as we're still working with the same server, just a different API key + CachedRootResource = CachedRootResource, + }; } /// @@ -156,6 +161,8 @@ public OctopusServerEndpoint AsUser(string newUserApiKey) /// public IWebProxy Proxy { get; set; } + public RootResource CachedRootResource { get; set; } = null; + private static DefaultLinkResolver GetLinkResolverFromServerUrl(string octopusServerAddress) { if (string.IsNullOrWhiteSpace(octopusServerAddress))