Skip to content

Commit ef80955

Browse files
525 warn on mismatch between app and schema versions (#1196)
* Add schema version validation and log a warning, also copying dup of CompareSemVer * Add schema version validation for devproxy config * Add schema version validation for plugin section config * Add schema version validation for plugin config * Remove CompareSemVer dup and refactor a bit * Fix build and adjust format * Remove pre-release suffix from scheme version if any * Refactor GetVersionString(), rename to NormalizeVersion * Remove redundant null-coalescing assignment * Layout * Fix regression, notify to update a beta version if there is a released one * Fix to compare normalized scheme versions and avoid false-positive scheme warning if current is beta version --------- Co-authored-by: Waldek Mastykarz <[email protected]>
1 parent f1f50b1 commit ef80955

File tree

6 files changed

+141
-81
lines changed

6 files changed

+141
-81
lines changed

dev-proxy-abstractions/BaseLoader.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@ private async Task<bool> ValidateFileContents(string fileContents)
2424
using var document = JsonDocument.Parse(fileContents, ProxyUtils.JsonDocumentOptions);
2525
var root = document.RootElement;
2626

27-
if (!root.TryGetProperty("$schema", out var schemaUrl))
27+
if (!root.TryGetProperty("$schema", out var schemaUrlElement))
2828
{
2929
_logger.LogDebug("Schema reference not found in file {File}. Skipping schema validation", FilePath);
3030
return true;
3131
}
3232

33-
var (IsValid, ValidationErrors) = await ProxyUtils.ValidateJson(fileContents, schemaUrl.GetString(), _logger);
33+
var schemaUrl = schemaUrlElement.GetString() ?? "";
34+
ProxyUtils.ValidateSchemaVersion(schemaUrl, _logger);
35+
var (IsValid, ValidationErrors) = await ProxyUtils.ValidateJson(fileContents, schemaUrl, _logger);
36+
3437
if (!IsValid)
3538
{
3639
_logger.LogError("Schema validation failed for {File} with the following errors: {Errors}", FilePath, string.Join(", ", ValidationErrors));

dev-proxy-abstractions/BaseProxyPlugin.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public virtual async Task RegisterAsync()
8383
return (false, [string.Format(CultureInfo.InvariantCulture, "Configuration section {0} not found in configuration file", configSectionName)]);
8484
}
8585

86+
ProxyUtils.ValidateSchemaVersion(schemaUrl, Logger);
8687
return await ProxyUtils.ValidateJson(configSection.GetRawText(), schemaUrl, Logger);
8788
}
8889
catch (Exception ex)

dev-proxy-abstractions/ProxyUtils.cs

Lines changed: 123 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,6 @@ public static string ProductVersion
320320
}
321321
}
322322

323-
324323
return _productVersion;
325324
}
326325
}
@@ -355,7 +354,7 @@ public static void MergeHeaders(IList<MockResponseHeader> allHeaders, IList<Mock
355354
}
356355

357356
public static JsonSerializerOptions JsonSerializerOptions => jsonSerializerOptions;
358-
357+
359358
public static JsonDocumentOptions JsonDocumentOptions { get; } = new()
360359
{
361360
AllowTrailingCommas = true,
@@ -425,7 +424,8 @@ public static List<string> GetWildcardPatterns(List<string> urls)
425424
}
426425

