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

Add client operations to support registering the Kubernetes agent as a worker #858

Merged
merged 9 commits into from
Jun 28, 2024

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Octopus.Client.Extensibility;
using Octopus.Client.Model;
using Octopus.Client.Model.Endpoints;
using Octopus.Client.Operations;

namespace Octopus.Client.Tests.Operations
{
[TestFixture]
public class RegisterKubernetesWorkerOperationFixture
{
RegisterKubernetesWorkerOperation operation;
IOctopusClientFactory clientFactory;
IOctopusAsyncClient client;
OctopusServerEndpoint serverEndpoint;
ResourceCollection<WorkerResource> workers;
ResourceCollection<WorkerPoolResource> workerPools;
private ResourceCollection<ProxyResource> proxies;

[SetUp]
public void SetUp()
{
clientFactory = Substitute.For<IOctopusClientFactory>();
client = Substitute.For<IOctopusAsyncClient>();
clientFactory.CreateAsyncClient(Arg.Any<OctopusServerEndpoint>()).Returns(client);
operation = new RegisterKubernetesWorkerOperation(clientFactory);
serverEndpoint = new OctopusServerEndpoint("http://octopus", "ABC123");

workers = new ResourceCollection<WorkerResource>(new WorkerResource[0], LinkCollection.Self("/foo"));
workerPools = new ResourceCollection<WorkerPoolResource>(new WorkerPoolResource[0], LinkCollection.Self("/foo"));
proxies = new ResourceCollection<ProxyResource>(new ProxyResource[0], LinkCollection.Self("/foo"));
var rootDocument = new RootResource
{
ApiVersion = "3.0.0",
Version = "2099.0.0",
Links = LinkCollection.Self("/api")
.Add("Workers", "/api/workers")
.Add("WorkerPools", "/api/workerpools")
.Add("CurrentUser", "/api/users/me")
.Add("SpaceHome", "/api/spaces")
.Add("Proxies", "/api/proxies")
};

client.Get<RootResource>(Arg.Any<string>(), Arg.Any<CancellationToken>()).Returns(rootDocument);
client.Repository.HasLink(Arg.Any<string>()).Returns(ci => rootDocument.HasLink(ci.Arg<string>()));
client.Repository.Link(Arg.Any<string>()).Returns(ci => rootDocument.Link(ci.Arg<string>()));

client.When(x => x.Paginate(Arg.Any<string>(), Arg.Any<object>(), Arg.Any<Func<ResourceCollection<WorkerResource>, bool>>(), Arg.Any<CancellationToken>()))
.Do(ci => ci.Arg<Func<ResourceCollection<WorkerResource>, bool>>()(workers));
client.When(x => x.Paginate(Arg.Any<string>(), Arg.Any<object>(), Arg.Any<Func<ResourceCollection<WorkerPoolResource>, bool>>(), Arg.Any<CancellationToken>()))
.Do(ci => ci.Arg<Func<ResourceCollection<WorkerPoolResource>, bool>>()(workerPools));
client.When(x => x.Paginate(Arg.Any<string>(), Arg.Any<object>(), Arg.Any<Func<ResourceCollection<ProxyResource>, bool>>(), Arg.Any<CancellationToken>()))
.Do(ci => ci.Arg<Func<ResourceCollection<ProxyResource>, bool>>()(proxies));
}

[Test]
public async Task ShouldCreateNewListeningKubernetesTentacle()
{
var workerResources = new List<WorkerResource>();
await client.Create("/api/workers", Arg.Do<WorkerResource>(m => workerResources.Add(m)), Arg.Any<object>(), Arg.Any<CancellationToken>());

workerPools.Items.Add(new WorkerPoolResource {Id = "WorkerPools-1", Name = "PoolA", Slug = "workerpool-1", WorkerPoolType = WorkerPoolType.StaticWorkerPool, Links = LinkCollection.Self("/api/workerpools/WorkerPools-1")});
workerPools.Items.Add(new WorkerPoolResource {Id = "WorkerPools-2", Name = "PoolB", Slug = "workerpool-2", WorkerPoolType = WorkerPoolType.DynamicWorkerPool, Links = LinkCollection.Self("/api/workerpools/WorkerPools-2")});

operation.TentacleThumbprint = "ABCDEF";
operation.TentaclePort = 10930;
operation.MachineName = "MyMachine";
operation.TentacleHostname = "mymachine.test.com";
operation.CommunicationStyle = CommunicationStyle.TentaclePassive;
operation.WorkerPools = new[] {"PoolA", "PoolB"};

await operation.ExecuteAsync(serverEndpoint).ConfigureAwait(false);

workerResources.Should().ContainSingle().Which.Endpoint.Should().BeOfType<KubernetesTentacleEndpointResource>().Which.Should().BeEquivalentTo(new KubernetesTentacleEndpointResource(new ListeningTentacleEndpointConfigurationResource("ABCDEF", "https://mymachine.test.com:10930/")));
}

[Test]
public async Task ShouldCreateNewPollingKubernetesTentacle()
{
var workerResources = new List<WorkerResource>();
await client.Create("/api/workers", Arg.Do<WorkerResource>(m => workerResources.Add(m)), Arg.Any<object>(), Arg.Any<CancellationToken>());

workerPools.Items.Add(new WorkerPoolResource {Id = "WorkerPools-1", Name = "PoolA", Slug = "workerpool-1", WorkerPoolType = WorkerPoolType.StaticWorkerPool, Links = LinkCollection.Self("/api/workerpools/WorkerPools-1")});
workerPools.Items.Add(new WorkerPoolResource {Id = "WorkerPools-2", Name = "PoolB", Slug = "workerpool-2", WorkerPoolType = WorkerPoolType.DynamicWorkerPool, Links = LinkCollection.Self("/api/workerpools/WorkerPools-2")});

operation.TentacleThumbprint = "ABCDEF";
operation.TentaclePort = 10930;
operation.MachineName = "MyOtherMachine";
operation.TentacleHostname = "myothermachine.test.com";
operation.CommunicationStyle = CommunicationStyle.TentacleActive;
operation.WorkerPools = new[] {"workerpool-2"};
operation.SubscriptionId = new Uri("poll://ckyhfyfkcbmzjl8sfgch/");

await operation.ExecuteAsync(serverEndpoint).ConfigureAwait(false);

workerResources.Should().ContainSingle().Which.Endpoint.Should().BeOfType<KubernetesTentacleEndpointResource>().Which.Should().BeEquivalentTo(new KubernetesTentacleEndpointResource(new PollingTentacleEndpointConfigurationResource("ABCDEF", "poll://ckyhfyfkcbmzjl8sfgch/")));
}

[Test]
public async Task ShouldOnlyUpdateEndpointConnectionConfigurationOnExistingWorker()
{
var updatedWorkers = await GetUpdatedWorkerAfterOperation(false);

var worker = updatedWorkers.Should().ContainSingle().Which;
worker.Should().BeEquivalentTo(new
{
Id = "workers-84",
Name = "MyWorker",
WorkerPoolIds = new ReferenceCollection("WorkerPools-1"),
Endpoint = new KubernetesTentacleEndpointResource(new ListeningTentacleEndpointConfigurationResource("ABCDEF", "https://my-new-worker.test.com:10930/") {ProxyId = "proxy-1"}),
Links = LinkCollection.Self("/machines/workers-84")
});
}

[Test]
public async Task ShouldUpdateExistingWorkerWhenForceIsEnabled()
{
var updatedWorkers = await GetUpdatedWorkerAfterOperation(true);

var worker =updatedWorkers.Should().ContainSingle().Which;
worker.Should().BeEquivalentTo(new
{
Id = "workers-84",
Name = "MyWorker",
WorkerPoolIds = new ReferenceCollection("WorkerPools-2"),
Endpoint = new KubernetesTentacleEndpointResource(new ListeningTentacleEndpointConfigurationResource("ABCDEF", "https://my-new-worker.test.com:10930/") {ProxyId = "proxy-1"}),
Links = LinkCollection.Self("/machines/workers-84")
});
}

async Task<List<WorkerResource>> GetUpdatedWorkerAfterOperation(bool allowOverwrite)
{
var updatedWorkers = new List<WorkerResource>();
await client.Update("/machines/workers-84", Arg.Do<WorkerResource>(m => updatedWorkers.Add(m)), Arg.Any<object>(), Arg.Any<CancellationToken>());

workerPools.Items.Add(new WorkerPoolResource {Id = "WorkerPools-1", Name = "PoolA", Slug = "workerpool-1", WorkerPoolType = WorkerPoolType.StaticWorkerPool, Links = LinkCollection.Self("/api/workerpools/WorkerPools-1")});
workerPools.Items.Add(new WorkerPoolResource {Id = "WorkerPools-2", Name = "PoolB", Slug = "workerpool-2", WorkerPoolType = WorkerPoolType.DynamicWorkerPool, Links = LinkCollection.Self("/api/workerpools/WorkerPools-2")});

workers.Items.Add(new WorkerResource
{
Id = "workers-84", WorkerPoolIds = new ReferenceCollection(new[] { "WorkerPools-1" }), Name = "MyWorker", Links = LinkCollection.Self("/machines/workers-84"),
Endpoint = new KubernetesTentacleEndpointResource(new ListeningTentacleEndpointConfigurationResource("123456", "myworker.test.com") { ProxyId = "proxy-2" })
});

proxies.Items.Add(new ProxyResource { Id = "proxy-1", Name = "MyNewProxy", Links = LinkCollection.Self("/api/proxies/proxy-1") });

operation.TentacleThumbprint = "ABCDEF";
operation.TentaclePort = 10930;
operation.MachineName = "MyWorker";
operation.TentacleHostname = "my-new-worker.test.com";
operation.CommunicationStyle = CommunicationStyle.TentaclePassive;
operation.WorkerPools = new[] {"PoolB"};
operation.ProxyName = "MyNewProxy";
operation.AllowOverwrite = allowOverwrite;

await operation.ExecuteAsync(serverEndpoint).ConfigureAwait(false);

return updatedWorkers;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class RegisterMachineOperationFixture
IOctopusAsyncClient client;
OctopusServerEndpoint serverEndpoint;
ResourceCollection<EnvironmentResource> environments;
ResourceCollection<TenantResource> tenants;
ResourceCollection<MachineResource> machines;
ResourceCollection<MachinePolicyResource> machinePolicies;

Expand All @@ -34,6 +35,7 @@ public void SetUp()
serverEndpoint = new OctopusServerEndpoint("http://octopus", "ABC123");

environments = new ResourceCollection<EnvironmentResource>(Array.Empty<EnvironmentResource>(), LinkCollection.Self("/foo"));
tenants = new ResourceCollection<TenantResource>(Array.Empty<TenantResource>(), LinkCollection.Self("/foo"));
machines = new ResourceCollection<MachineResource>(Array.Empty<MachineResource>(), LinkCollection.Self("/foo"));
machinePolicies = new ResourceCollection<MachinePolicyResource>(Array.Empty<MachinePolicyResource>(), LinkCollection.Self("/foo"));
var rootDocument = new RootResource
Expand All @@ -42,6 +44,7 @@ public void SetUp()
Version = "2099.0.0",
Links = LinkCollection.Self("/api")
.Add("Environments", "/api/environments")
.Add("Tenants", "/api/tenants")
.Add("Machines", "/api/machines")
.Add("MachinePolicies", "/api/machinepolicies")
.Add("CurrentUser", "/api/users/me")
Expand All @@ -54,45 +57,47 @@ public void SetUp()

client.When(x => x.Paginate(Arg.Any<string>(), Arg.Any<object>(), Arg.Any<Func<ResourceCollection<EnvironmentResource>, bool>>(), Arg.Any<CancellationToken>()))
.Do(ci => ci.Arg<Func<ResourceCollection<EnvironmentResource>, bool>>()(environments));
client.When(x => x.Paginate(Arg.Any<string>(), Arg.Any<object>(), Arg.Any<Func<ResourceCollection<TenantResource>, bool>>(), Arg.Any<CancellationToken>()))
.Do(ci => ci.Arg<Func<ResourceCollection<TenantResource>, bool>>()(tenants));
client.When(x => x.Paginate(Arg.Any<string>(), Arg.Any<object>(), Arg.Any<Func<ResourceCollection<MachineResource>, bool>>(), Arg.Any<CancellationToken>()))
.Do(ci => ci.Arg<Func<ResourceCollection<MachineResource>, bool>>()(machines));
client.When(x => x.Paginate(Arg.Any<string>(), Arg.Any<object>(), Arg.Any<Func<ResourceCollection<MachinePolicyResource>, bool>>(), Arg.Any<CancellationToken>()))
.Do(ci => ci.Arg<Func<ResourceCollection<MachinePolicyResource>, bool>>()(machinePolicies));
}

[Test]
public void ShouldThrowIfEnvironmentNameNotFound()
public async Task ShouldThrowIfEnvironmentNameNotFound()
{
operation.EnvironmentNames = new[] {"Atlantis"};
Func<Task> exec = () => operation.ExecuteAsync(serverEndpoint);
exec.Should().ThrowAsync<ArgumentException>().WithMessage("Could not find the environment named Atlantis on the Octopus Server. Ensure the environment exists and you have permission to access it.");
await exec.Should().ThrowAsync<ArgumentException>().WithMessage("Could not find the environment named Atlantis on the Octopus Server. Ensure the environment exists and you have permission to access it.");
}

[Test]
public void ShouldThrowIfEnvironmentNameNotFoundEvenWhenEnvironmentsHasValid()
public async Task ShouldThrowIfEnvironmentNameNotFoundEvenWhenEnvironmentsHasValid()
{
environments.Items.Add(new EnvironmentResource { Id = "environments-2", Name = "Production", Links = LinkCollection.Self("/api/environments/environments-2").Add("Machines", "/api/environments/environments-2/machines") });

operation.EnvironmentNames = new[] {"Atlantis"};
operation.Environments = new[] {"Production"};
Func<Task> exec = () => operation.ExecuteAsync(serverEndpoint);
exec.Should().ThrowAsync<ArgumentException>().WithMessage("Could not find the environment named Atlantis on the Octopus Server. Ensure the environment exists and you have permission to access it.");
await exec.Should().ThrowAsync<ArgumentException>().WithMessage("Could not find the environment named Atlantis on the Octopus Server. Ensure the environment exists and you have permission to access it.");
}

[Test]
public void ShouldThrowIfEnvironmentNotFoundBySlug()
public async Task ShouldThrowIfEnvironmentNotFoundBySlug()
{
operation.Environments = new[] {"atlantis-slug"};
Func<Task> exec = () => operation.ExecuteAsync(serverEndpoint);
exec.Should().ThrowAsync<ArgumentException>().WithMessage("Could not find the environment with name, slug or Id atlantis-slug on the Octopus Server. Ensure the environment exists and you have permission to access it.");
await exec.Should().ThrowAsync<ArgumentException>().WithMessage("Could not find the environment with name, slug or Id atlantis-slug on the Octopus Server. Ensure the environment exists and you have permission to access it.");
}

[Test]
public void ShouldThrowIfEnvironmentNotFoundByIdOrSlug()
public async Task ShouldThrowIfEnvironmentNotFoundByIdOrSlug()
{
operation.Environments = new[] {"Environments-12345","atlantis-slug" };
Func<Task> exec = () => operation.ExecuteAsync(serverEndpoint);
exec.Should().ThrowAsync<ArgumentException>().WithMessage("Could not find the environment with names, slugs or Ids: Environments-12345, \"atlantis-slug\" on the Octopus Server. Ensure the environment exists and you have permission to access it.");
await exec.Should().ThrowAsync<ArgumentException>().WithMessage("Could not find the environments with names, slugs or Ids: Environments-12345, atlantis-slug on the Octopus Server. Ensure the environments exist and you have permission to access them.");
}

[Test]
Expand All @@ -104,6 +109,28 @@ public async Task ShouldThrowIfAnyEnvironmentNotFound()
Func<Task> exec = () => operation.ExecuteAsync(serverEndpoint);
await exec.Should().ThrowAsync<ArgumentException>().WithMessage("Could not find the environments named: Atlantis, Hyperborea on the Octopus Server. Ensure the environments exist and you have permission to access them.");
}

[Test]
public async Task ShouldThrowIfAnyTenantNotFoundByNameIdOrSlug()
{
tenants.Items.Add(new TenantResource { Id = "Tenants-2", Name = "MyTenant", Slug = "my-tenant", Links = LinkCollection.Self("/api/environments/tenants-2") });

operation.Tenants = new[] {"some-tenant", "Atlantis", "MyTenant"};
Func<Task> exec = () => operation.ExecuteAsync(serverEndpoint);
await exec.Should().ThrowAsync<ArgumentException>().WithMessage("Could not find the tenants with names, slugs or Ids: some-tenant, Atlantis on the Octopus Server. Ensure the tenants exist and you have permission to access them.");
}

[Test]
public async Task ShouldNotThrowIfAllTenantsAreFoundByNameIdOrSlug()
{
tenants.Items.Add(new TenantResource { Id = "Tenants-1", Name = "SomeTenant", Slug = "some-tenant", Links = LinkCollection.Self("/api/environments/tenants-1") });
tenants.Items.Add(new TenantResource { Id = "Tenants-2", Name = "MyTenant", Slug = "my-tenant", Links = LinkCollection.Self("/api/environments/tenants-2") });
tenants.Items.Add(new TenantResource { Id = "Tenants-3", Name = "Atlantis", Slug = "atlantis", Links = LinkCollection.Self("/api/environments/tenants-3") });

operation.Tenants = new[] {"Atlantis", "Tenants-1", "some-tenant"};
Func<Task> exec = () => operation.ExecuteAsync(serverEndpoint);
await exec.Should().NotThrowAsync();
}

[Test]
public async Task ShouldCreateNewMachine()
Expand Down
Loading
Loading