Skip to content

Commit 29d485b

Browse files
authored
chore: add Vpn.Service app for Manager (#9)
1 parent 559b111 commit 29d485b

40 files changed

+2372
-345
lines changed

Coder.Desktop.sln

+25-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +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}") = "Coder.Desktop.Tests", "Tests\Tests.csproj", "{D247B2E7-38A0-4A69-A710-7E8FAA7B807E}"
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Coder.Desktop.Vpn.Service", "Vpn.Service\Vpn.Service.csproj", "{51B91794-0A2A-4F84-9935-8E17DD2AB260}"
9+
EndProject
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}"
15+
EndProject
16+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Coder.Desktop.CoderSdk", "CoderSdk\CoderSdk.csproj", "{A3D2B2B3-A051-46BD-A190-5487A9F24C28}"
917
EndProject
1018
Global
1119
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -21,9 +29,25 @@ Global
2129
{318E78BB-E6AD-410F-8F3F-B680F6880293}.Debug|Any CPU.Build.0 = Debug|Any CPU
2230
{318E78BB-E6AD-410F-8F3F-B680F6880293}.Release|Any CPU.ActiveCfg = Release|Any CPU
2331
{318E78BB-E6AD-410F-8F3F-B680F6880293}.Release|Any CPU.Build.0 = Release|Any CPU
32+
{51B91794-0A2A-4F84-9935-8E17DD2AB260}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33+
{51B91794-0A2A-4F84-9935-8E17DD2AB260}.Debug|Any CPU.Build.0 = Debug|Any CPU
34+
{51B91794-0A2A-4F84-9935-8E17DD2AB260}.Release|Any CPU.ActiveCfg = Release|Any CPU
35+
{51B91794-0A2A-4F84-9935-8E17DD2AB260}.Release|Any CPU.Build.0 = Release|Any CPU
2436
{D247B2E7-38A0-4A69-A710-7E8FAA7B807E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
2537
{D247B2E7-38A0-4A69-A710-7E8FAA7B807E}.Debug|Any CPU.Build.0 = Debug|Any CPU
2638
{D247B2E7-38A0-4A69-A710-7E8FAA7B807E}.Release|Any CPU.ActiveCfg = Release|Any CPU
2739
{D247B2E7-38A0-4A69-A710-7E8FAA7B807E}.Release|Any CPU.Build.0 = Release|Any CPU
40+
{AA3EEFF4-414B-4A83-8ACF-188C3C61CCE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41+
{AA3EEFF4-414B-4A83-8ACF-188C3C61CCE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
42+
{AA3EEFF4-414B-4A83-8ACF-188C3C61CCE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
43+
{AA3EEFF4-414B-4A83-8ACF-188C3C61CCE1}.Release|Any CPU.Build.0 = Release|Any CPU
44+
{D32E5FE1-C251-4A08-8EBE-B8D4F18A36F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
45+
{D32E5FE1-C251-4A08-8EBE-B8D4F18A36F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
46+
{D32E5FE1-C251-4A08-8EBE-B8D4F18A36F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
47+
{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
2852
EndGlobalSection
2953
EndGlobal

Coder.Desktop.sln.DotSettings

+3
Original file line numberDiff line numberDiff line change
@@ -253,4 +253,7 @@
253253
</Patterns>
254254
</s:String>
255255
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002EMemberReordering_002EMigrations_002ECSharpFileLayoutPatternRemoveIsAttributeUpgrade/@EntryIndexedValue">True</s:Boolean>
256+
<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>
258+
<s:Boolean x:Key="/Default/UserDictionary/Words/=replyable/@EntryIndexedValue">True</s:Boolean>
256259
<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 renamed to 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

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Coder.Desktop.Vpn.Proto;
2+
3+
namespace Coder.Desktop.Tests.Vpn.Proto;
4+
5+
[TestFixture]
6+
public class RpcRoleAttributeTest
7+
{
8+
[Test]
9+
public void Ok()
10+
{
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"));
19+
}
20+
}
21+
22+
[TestFixture]
23+
public class RpcMessageTest
24+
{
25+
[Test]
26+
public void GetRole()
27+
{
28+
// RpcMessage<RPC> is not a supported message type and doesn't have an
29+
// RpcRoleAttribute
30+
var ex = Assert.Throws<ArgumentException>(() => _ = RpcMessage<RPC>.GetRole());
31+
Assert.That(ex.Message,
32+
Does.Contain("Message type 'Coder.Desktop.Vpn.Proto.RPC' does not have a RpcRoleAttribute"));
33+
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"));
38+
}
39+
}
File renamed without changes.
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<RootNamespace>Coder.Desktop.Tests.Vpn.Proto</RootNamespace>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
9+
<IsPackable>false</IsPackable>
10+
<IsTestProject>true</IsTestProject>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<PackageReference Include="coverlet.collector" Version="6.0.2">
15+
<PrivateAssets>all</PrivateAssets>
16+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
17+
</PackageReference>
18+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
19+
<PackageReference Include="NUnit" Version="4.2.2"/>
20+
<PackageReference Include="NUnit.Analyzers" Version="4.4.0">
21+
<PrivateAssets>all</PrivateAssets>
22+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
23+
</PackageReference>
24+
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"/>
25+
</ItemGroup>
26+
27+
<ItemGroup>
28+
<Using Include="NUnit.Framework"/>
29+
</ItemGroup>
30+
31+
<ItemGroup>
32+
<ProjectReference Include="..\Vpn.Proto\Vpn.Proto.csproj"/>
33+
</ItemGroup>
34+
35+
</Project>

0 commit comments

Comments
 (0)