Skip to content

Commit 8b44bc4

Browse files
authored
Add utility for config loading (#521)
Just a simple utility class containing code for fetching configs from CDF. It's maybe a bit early for this, since I still don't have a perfectly clear idea of how it's going to be used, but most of the logic here makes sense in general, and is a foundation for future work.
1 parent bf8116d commit 8b44bc4

File tree

6 files changed

+551
-19
lines changed

6 files changed

+551
-19
lines changed

Cognite.Config/Configuration.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,11 @@ public static int GetVersionFromString(string yaml)
119119
/// in case of yaml parsing errors.</exception>
120120
public static T TryReadConfigFromString<T>(string yaml, params int[]? acceptedConfigVersions) where T : VersionedConfig
121121
{
122-
int configVersion = GetVersionFromString(yaml);
123-
CheckVersion(configVersion, acceptedConfigVersions);
122+
if ((acceptedConfigVersions?.Length ?? 0) > 0)
123+
{
124+
int configVersion = GetVersionFromString(yaml);
125+
CheckVersion(configVersion, acceptedConfigVersions);
126+
}
124127

125128
var config = ReadString<T>(yaml);
126129
config.GenerateDefaults();
@@ -142,8 +145,11 @@ public static T TryReadConfigFromString<T>(string yaml, params int[]? acceptedCo
142145
/// the yaml file is not found or in case of yaml parsing error.</exception>
143146
public static T TryReadConfigFromFile<T>(string path, params int[]? acceptedConfigVersions) where T : VersionedConfig
144147
{
145-
int configVersion = GetVersionFromFile(path);
146-
CheckVersion(configVersion, acceptedConfigVersions);
148+
if ((acceptedConfigVersions?.Length ?? 0) > 0)
149+
{
150+
int configVersion = GetVersionFromFile(path);
151+
CheckVersion(configVersion, acceptedConfigVersions);
152+
}
147153

148154
var config = Read<T>(path);
149155
config.GenerateDefaults();

ExtractorUtils.Test/unit/Unstable/CheckInWorkerTests.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using Cognite.ExtractorUtils.Unstable.Tasks;
2323
using Microsoft.Extensions.Logging;
2424
using CogniteSdk.Alpha;
25+
using CogniteSdk;
2526

2627

2728
namespace ExtractorUtils.Test.Unit.Unstable
@@ -64,11 +65,9 @@ private ConnectionConfig GetConfig()
6465
private (ServiceProvider, CheckInWorker) GetCheckInWorker()
6566
{
6667
var config = GetConfig();
67-
var baseCogniteConfig = new BaseCogniteConfig();
6868

6969
var services = new ServiceCollection();
7070
services.AddConfig(config, typeof(ConnectionConfig));
71-
services.AddConfig(baseCogniteConfig, typeof(BaseCogniteConfig));
7271
var mocks = TestUtilities.GetMockedHttpClientFactory(mockCheckInAsync);
7372
var mockHttpMessageHandler = mocks.handler;
7473
var mockFactory = mocks.factory;
@@ -77,9 +76,9 @@ private ConnectionConfig GetConfig()
7776
DestinationUtilsUnstable.AddCogniteClient(services, "myApp", null, setLogger: true, setMetrics: true, setHttpClient: true);
7877
var provider = services.BuildServiceProvider();
7978

80-
var dest = provider.GetRequiredService<CogniteDestination>();
79+
var client = provider.GetRequiredService<Client>();
8180

82-
return (provider, new CheckInWorker(config.Integration, provider.GetRequiredService<ILogger<CheckInWorker>>(), dest.CogniteClient));
81+
return (provider, new CheckInWorker(config.Integration, provider.GetRequiredService<ILogger<CheckInWorker>>(), client));
8382
}
8483

8584
[Fact]

ExtractorUtils.Test/unit/Unstable/ConnectionConfigTests.cs

Lines changed: 220 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,37 @@
1414
using Cognite.Extensions;
1515
using Cognite.Extractor.Utils;
1616
using Cognite.ExtractorUtils.Unstable.Configuration;
17+
using CogniteSdk.Alpha;
18+
using System.Net.Http.Headers;
19+
using CogniteSdk;
20+
using Microsoft.Extensions.Logging;
21+
using System.IO;
22+
using Cognite.ExtractorUtils.Unstable.Tasks;
23+
using ExtractorUtils.Test.unit.Unstable;
24+
using YamlDotNet.Core;
25+
using Cognite.Extractor.Common;
26+
using System.Dynamic;
27+
using Newtonsoft.Json;
1728

1829
namespace ExtractorUtils.Test.Unit.Unstable
1930
{
2031
public class ConnectionConfigTests
2132
{
22-
private static int _tokenCounter;
33+
private int _tokenCounter;
2334

2435
private readonly ITestOutputHelper _output;
2536
public ConnectionConfigTests(ITestOutputHelper output)
2637
{
2738
_output = output;
2839
}
2940

30-
[Fact]
31-
public async Task TestGetClient()
41+
private ConnectionConfig GetConfig()
3242
{
33-
var config = new ConnectionConfig
43+
return new ConnectionConfig
3444
{
3545
Project = "project",
3646
BaseUrl = "https://greenfield.cognitedata.com",
47+
Integration = "test-integration",
3748
Authentication = new ClientCredentialsConfig
3849
{
3950
ClientId = "someId",
@@ -45,6 +56,13 @@ public async Task TestGetClient()
4556
TokenUrl = "http://example.url/token",
4657
}
4758
};
59+
}
60+
61+
[Fact]
62+
public async Task TestGetClient()
63+
{
64+
var config = GetConfig();
65+
_tokenCounter = 0;
4866

4967
var baseCogniteConfig = new BaseCogniteConfig();
5068

@@ -57,6 +75,7 @@ public async Task TestGetClient()
5775
services.AddSingleton(mockFactory.Object);
5876
services.AddTestLogging(_output);
5977
DestinationUtilsUnstable.AddCogniteClient(services, "myApp", null, setLogger: true, setMetrics: true, setHttpClient: true);
78+
services.AddCogniteDestination();
6079
using var provider = services.BuildServiceProvider();
6180

6281
var auth = provider.GetRequiredService<IAuthenticator>();
@@ -68,7 +87,7 @@ public async Task TestGetClient()
6887
}
6988

7089

71-
private static Task<HttpResponseMessage> mockAuthSendAsync(HttpRequestMessage message, CancellationToken token)
90+
private Task<HttpResponseMessage> mockAuthSendAsync(HttpRequestMessage message, CancellationToken token)
7291
{
7392
// Verify endpoint and method
7493
Assert.Equal($@"http://example.url/token", message.RequestUri.ToString());
@@ -86,10 +105,205 @@ private static Task<HttpResponseMessage> mockAuthSendAsync(HttpRequestMessage me
86105
{
87106
StatusCode = HttpStatusCode.OK,
88107
Content = new StringContent(reply)
89-
90108
};
91109

92110
return Task.FromResult(response);
93111
}
112+
113+
class MyFancyConfig : VersionedConfig
114+
{
115+
public int Foo { get; set; }
116+
public string Bar { get; set; }
117+
118+
public override void GenerateDefaults()
119+
{
120+
}
121+
}
122+
123+
private (ConfigSource<MyFancyConfig>, DummySink) GetConfigSource(string configPath)
124+
{
125+
var config = GetConfig();
126+
_tokenCounter = 0;
127+
128+
var services = new ServiceCollection();
129+
services.AddConfig(config, typeof(ConnectionConfig));
130+
var mocks = TestUtilities.GetMockedHttpClientFactory(mockGetConfig);
131+
var mockHttpMessageHandler = mocks.handler;
132+
var mockFactory = mocks.factory;
133+
services.AddSingleton(mockFactory.Object);
134+
services.AddTestLogging(_output);
135+
DestinationUtilsUnstable.AddCogniteClient(services, "myApp", null, setLogger: true, setMetrics: true, setHttpClient: true);
136+
var provider = services.BuildServiceProvider();
137+
138+
Directory.CreateDirectory(configPath);
139+
140+
var configFile = configPath + "/config.yml";
141+
142+
var source = new ConfigSource<MyFancyConfig>(
143+
provider.GetRequiredService<Client>(),
144+
provider.GetRequiredService<ILogger<ConfigSource<MyFancyConfig>>>(),
145+
"test-integration",
146+
configFile,
147+
true);
148+
149+
var reporter = new DummySink();
150+
151+
return (source, reporter);
152+
}
153+
154+
[Fact]
155+
public async Task TestConfigSource()
156+
{
157+
var config = GetConfig();
158+
159+
var configPath = TestUtils.AlphaNumericPrefix("dotnet_extractor_test") + "_config";
160+
var (source, reporter) = GetConfigSource(configPath);
161+
var configFile = source.ConfigFilePath;
162+
163+
// Try to load a new config when one doesn't exist.
164+
await Assert.ThrowsAnyAsync<Exception>(async () => await source.ResolveLocalConfig(reporter, CancellationToken.None));
165+
166+
// Write an invalid local file.
167+
System.IO.File.WriteAllText(configFile, @"
168+
foo: 123
169+
baz: test
170+
");
171+
await Assert.ThrowsAsync<ConfigurationException>(async () => await source.ResolveLocalConfig(reporter, CancellationToken.None));
172+
173+
// 2 start, 2 end.
174+
Assert.Equal(4, reporter.Errors.Count);
175+
176+
await Assert.ThrowsAsync<ConfigurationException>(async () => await source.ResolveLocalConfig(reporter, CancellationToken.None));
177+
// Nothing has changed, no new errors.
178+
Assert.Equal(4, reporter.Errors.Count);
179+
180+
// Write a valid local file.
181+
System.IO.File.WriteAllText(configFile, @"
182+
foo: 123
183+
bar: test
184+
");
185+
var isNew = await source.ResolveLocalConfig(reporter, CancellationToken.None);
186+
Assert.True(isNew);
187+
188+
isNew = await source.ResolveLocalConfig(reporter, CancellationToken.None);
189+
Assert.False(isNew);
190+
Assert.Equal(123, source.Config.Foo);
191+
Assert.Equal("test", source.Config.Bar);
192+
193+
// Fail to fetch remote config
194+
await Assert.ThrowsAnyAsync<Exception>(async () => await source.ResolveRemoteConfig(null, reporter, CancellationToken.None));
195+
// Another 2 error reports.
196+
Assert.Equal(6, reporter.Errors.Count);
197+
198+
// Fail to fetch again with the same error.
199+
await Assert.ThrowsAnyAsync<Exception>(async () => await source.ResolveRemoteConfig(null, reporter, CancellationToken.None));
200+
// No new reports.
201+
Assert.Equal(6, reporter.Errors.Count);
202+
203+
_responseRevision = new ConfigRevision
204+
{
205+
ExternalId = "test-integration",
206+
Config = @"
207+
foo: 321
208+
bar: test
209+
",
210+
Revision = 1,
211+
};
212+
213+
Assert.Equal(2, _getConfigCount);
214+
215+
isNew = await source.ResolveRemoteConfig(null, reporter, CancellationToken.None);
216+
Assert.True(isNew);
217+
Assert.Equal(321, source.Config.Foo);
218+
Assert.Equal("test", source.Config.Bar);
219+
220+
isNew = await source.ResolveRemoteConfig(1, reporter, CancellationToken.None);
221+
Assert.False(isNew);
222+
// Only one new request
223+
Assert.Equal(3, _getConfigCount);
224+
225+
Assert.True(System.IO.File.Exists(configPath + "/_temp_config.yml"));
226+
227+
Directory.Delete(configPath, true);
228+
}
229+
230+
[Fact]
231+
public async Task TestBufferConfigFile()
232+
{
233+
var config = GetConfig();
234+
var configPath = TestUtils.AlphaNumericPrefix("dotnet_extractor_test") + "_config";
235+
var (source, reporter) = GetConfigSource(configPath);
236+
var configFile = source.ConfigFilePath;
237+
var bufferFile = configPath + "/_temp_config.yml";
238+
239+
var okRevision = new ConfigRevision
240+
{
241+
ExternalId = "test-integration",
242+
Config = @"
243+
foo: 321
244+
bar: test
245+
",
246+
Revision = 1,
247+
};
248+
_responseRevision = okRevision;
249+
250+
var isNew = await source.ResolveRemoteConfig(null, reporter, CancellationToken.None);
251+
Assert.True(isNew);
252+
Assert.True(System.IO.File.Exists(bufferFile));
253+
254+
// We can load the config from the buffer file.
255+
_responseRevision = null;
256+
isNew = await source.ResolveRemoteConfig(null, reporter, CancellationToken.None);
257+
Assert.True(isNew);
258+
259+
// Make the file write protected. Now loading remote config should fail until we delete it.
260+
System.IO.File.SetAttributes(bufferFile, FileAttributes.ReadOnly);
261+
_responseRevision = okRevision;
262+
await Assert.ThrowsAnyAsync<Exception>(async () => await source.ResolveRemoteConfig(null, reporter, CancellationToken.None));
263+
264+
// Delete the buffer file, loading remote config should now fail.
265+
System.IO.File.SetAttributes(bufferFile, FileAttributes.Normal);
266+
System.IO.File.Delete(bufferFile);
267+
_responseRevision = null;
268+
await Assert.ThrowsAnyAsync<Exception>(async () => await source.ResolveRemoteConfig(null, reporter, CancellationToken.None));
269+
270+
Directory.Delete(configPath, true);
271+
}
272+
273+
private ConfigRevision _responseRevision;
274+
private int _getConfigCount;
275+
276+
private async Task<HttpResponseMessage> mockGetConfig(HttpRequestMessage message, CancellationToken token)
277+
{
278+
var uri = message.RequestUri.ToString();
279+
if (uri == "http://example.url/token") return await mockAuthSendAsync(message, token);
280+
281+
Assert.Contains("/integrations/config", uri);
282+
_getConfigCount++;
283+
284+
if (_responseRevision == null)
285+
{
286+
dynamic res = new ExpandoObject();
287+
res.error = new ExpandoObject();
288+
res.error.code = 400;
289+
res.error.message = "Something went wrong";
290+
return new HttpResponseMessage
291+
{
292+
StatusCode = HttpStatusCode.BadRequest,
293+
Content = new StringContent(JsonConvert.SerializeObject(res)),
294+
};
295+
}
296+
297+
var resBody = System.Text.Json.JsonSerializer.Serialize(_responseRevision, Oryx.Cognite.Common.jsonOptions);
298+
var fresponse = new HttpResponseMessage
299+
{
300+
StatusCode = HttpStatusCode.OK,
301+
Content = new StringContent(resBody)
302+
};
303+
fresponse.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
304+
fresponse.Headers.Add("x-request-id", "1");
305+
306+
return fresponse;
307+
}
94308
}
95309
}

ExtractorUtils.Test/unit/Unstable/TaskSchedulerTest.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@
1010

1111
namespace ExtractorUtils.Test.unit.Unstable
1212
{
13-
class DummySink : IIntegrationSink
13+
class DummySink : BaseErrorReporter, IIntegrationSink
1414
{
1515
public List<ExtractorError> Errors { get; } = new();
1616
public List<(string, DateTime)> TaskStart { get; } = new();
1717
public List<(string, DateTime)> TaskEnd { get; } = new();
1818

19+
public override ExtractorError NewError(ErrorLevel level, string description, string details = null, DateTime? now = null)
20+
{
21+
return new ExtractorError(level, description, this, details, null, now);
22+
}
1923

2024
public void ReportError(ExtractorError error)
2125
{

0 commit comments

Comments
 (0)