427426
// For multiple URLs, find the common prefix
428-
var paths = group.Select(url => {
427+
var paths = group.Select(url =>
428+
{
429429
if (url.Contains('*'))
430430
{
431431
return url;
@@ -518,10 +518,126 @@ public static string ReplaceVariables(string s, Dictionary<string, string> varia
518518
return s1;
519519
}
520520

521-
public static string GetVersionString(string productVersion)
521+
public static void ValidateSchemaVersion(string schemaUrl, ILogger logger)
522+
{
523+
if (string.IsNullOrWhiteSpace(schemaUrl))
524+
{
525+
logger.LogDebug("Schema is empty, skipping schema version validation.");
526+
return;
527+
}
528+
529+
try
530+
{
531+
var uri = new Uri(schemaUrl);
532+
if (uri.Segments.Length > 2)
533+
{
534+
var schemaVersion = uri.Segments[^2]
535+
.TrimStart('v')
536+
.TrimEnd('/');
537+
var currentVersion = NormalizeVersion(ProductVersion);
538+
if (CompareSemVer(currentVersion, schemaVersion) != 0)
539+
{
540+
var currentSchemaUrl = uri.ToString().Replace($"/v{schemaVersion}/", $"/v{currentVersion}/");
541+
logger.LogWarning("The version of schema does not match the installed Dev Proxy version, the expected schema is {schema}", currentSchemaUrl);
542+
}
543+
}
544+
else
545+
{
546+
logger.LogDebug("Invalid schema {schemaUrl}, skipping schema version validation.", schemaUrl);
547+
}
548+
}
549+
catch (Exception ex)
550+
{
551+
logger.LogWarning("Invalid schema {schemaUrl}, skipping schema version validation. Error: {error}", schemaUrl, ex.Message);
552+
}
553+
}
554+
555+
/// <summary>
556+
/// Compares two semantic versions strings.
557+
/// </summary>
558+
/// <param name="a">ver1</param>
559+
/// <param name="b">ver2</param>
560+
/// <returns>
561+
/// Returns 0 if the versions are equal, -1 if a is less than b, and 1 if a is greater than b.
562+
/// An invalid argument is "rounded" to a minimal version.
563+
/// </returns>
564+
public static int CompareSemVer(string? a, string? b)
565+
{
566+
if (string.IsNullOrWhiteSpace(a) && string.IsNullOrWhiteSpace(b))
567+
{
568+
return 0;
569+
}
570+
else if (string.IsNullOrWhiteSpace(a))
571+
{
572+
return -1;
573+
}
574+
else if (string.IsNullOrWhiteSpace(b))
575+
{
576+
return 1;
577+
}
578+
579+
a = a.TrimStart('v');
580+
b = b.TrimStart('v');
581+
582+
var aParts = a.Split('-');
583+
var bParts = b.Split('-');
584+
585+
var aParsed = Version.TryParse(aParts[0], out var aVersion);
586+
var bParsed = Version.TryParse(bParts[0], out var bVersion);
587+
if (!aParsed && !bParsed)
588+
{
589+
return 0;
590+
}
591+
else if (!aParsed)
592+
{
593+
return -1;
594+
}
595+
else if (!bParsed)
596+
{
597+
return 1;
598+
}
599+
600+
var compare = aVersion!.CompareTo(bVersion);
601+
if (compare != 0)
602+
{
603+
// if the versions are different, return the comparison result
604+
return compare;
605+
}
606+
607+
// if the versions are the same, compare the prerelease tags
608+
if (aParts.Length == 1 && bParts.Length == 1)
609+
{
610+
// if both versions are stable, they are equal
611+
return 0;
612+
}
613+
else if (aParts.Length == 1)
614+
{
615+
// if a is stable and b is not, a is greater
616+
return 1;
617+
}
618+
else if (bParts.Length == 1)
619+
{
620+
// if b is stable and a is not, b is greater
621+
return -1;
622+
}
623+
else if (aParts[1] == bParts[1])
624+
{
625+
// if both versions are prerelease and the tags are the same, they are equal
626+
return 0;
627+
}
628+
else
629+
{
630+
// if both versions are prerelease, b is greater
631+
return -1;
632+
}
633+
}
634+
635+
/// <summary>
636+
/// Produces major.minor.patch version dropping a pre-release suffix.
637+
/// </summary>
638+
/// <param name="version">A version looks like "0.28.1", "0.28.1-alpha", "0.28.10-beta.1", "0.28.10-rc.1", or "0.28.0-preview-1", etc.</param>
639+
public static string NormalizeVersion(string version)
522640
{
523-
return productVersion.Contains("-beta")
524-
? productVersion.Split("-")[0]
525-
: productVersion;
641+
return version.Split('-', StringSplitOptions.None)[0];
526642
}
527643
}

dev-proxy/CommandHandlers/ConfigNewCommandHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class VisualStudioCodeSnippet
1717

1818
public static class ConfigNewCommandHandler
1919
{
20-
private static readonly string snippetsFileUrl = $"https://aka.ms/devproxy/snippets/v{ProxyUtils.GetVersionString(ProxyUtils.ProductVersion)}";
20+
private static readonly string snippetsFileUrl = $"https://aka.ms/devproxy/snippets/v{ProxyUtils.NormalizeVersion(ProxyUtils.ProductVersion)}";
2121
private static readonly string configFileSnippetName = "ConfigFile";
2222

2323
public static async Task CreateConfigFileAsync(string name, ILogger logger)

dev-proxy/PluginLoader.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,16 @@ private PluginConfig PluginConfig
134134
{
135135
if (_pluginConfig == null)
136136
{
137+
var schemaUrl = Configuration.GetValue<string>("$schema");
138+
if (string.IsNullOrWhiteSpace(schemaUrl))
139+
{
140+
_logger.LogDebug("No schema URL found in configuration file, skipping schema version validation");
141+
}
142+
else
143+
{
144+
ProxyUtils.ValidateSchemaVersion(schemaUrl, _logger);
145+
}
146+
137147
_pluginConfig = new PluginConfig();
138148

139149
if (isDiscover)

dev-proxy/UpdateNotification.cs

Lines changed: 1 addition & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ internal static class UpdateNotification
4545
// -1 = latest release is greater
4646
// 0 = versions are equal
4747
// 1 = current version is greater
48-
if (CompareSemVer(currentVersion, latestReleaseVersion) < 0)
48+
if (ProxyUtils.CompareSemVer(currentVersion, latestReleaseVersion) < 0)
4949
{
5050
return latestRelease;
5151
}
@@ -60,76 +60,6 @@ internal static class UpdateNotification
6060
}
6161
}
6262

63-
/// <summary>
64-
/// Compares two semantic versions strings.
65-
/// </summary>
66-
/// <param name="a">ver1</param>
67-
/// <param name="b">ver2</param>
68-
/// <returns>Returns 0 if the versions are equal, -1 if a is less than b, and 1 if a is greater than b.</returns>
69-
private static int CompareSemVer(string? a, string? b)
70-
{
71-
if (a == null && b == null)
72-
{
73-
return 0;
74-
}
75-
else if (a == null)
76-
{
77-
return -1;
78-
}
79-
else if (b == null)
80-
{
81-
return 1;
82-
}
83-
84-
if (a.StartsWith('v'))
85-
{
86-
a = a[1..];
87-
}
88-
if (b.StartsWith('v'))
89-
{
90-
b = b[1..];
91-
}
92-
93-
var aParts = a.Split('-');
94-
var bParts = b.Split('-');
95-
96-
var aVersion = new Version(aParts[0]);
97-
var bVersion = new Version(bParts[0]);
98-
99-
var compare = aVersion.CompareTo(bVersion);
100-
if (compare != 0)
101-
{
102-
// if the versions are different, return the comparison result
103-
return compare;
104-
}
105-
106-
// if the versions are the same, compare the prerelease tags
107-
if (aParts.Length == 1 && bParts.Length == 1)
108-
{
109-
// if both versions are stable, they are equal
110-
return 0;
111-
}
112-
else if (aParts.Length == 1)
113-
{
114-
// if a is stable and b is not, a is greater
115-
return 1;
116-
}
117-
else if (bParts.Length == 1)
118-
{
119-
// if b is stable and a is not, b is greater
120-
return -1;
121-
}
122-
else if (aParts[1] == bParts[1])
123-
{
124-
// if both versions are prerelease and the tags are the same, they are equal
125-
return 0;
126-
}
127-
else {
128-
// if both versions are prerelease, b is greater
129-
return -1;
130-
}
131-
}
132-
13363
private static async Task<ReleaseInfo?> GetLatestReleaseAsync(ReleaseType releaseType)
13464
{
13565
var http = new HttpClient();

0 commit comments

Comments
 (0)