Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion SampleArtifacts/apis/basic-api/policy.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<policies>
<inbound>
<set-backend-service base-url="{{intranet-proxy-url}}" />
<set-backend-service backend-id="backend1" />
<set-header name="X-Api" exists-action="override">
<value>basic-api</value>
</set-header>
Expand Down
6 changes: 6 additions & 0 deletions src/common/Resource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,12 @@ public static ImmutableHashSet<IResource> ListDependencies(this IResource resour
{
list.Add(PolicyFragmentResource.Instance);
}

// Policies can reference backends
var isWorkspacePolicy = resource.GetTraversalPredecessorHierarchy()
.Contains(WorkspaceResource.Instance);

list.Add(isWorkspacePolicy ? WorkspaceBackendResource.Instance : BackendResource.Instance);
}

return [.. list];
Expand Down
3 changes: 2 additions & 1 deletion src/integration.tests/ApiOperationPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ private static Gen<string> GenerateContent(IEnumerable<ITestModel> models)
{
var namedValues = models.OfType<NamedValueModel>();
var fragments = models.OfType<PolicyFragmentModel>();
var backends = models.OfType<BackendModel>();

return from inboundSnippet in PolicyModule.GenerateInboundSnippet(namedValues, fragments)
return from inboundSnippet in PolicyModule.GenerateInboundSnippet(namedValues, fragments, backends)
from outboundSnippet in PolicyModule.GenerateOutboundSnippet(namedValues, fragments)
select $"""
<policies>
Expand Down
3 changes: 2 additions & 1 deletion src/integration.tests/ApiPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ private static Gen<string> GenerateContent(IEnumerable<ITestModel> models)
{
var namedValues = models.OfType<NamedValueModel>();
var fragments = models.OfType<PolicyFragmentModel>();
var backends = models.OfType<BackendModel>();

return from inboundSnippet in PolicyModule.GenerateInboundSnippet(namedValues, fragments)
return from inboundSnippet in PolicyModule.GenerateInboundSnippet(namedValues, fragments, backends)
from outboundSnippet in PolicyModule.GenerateOutboundSnippet(namedValues, fragments)
select $"""
<policies>
Expand Down
14 changes: 12 additions & 2 deletions src/integration.tests/Common.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,29 @@ private static Gen<string> GenerateWordOrNamedValue(IEnumerable<NamedValueModel>
.Select(model => Gen.Const($$$"""{{{{{model.DisplayName}}}}}"""))
.Append(Generator.AlphanumericWord)]);

public static Gen<string> GenerateInboundSnippet(IEnumerable<NamedValueModel> namedValues, IEnumerable<PolicyFragmentModel> fragments) =>
public static Gen<string> GenerateInboundSnippet(IEnumerable<NamedValueModel> namedValues, IEnumerable<PolicyFragmentModel> fragments, IEnumerable<BackendModel> backends) =>
from snippetGens in Generator.SubSetOf([GenerateSetVariableSnippet(namedValues),
GenerateIpFilterSnippet(),
GenerateFindAndReplaceSnippet(namedValues),
GenerateSetHeaderSnippet(namedValues),
GenerateIncludeFragmentSnippet(fragments)])
GenerateIncludeFragmentSnippet(fragments),
GenerateSetBackendServiceSnippet(backends)])
from snippets in Generator.Traverse(snippetGens, gen => gen)
select $"""
<inbound>
{string.Join(Environment.NewLine, snippets)}
</inbound>
""";

private static Gen<string> GenerateSetBackendServiceSnippet(IEnumerable<BackendModel> backends) =>
backends.ToImmutableArray() switch
{
[] => from url in Generator.AbsoluteUri
select $"""<set-backend-service base-url="{url}" />""",
var nonEmptyBackends => from backend in Gen.OneOfConst([.. nonEmptyBackends])
select $"""<set-backend-service backend-id="{backend.Key.Name}" />"""
};

private static Gen<string> GenerateIpFilterSnippet() =>
from last3 in Gen.Int[0, 255].Array[3]
let ips = last3.Prepend(10)
Expand Down
5 changes: 3 additions & 2 deletions src/integration.tests/ProductPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ private static Gen<string> GenerateContent(IEnumerable<ITestModel> models)
{
var namedValues = models.OfType<NamedValueModel>();
var fragments = models.OfType<PolicyFragmentModel>();
var backends = models.OfType<BackendModel>();

return from inboundSnippet in PolicyModule.GenerateInboundSnippet(namedValues, fragments)
return from inboundSnippet in PolicyModule.GenerateInboundSnippet(namedValues, fragments, backends)
from outboundSnippet in PolicyModule.GenerateOutboundSnippet(namedValues, fragments)
select $"""
<policies>
Expand Down Expand Up @@ -87,4 +88,4 @@ select model with

public static Gen<ImmutableHashSet<ProductPolicyModel>> GenerateNextState(IEnumerable<ITestModel> previousModels, IEnumerable<ITestModel> accumulatedNextModels) =>
GenerateSet(accumulatedNextModels);
}
}
3 changes: 2 additions & 1 deletion src/integration.tests/ServicePolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ private static Gen<string> GenerateContent(IEnumerable<ITestModel> models)
{
var namedValues = models.OfType<NamedValueModel>();
var fragments = models.OfType<PolicyFragmentModel>();
var backends = models.OfType<BackendModel>();

return from inboundSnippet in PolicyModule.GenerateInboundSnippet(namedValues, fragments)
return from inboundSnippet in PolicyModule.GenerateInboundSnippet(namedValues, fragments, backends)
from outboundSnippet in PolicyModule.GenerateOutboundSnippet(namedValues, fragments)
select $"""
<policies>
Expand Down
94 changes: 93 additions & 1 deletion src/publisher.tests/Relationships.cs
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,98 @@ await Assert.That(relationships.Predecessors[policyKey])
});
}

