Skip to content

Commit de6f40e

Browse files
committed
Add configuration, remove RpcRole, add CoderSdk
1 parent 0fb9567 commit de6f40e

30 files changed

+960
-289
lines changed

Coder.Desktop.sln

+14-8
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Coder.Desktop.Vpn", "Vpn\Vp
55
EndProject
66
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Coder.Desktop.Vpn.Proto", "Vpn.Proto\Vpn.Proto.csproj", "{318E78BB-E6AD-410F-8F3F-B680F6880293}"
77
EndProject
8-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Vpn", "Tests.Vpn\Tests.Vpn.csproj", "{D247B2E7-38A0-4A69-A710-7E8FAA7B807E}"
9-
EndProject
108
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Coder.Desktop.Vpn.Service", "Vpn.Service\Vpn.Service.csproj", "{51B91794-0A2A-4F84-9935-8E17DD2AB260}"
119
EndProject
12-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Vpn.Proto", "Tests.Vpn.Proto\Tests.Vpn.Proto.csproj", "{AA3EEFF4-414B-4A83-8ACF-188C3C61CCE1}"
10+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Coder.Desktop.Tests.Vpn", "Tests.Vpn\Tests.Vpn.csproj", "{D247B2E7-38A0-4A69-A710-7E8FAA7B807E}"
11+
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Coder.Desktop.Tests.Vpn.Proto", "Tests.Vpn.Proto\Tests.Vpn.Proto.csproj", "{AA3EEFF4-414B-4A83-8ACF-188C3C61CCE1}"
13+
EndProject
14+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Coder.Desktop.Tests.Vpn.Service", "Tests.Vpn.Service\Tests.Vpn.Service.csproj", "{D32E5FE1-C251-4A08-8EBE-B8D4F18A36F1}"
1315
EndProject
14-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Vpn.Service", "Tests.Vpn.Service\Tests.Vpn.Service.csproj", "{D32E5FE1-C251-4A08-8EBE-B8D4F18A36F1}"
16+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Coder.Desktop.CoderSdk", "CoderSdk\CoderSdk.csproj", "{A3D2B2B3-A051-46BD-A190-5487A9F24C28}"
1517
EndProject
1618
Global
1719
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -27,14 +29,14 @@ Global
2729
{318E78BB-E6AD-410F-8F3F-B680F6880293}.Debug|Any CPU.Build.0 = Debug|Any CPU
2830
{318E78BB-E6AD-410F-8F3F-B680F6880293}.Release|Any CPU.ActiveCfg = Release|Any CPU
2931
{318E78BB-E6AD-410F-8F3F-B680F6880293}.Release|Any CPU.Build.0 = Release|Any CPU
30-
{D247B2E7-38A0-4A69-A710-7E8FAA7B807E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31-
{D247B2E7-38A0-4A69-A710-7E8FAA7B807E}.Debug|Any CPU.Build.0 = Debug|Any CPU
32-
{D247B2E7-38A0-4A69-A710-7E8FAA7B807E}.Release|Any CPU.ActiveCfg = Release|Any CPU
33-
{D247B2E7-38A0-4A69-A710-7E8FAA7B807E}.Release|Any CPU.Build.0 = Release|Any CPU
3432
{51B91794-0A2A-4F84-9935-8E17DD2AB260}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
3533
{51B91794-0A2A-4F84-9935-8E17DD2AB260}.Debug|Any CPU.Build.0 = Debug|Any CPU
3634
{51B91794-0A2A-4F84-9935-8E17DD2AB260}.Release|Any CPU.ActiveCfg = Release|Any CPU
3735
{51B91794-0A2A-4F84-9935-8E17DD2AB260}.Release|Any CPU.Build.0 = Release|Any CPU
36+
{D247B2E7-38A0-4A69-A710-7E8FAA7B807E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
37+
{D247B2E7-38A0-4A69-A710-7E8FAA7B807E}.Debug|Any CPU.Build.0 = Debug|Any CPU
38+
{D247B2E7-38A0-4A69-A710-7E8FAA7B807E}.Release|Any CPU.ActiveCfg = Release|Any CPU
39+
{D247B2E7-38A0-4A69-A710-7E8FAA7B807E}.Release|Any CPU.Build.0 = Release|Any CPU
3840
{AA3EEFF4-414B-4A83-8ACF-188C3C61CCE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
3941
{AA3EEFF4-414B-4A83-8ACF-188C3C61CCE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
4042
{AA3EEFF4-414B-4A83-8ACF-188C3C61CCE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -43,5 +45,9 @@ Global
4345
{D32E5FE1-C251-4A08-8EBE-B8D4F18A36F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
4446
{D32E5FE1-C251-4A08-8EBE-B8D4F18A36F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
4547
{D32E5FE1-C251-4A08-8EBE-B8D4F18A36F1}.Release|Any CPU.Build.0 = Release|Any CPU
48+
{A3D2B2B3-A051-46BD-A190-5487A9F24C28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
49+
{A3D2B2B3-A051-46BD-A190-5487A9F24C28}.Debug|Any CPU.Build.0 = Debug|Any CPU
50+
{A3D2B2B3-A051-46BD-A190-5487A9F24C28}.Release|Any CPU.ActiveCfg = Release|Any CPU
51+
{A3D2B2B3-A051-46BD-A190-5487A9F24C28}.Release|Any CPU.Build.0 = Release|Any CPU
4652
EndGlobalSection
4753
EndGlobal

Coder.Desktop.sln.DotSettings

+1
Original file line numberDiff line numberDiff line change
@@ -254,4 +254,5 @@
254254
</s:String>
255255
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002EMemberReordering_002EMigrations_002ECSharpFileLayoutPatternRemoveIsAttributeUpgrade/@EntryIndexedValue">True</s:Boolean>
256256
<s:Boolean x:Key="/Default/UserDictionary/Words/=codervpn/@EntryIndexedValue">True</s:Boolean>
257+
<s:Boolean x:Key="/Default/UserDictionary/Words/=hkey/@EntryIndexedValue">True</s:Boolean>
257258
<s:Boolean x:Key="/Default/UserDictionary/Words/=serdes/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

CoderSdk/CoderApiClient.cs

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System.Text;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
5+
namespace CoderSdk;
6+
7+
/// <summary>
8+
/// Changes names from PascalCase to snake_case.
9+
/// </summary>
10+
internal class SnakeCaseNamingPolicy : JsonNamingPolicy
11+
{
12+
public override string ConvertName(string name)
13+
{
14+
return string.Concat(
15+
name.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + char.ToLower(x) : char.ToLower(x).ToString())
16+
);
17+
}
18+
}
19+
20+
/// <summary>
21+
/// Provides a limited selection of API methods for a Coder instance.
22+
/// </summary>
23+
public partial class CoderApiClient
24+
{
25+
// TODO: allow adding headers
26+
private readonly HttpClient _httpClient = new();
27+
private readonly JsonSerializerOptions _jsonOptions;
28+
29+
public CoderApiClient(string baseUrl)
30+
{
31+
var url = new Uri(baseUrl, UriKind.Absolute);
32+
if (url.PathAndQuery != "/")
33+
throw new ArgumentException($"Base URL '{baseUrl}' must not contain a path", nameof(baseUrl));
34+
_httpClient.BaseAddress = url;
35+
_jsonOptions = new JsonSerializerOptions
36+
{
37+
PropertyNameCaseInsensitive = true,
38+
PropertyNamingPolicy = new SnakeCaseNamingPolicy(),
39+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
40+
};
41+
}
42+
43+
public CoderApiClient(string baseUrl, string token) : this(baseUrl)
44+
{
45+
SetSessionToken(token);
46+
}
47+
48+
public void SetSessionToken(string token)
49+
{
50+
_httpClient.DefaultRequestHeaders.Remove("Coder-Session-Token");
51+
_httpClient.DefaultRequestHeaders.Add("Coder-Session-Token", token);
52+
}
53+
54+
private async Task<TResponse> SendRequestAsync<TResponse>(HttpMethod method, string path,
55+
object? payload, CancellationToken ct = default)
56+
{
57+
try
58+
{
59+
var request = new HttpRequestMessage(method, path);
60+
61+
if (payload is not null)
62+
{
63+
var json = JsonSerializer.Serialize(payload, _jsonOptions);
64+
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
65+
}
66+
67+
var res = await _httpClient.SendAsync(request, ct);
68+
// TODO: this should be improved to try and parse a codersdk.Error response
69+
res.EnsureSuccessStatusCode();
70+
71+
var content = await res.Content.ReadAsStringAsync(ct);
72+
var data = JsonSerializer.Deserialize<TResponse>(content, _jsonOptions);
73+
if (data is null) throw new JsonException("Deserialized response is null");
74+
return data;
75+
}
76+
catch (Exception e)
77+
{
78+
throw new Exception($"API Request: {method} {path} (req body: {payload is not null})", e);
79+
}
80+
}
81+
}

CoderSdk/CoderSdk.csproj

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
</Project>

CoderSdk/Deployment.cs

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace CoderSdk;
2+
3+
public class BuildInfo
4+
{
5+
public string ExternalUrl { get; set; } = "";
6+
public string Version { get; set; } = "";
7+
public string DashboardUrl { get; set; } = "";
8+
public bool Telemetry { get; set; } = false;
9+
public bool WorkspaceProxy { get; set; } = false;
10+
public string AgentApiVersion { get; set; } = "";
11+
public string ProvisionerApiVersion { get; set; } = "";
12+
public string UpgradeMessage { get; set; } = "";
13+
public string DeploymentId { get; set; } = "";
14+
}
15+
16+
public partial class CoderApiClient
17+
{
18+
public Task<BuildInfo> GetBuildInfo(CancellationToken ct = default)
19+
{
20+
return SendRequestAsync<BuildInfo>(HttpMethod.Get, "/api/v2/buildinfo", null, ct);
21+
}
22+
}

CoderSdk/Users.cs

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace CoderSdk;
2+
3+
public class User
4+
{
5+
public const string Me = "me";
6+
7+
// TODO: fill out more fields
8+
public string Username { get; set; } = "";
9+
}
10+
11+
public partial class CoderApiClient
12+
{
13+
public Task<User> GetUser(string user, CancellationToken ct = default)
14+
{
15+
return SendRequestAsync<User>(HttpMethod.Get, $"/api/v2/users/{user}", null, ct);
16+
}
17+
}

Tests.Vpn.Proto/RpcHeaderTest.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ public void Valid()
1111
{
1212
var headerStr = "codervpn manager 1.3,2.1";
1313
var header = RpcHeader.Parse(headerStr);
14-
Assert.That(header.Role.ToString(), Is.EqualTo(RpcRole.Manager));
14+
Assert.That(header.Role, Is.EqualTo("manager"));
1515
Assert.That(header.VersionList, Is.EqualTo(new RpcVersionList(new RpcVersion(1, 3), new RpcVersion(2, 1))));
1616
Assert.That(header.ToString(), Is.EqualTo(headerStr + "\n"));
1717
Assert.That(header.ToBytes().ToArray(), Is.EqualTo(Encoding.UTF8.GetBytes(headerStr + "\n")));
1818

1919
headerStr = "codervpn tunnel 1.0";
2020
header = RpcHeader.Parse(headerStr);
21-
Assert.That(header.Role.ToString(), Is.EqualTo(RpcRole.Tunnel));
21+
Assert.That(header.Role, Is.EqualTo("tunnel"));
2222
Assert.That(header.VersionList, Is.EqualTo(new RpcVersionList(new RpcVersion(1, 0))));
2323
Assert.That(header.ToString(), Is.EqualTo(headerStr + "\n"));
2424
Assert.That(header.ToBytes().ToArray(), Is.EqualTo(Encoding.UTF8.GetBytes(headerStr + "\n")));
@@ -35,7 +35,8 @@ public void ParseInvalid()
3535
Assert.That(ex.Message, Does.Contain("Wrong number of parts"));
3636
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("cats manager 1.0"));
3737
Assert.That(ex.Message, Does.Contain("Invalid preamble"));
38-
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("codervpn cats 1.0"));
39-
Assert.That(ex.Message, Does.Contain("Unknown role 'cats'"));
38+
// RpcHeader doesn't care about the role string as long as it isn't empty.
39+
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("codervpn 1.0"));
40+
Assert.That(ex.Message, Does.Contain("Invalid role in header string"));
4041
}
4142
}

Tests.Vpn.Proto/RpcMessageTest.cs

+13-13
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,16 @@ namespace Coder.Desktop.Tests.Vpn.Proto;
66
public class RpcRoleAttributeTest
77
{
88
[Test]
9-
public void Valid()
9+
public void Ok()
1010
{
11-
var role = new RpcRoleAttribute(RpcRole.Manager);
12-
Assert.That(role.Role.ToString(), Is.EqualTo(RpcRole.Manager));
13-
role = new RpcRoleAttribute(RpcRole.Tunnel);
14-
Assert.That(role.Role.ToString(), Is.EqualTo(RpcRole.Tunnel));
15-
}
16-
17-
[Test]
18-
public void Invalid()
19-
{
20-
Assert.Throws<ArgumentException>(() => _ = new RpcRoleAttribute("cats"));
11+
var role = new RpcRoleAttribute("manager");
12+
Assert.That(role.Role, Is.EqualTo("manager"));
13+
role = new RpcRoleAttribute("tunnel");
14+
Assert.That(role.Role, Is.EqualTo("tunnel"));
15+
role = new RpcRoleAttribute("service");
16+
Assert.That(role.Role, Is.EqualTo("service"));
17+
role = new RpcRoleAttribute("client");
18+
Assert.That(role.Role, Is.EqualTo("client"));
2119
}
2220
}
2321

@@ -33,7 +31,9 @@ public void GetRole()
3331
Assert.That(ex.Message,
3432
Does.Contain("Message type 'Coder.Desktop.Vpn.Proto.RPC' does not have a RpcRoleAttribute"));
3533

36-
Assert.That(ManagerMessage.GetRole().ToString(), Is.EqualTo(RpcRole.Manager));
37-
Assert.That(TunnelMessage.GetRole().ToString(), Is.EqualTo(RpcRole.Tunnel));
34+
Assert.That(ManagerMessage.GetRole(), Is.EqualTo("manager"));
35+
Assert.That(TunnelMessage.GetRole(), Is.EqualTo("tunnel"));
36+
Assert.That(ServiceMessage.GetRole(), Is.EqualTo("service"));
37+
Assert.That(ClientMessage.GetRole(), Is.EqualTo("client"));
3838
}
3939
}

Tests.Vpn.Proto/RpcRoleTest.cs

-22
This file was deleted.

Tests.Vpn.Service/DownloaderTest.cs

+53
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,59 @@ public async Task CoderSigned(CancellationToken ct)
5757
}
5858
}
5959