[Test]
public async Task Returns_policy_to_backend_relationships()
{
var gen = from fixture in Fixture.Generate()
from backendKey in Generator.GenerateResourceKey(resource => resource is BackendResource)
from policyKey in Generator.GenerateResourceKey(resource => resource is IPolicyResource
and not PolicyFragmentResource
and not WorkspacePolicyFragmentResource)
let policyContent = $"<policies><inbound><set-backend-service backend-id=\"{backendKey.Name}\" /><base /></inbound></policies>"
let resources = RelationshipTestData.ToResourceMap([policyKey, backendKey])
select (backendKey, policyKey, policyContent, resources, Common.NoOpFileOperations, fixture with
{
GetPolicyFileContents = async (resource, name, parents, readFile, cancellationToken) =>
{
await ValueTask.CompletedTask;

var key = ResourceKey.From(resource, name, parents);

return key == policyKey
? BinaryData.FromString(policyContent)
: Option.None;
}
});

await gen.SampleAsync(async tuple =>
{
// Arrange
var (backendKey, policyKey, policyContent, resources, fileOperations, fixture) = tuple;
var getRelationshipPairs = fixture.Resolve();

// Act
var pairs = await getRelationshipPairs(resources, fileOperations, CancellationToken);
var relationships = Relationships.From(pairs, CancellationToken);

// Assert that the backend is a predecessor of the policy
await Assert.That(relationships.Predecessors[policyKey])
.Contains(backendKey);
});
}

[Test]
public async Task Returns_workspace_policy_to_workspace_backend_relationships()
{
var gen = from fixture in Fixture.Generate()
from workspaceKey in Generator.GenerateResourceKey(resource => resource is WorkspaceResource)
let setWorkspace = new Func<ResourceKey, ResourceKey>(key => key with
{
Parents = ParentChain.From([.. from pair in key.Parents
select pair.Resource is WorkspaceResource
? (WorkspaceResource.Instance, workspaceKey.Name)
: pair])
})
from backendKey in
from backendKey in Generator.GenerateResourceKey(resource => resource is WorkspaceBackendResource)
select setWorkspace(backendKey)
from policyKey in
from policyKey in Generator.GenerateResourceKey(resource => resource is IPolicyResource and not WorkspacePolicyFragmentResource
&& resource.GetTraversalPredecessorHierarchy()
.Contains(WorkspaceResource.Instance))
select setWorkspace(policyKey)
let policyContent = $"<policies><inbound><set-backend-service backend-id=\"{backendKey.Name}\" /><base /></inbound></policies>"
let resources = RelationshipTestData.ToResourceMap([policyKey, backendKey])
select (backendKey, policyKey, policyContent, resources, Common.NoOpFileOperations, fixture with
{
GetPolicyFileContents = async (resource, name, parents, readFile, cancellationToken) =>
{
await ValueTask.CompletedTask;

var key = ResourceKey.From(resource, name, parents);

return key == policyKey
? BinaryData.FromString(policyContent)
: Option.None;
}
});

await gen.SampleAsync(async tuple =>
{
// Arrange
var (backendKey, policyKey, policyContent, resources, fileOperations, fixture) = tuple;
var getRelationshipPairs = fixture.Resolve();

// Act
var pairs = await getRelationshipPairs(resources, fileOperations, CancellationToken);
var relationships = Relationships.From(pairs, CancellationToken);

// Assert that the workspace backend is a predecessor of the policy
await Assert.That(relationships.Predecessors[policyKey])
.Contains(backendKey);
});
}

[Test]
public async Task Returns_a_missing_parent_relationship_for_a_child_resource()
{
Expand Down Expand Up @@ -1294,4 +1386,4 @@ .. relationships.Predecessors.SelectMany(kvp => kvp.Value),
public static ImmutableDictionary<IResource, ImmutableHashSet<ResourceKey>> ToResourceMap(IEnumerable<ResourceKey> keys) =>
[.. keys.GroupBy(key => key.Resource)
.Select(group => KeyValuePair.Create(group.Key, group.ToImmutableHashSet()))];
}
}
46 changes: 44 additions & 2 deletions src/publisher/Relationships.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ private static GetRelationshipPairs ResolveGetRelationshipPairs(IServiceProvider

var namedValueTokenRegex = new Regex("{{\\s*(.*?)\\s*}}", RegexOptions.CultureInvariant);
var includeFragmentRegex = new Regex("<include-fragment\\s+fragment-id=\"([^\"]+)\"\\s*/>", RegexOptions.CultureInvariant);
var policyBackendIdRegex = new Regex("<set-backend-service\\b[^>]*\\bbackend-id\\s*=\\s*[\"']([^\"']+)[\"'][^>]*/?>", RegexOptions.CultureInvariant);

return async (resources, operations, cancellationToken) =>
{
Expand Down Expand Up @@ -352,6 +353,12 @@ await dtoJsonOption.IterTask(async dto =>
policyFragments.Iter(fragment => pairs.Add((fragment, key)), cancellationToken);
}

if (key.Resource is IPolicyResource policyResourceForBackends)
{
var policyBackends = await getPolicyBackends(policyResourceForBackends, key.Name, key.Parents, operations.ReadFile, resources, cancellationToken);
policyBackends.Iter(backend => pairs.Add((backend, key)), cancellationToken);
}

dtoJsonOption.Iter(dtoJson =>
{
var content = dtoJson.ToJsonString();
Expand Down Expand Up @@ -462,6 +469,15 @@ async ValueTask<ImmutableHashSet<ResourceKey>> getPolicyFragments(IPolicyResourc
return getPolicyFragmentsFromContent(contents, parents, resources);
}

async ValueTask<ImmutableHashSet<ResourceKey>> getPolicyBackends(IPolicyResource resource, ResourceName name, ParentChain parents, ReadFile readFile, IDictionary<IResource, ImmutableHashSet<ResourceKey>> resources, CancellationToken cancellationToken)
{
var contentsOption = await getEffectivePolicyContents(resource, name, parents, readFile, cancellationToken);

var contents = contentsOption.IfNoneNull() ?? string.Empty;

return getPolicyBackendsFromContent(contents, parents, resources);
}

ImmutableHashSet<ResourceKey> getNamedValuesFromContent(string content, ParentChain parents)
{
if (string.IsNullOrWhiteSpace(content))
Expand Down Expand Up @@ -505,12 +521,38 @@ ImmutableHashSet<ResourceKey> getPolicyFragmentsFromContent(string content, Pare
Option<ResourceKey> findPolicyFragment(ResourceName name, Option<(IResource, ResourceName)> workspaceOption, IDictionary<IResource, ImmutableHashSet<ResourceKey>> resources) =>
workspaceOption.Bind(workspace => from fragments in resources.Find(WorkspacePolicyFragmentResource.Instance)
from fragment in fragments.Head(key => key.Name == name
&& key.Parents.Any(parent => parent == workspace))
&& key.Parents.Any(parent => parent == workspace))
select fragment)
.IfNone(() => from fragments in resources.Find(PolicyFragmentResource.Instance)
from fragment in fragments.Head(key => key.Name == name)
select fragment);

ImmutableHashSet<ResourceKey> getPolicyBackendsFromContent(string content, ParentChain parents, IDictionary<IResource, ImmutableHashSet<ResourceKey>> resources)
{
if (string.IsNullOrWhiteSpace(content))
{
return [];
}

var workspaceOption = parents.Head(tuple => tuple.Resource is WorkspaceResource);

var referencedBackends = policyBackendIdRegex.Matches(content)
.Select(match => match.Groups[1].Value.Trim())
.Choose(nameString => ResourceName.From(nameString).ToOption())
.Choose(name => findBackend(name, workspaceOption, resources));

return [.. referencedBackends];
}

Option<ResourceKey> findBackend(ResourceName name, Option<(IResource, ResourceName)> workspaceOption, IDictionary<IResource, ImmutableHashSet<ResourceKey>> resources) =>
workspaceOption.Bind(workspace => from backends in resources.Find(WorkspaceBackendResource.Instance)
from backend in backends.Head(key => key.Name == name
&& key.Parents.Any(parent => parent == workspace))
select backend)
.IfNone(() => from backends in resources.Find(BackendResource.Instance)
from backend in backends.Head(key => key.Name == name)
select backend);

ResourceKey getParent(IChildResource resource, ResourceName name, ParentChain parents) =>
parents.LastOrDefault() switch
{
Expand Down Expand Up @@ -727,4 +769,4 @@ internal static IsValidationStrict ResolveIsValidationStrict(IServiceProvider pr
: throw new InvalidOperationException($"'STRICT_VALIDATION' must be 'true' or 'false'."))
.IfNone(() => false);
}
}
}