60+
[TestFixture]
61+
public class AssemblyVersionDownloadValidatorTest
62+
{
63+
[Test(Description = "No version on binary")]
64+
[CancelAfter(30_000)]
65+
public void NoVersion(CancellationToken ct)
66+
{
67+
// TODO: this
68+
}
69+
70+
[Test(Description = "Version mismatch")]
71+
[CancelAfter(30_000)]
72+
public void VersionMismatch(CancellationToken ct)
73+
{
74+
// TODO: this
75+
}
76+
77+
[Test(Description = "Version match")]
78+
[CancelAfter(30_000)]
79+
public async Task VersionMatch(CancellationToken ct)
80+
{
81+
// TODO: this
82+
await Task.CompletedTask;
83+
}
84+
}
85+
86+
[TestFixture]
87+
public class CombinationDownloadValidatorTest
88+
{
89+
[Test(Description = "All validators pass")]
90+
[CancelAfter(30_000)]
91+
public async Task AllPass(CancellationToken ct)
92+
{
93+
var validator = new CombinationDownloadValidator(
94+
NullDownloadValidator.Instance,
95+
NullDownloadValidator.Instance
96+
);
97+
await validator.ValidateAsync("test", ct);
98+
}
99+
100+
[Test(Description = "A validator fails")]
101+
[CancelAfter(30_000)]
102+
public void Fail(CancellationToken ct)
103+
{
104+
var validator = new CombinationDownloadValidator(
105+
NullDownloadValidator.Instance,
106+
new TestDownloadValidator(new Exception("test exception"))
107+
);
108+
var ex = Assert.ThrowsAsync<Exception>(() => validator.ValidateAsync("test", ct));
109+
Assert.That(ex.Message, Is.EqualTo("test exception"));
110+
}
111+
}
112+
60113
[TestFixture]
61114
public class DownloaderTest
62115
{

Tests.Vpn/SerdesTest.cs

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Buffers.Binary;
22
using Coder.Desktop.Vpn;
33
using Coder.Desktop.Vpn.Proto;
4+
using Coder.Desktop.Vpn.Utilities;
45
using Google.Protobuf;
56

67
namespace Coder.Desktop.Tests.Vpn;
@@ -12,7 +13,7 @@ public class SerdesTest
1213
[CancelAfter(30_000)]
1314
public async Task WriteReadMessage(CancellationToken ct)
1415
{
15-
var (stream1, stream2) = BidirectionalPipe.New();
16+
var (stream1, stream2) = BidirectionalPipe.NewInMemory();
1617
var serdes = new Serdes<ManagerMessage, ManagerMessage>();
1718

1819
var msg = new ManagerMessage
@@ -28,7 +29,7 @@ public async Task WriteReadMessage(CancellationToken ct)
2829
[CancelAfter(30_000)]
2930
public void WriteMessageTooLarge(CancellationToken ct)
3031
{
31-
var (stream1, _) = BidirectionalPipe.New();
32+
var (stream1, _) = BidirectionalPipe.NewInMemory();
3233
var serdes = new Serdes<ManagerMessage, ManagerMessage>();
3334

3435
var msg = new ManagerMessage
@@ -46,7 +47,7 @@ public void WriteMessageTooLarge(CancellationToken ct)
4647
[CancelAfter(30_000)]
4748
public async Task ReadMessageTooLarge(CancellationToken ct)
4849
{
49-
var (stream1, stream2) = BidirectionalPipe.New();
50+
var (stream1, stream2) = BidirectionalPipe.NewInMemory();
5051
var serdes = new Serdes<ManagerMessage, ManagerMessage>();
5152

5253
// In this test we don't actually write a message as the parser should
@@ -61,7 +62,7 @@ public async Task ReadMessageTooLarge(CancellationToken ct)
6162
[CancelAfter(30_000)]
6263
public async Task ReadEmptyMessage(CancellationToken ct)
6364
{
64-
var (stream1, stream2) = BidirectionalPipe.New();
65+
var (stream1, stream2) = BidirectionalPipe.NewInMemory();
6566
var serdes = new Serdes<ManagerMessage, ManagerMessage>();
6667

6768
// Write an empty message.
@@ -76,7 +77,7 @@ public async Task ReadEmptyMessage(CancellationToken ct)
7677
[CancelAfter(30_000)]
7778
public async Task ReadInvalidMessage(CancellationToken ct)
7879
{
79-
var (stream1, stream2) = BidirectionalPipe.New();
80+
var (stream1, stream2) = BidirectionalPipe.NewInMemory();
8081
var serdes = new Serdes<ManagerMessage, ManagerMessage>();
8182

8283
var lenBytes = new byte[4];

0 commit comments

Comments
 (0)