Closed vipentti closed 3 years ago
Upon testing, this problem also happens with YamlDecodeResult
https://github.com/pulumi/pulumi-kubernetes/blob/80d4c16c2240d77ca575273a9c167c739e7681a3/sdk/dotnet/Yaml/Invokes.cs#L43
Do you have a unit test to repro? I was attempting to repro with pulumi up
but it needs a K8S cluster. I will have one later in the day as I'm working on something adjacent and can try that then. But if the unit tests repros without a cluster that's even better.
Per our conversation in linked PR, I'd like to poke around here to see where the invalid ImmutableArray originates from in the SDK and perhaps we can come up with a fix that makes those go away.
@t0yv0 I've created a small repo which displays the problem https://github.com/vipentti/pulumi-helm-chart-unittest-issue
I have a workaround for your unit test that makes it pass for me:
https://github.com/vipentti/pulumi-helm-chart-unittest-issue/pull/1
At a high level, what I think is happening, the Mock implementation you were using was violating some assumptions of how kubernetes:helm:template
resource responds. I could only find this by perusing the provider.go source code in pulumi-kubernetes.. but it seems like a "result" property is populated with output to decodeYaml, which has this:
// decodeYaml parses a YAML string, and then returns a slice of untyped structs that can be marshalled into
// Pulumi RPC calls. If a default namespace is specified, set that on the relevant decoded objects.
So mocking the helm resources deeply and accurately sounds like something off the beaten path. I am a bit new to Pulumi and will ask around if there is a better way. Please let me know if my PR unblocks you to get to something sufficiently useful, or you run into more roadblocks trying to mock the Helm provider?
Considering changes to pulumi-kubernetes. Technically with the original shallow mock, this is what's happening:
V3/Chart.cs calls Invokes.HelmTemplate(..)
which calls Deployement.Instance.InvokeAsync<HelmTemplateResult>("kubernetes:helm:template", .., ..)
which calls internal interface IDeployment { Task<T> InvokeAsync(..) }
which returns null
which gets wrapped in HelmTemplateResult and continues downstream
So we could consider HelmTemplateResult normalizing null to empty ImmutableArray. It is clear though this only affects the scenario of testing with incomplete mocks, and I am wondering if this would actually unblock some useful testing?
Wow! Thank you @t0yv0!
Your PR indeed does unblock my testing and in addition to that it actually gives me the opportunity to actually mock Kubernetes resources that would be created by the template, which I had to do since rest of the code then extracts the loadBalancer IP from the service that is created.
Basically what I did was:
First in my unit test I define the service that I want the template to return in YAML, which I then deserialize into a custom class, but anything that can be turned to ImmutableDictionary<string, object>
probably works.
string ipAddress = "192.0.0.1";
string serviceName = "stack-http-ngx-ingress-nginx-controller";
string serviceYaml = $@"
kind: Service
apiVersion: v1
metadata:
name: {serviceName}
namespace: kube-system
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
spec:
externalTrafficPolicy: Local
type: LoadBalancer
selector:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
ports:
- name: http
port: 80
targetPort: http
- name: https
port: 443
targetPort: https
status:
loadBalancer:
ingress:
- ip: {ipAddress}";
// Read the YAML contents into a custom class
var serviceObject = YamlUtils.ReadYamlToMap(serviceYaml);
The serviceObject is then added to to the template results inside CallAsync
// Inside CallAsync
// Workaround for NRE with helm templates
if (token == "kubernetes:helm:template")
{
outputs.Add("result", new object[]
{
// Turn the serviceObject into ImmutableDictionary<string, object>
serviceObject.ToImmutable(),
}.ToImmutableArray());
}
Where the Helm.V3.Chart is created, I am able to get the IP / hostname of the loadbalancer defined by the service.
// nginxIngress is the Helm.V3.Chart
// get the IP or hostname of the LoadBalancer from the Kubernetes Service
var service = nginxIngress.GetResource<Pulumi.Kubernetes.Core.V1.Service>(
// Name is derived from the nginxIngress chart name
$"{name}-ingress-nginx-controller"
, namespaceName: "kube-system"
);
// Output
LoadBalancerEndpoint = service
.Apply(svc => svc.Status)
.Apply(sts => sts.LoadBalancer.Ingress[0].GetHostnameOrIp());
Inside my unit test I can then get the provided IP address from the service and verify it is the same
var ingressStack = resources.OfType<IngressStack>().Single();
var loadBalancerEndpoint = await ingressStack.LoadBalancerEndpoint.GetValueAsync();
loadBalancerEndpoint.Should().Be(ipAddress);
Ah, very nice!
My colleague @lblackstone had a pro tip here to consider local charts: https://www.pulumi.com/docs/reference/pkg/kubernetes/helm/v3/chart/#local-chart-directory
That sounds to be another way to get a local yaml in the picture. Would that help at all?
Your code makes sense and I see the value but it worries me a little we are using the magic "result" property that seems to be an implementation detail that can change over time.
Unfortunately local charts also have the same problem when using them inside unit tests, because the InvokeAsync
ends up calling the mocked IMocks.CallAsync
instead of actually invoking Helm.
I agree that the result
property is not ideal since it is not particularly clear where it comes from and why it is required. But for now it at least gives me the ability to write unit tests for stacks with Helm charts, even if the resources actually created by the chart are not available.
Perhaps at some point some higher level abstractions can be provided for unit testing, which wrap the current IMocks.CallAsync
and IMocks.NewResourceAsync
since both of them are quite "low-level" in that they require knowledge of Pulumi internals a bit.
For example using a StackReference
in a stack which is being unit tested require adding specific properties to the state returned from IMocks.NewResourceAsync
.
e.g.
// StackReferences need both secretOutputNames and outputs configured
if (type.Contains("StackReference"))
{
// These names match the names in the Output-attribute for each property
outputs.Add("secretOutputNames", Array.Empty<string>().ToImmutableArray());
outputs.Add("outputs", new Dictionary<string, object>()
{
{ "KubeConfig", "KubeConfig" },
{ "ClusterName", "ClusterName" },
}.ToImmutableDictionary());
}
Perhaps at some point some higher level abstractions can be provided for unit testing
Right! Let me circle back here on what's available after I sync up with @mikhailshilkov tomorrow.
I don't have too many good news yet around the testing facilities at the moment. It sounds like selective per-resource mocking by delegating to the engine a bit difficult due to interaction with engine state. And there are no helpers currently provided for typed access to what you might want to return or consume in IMocks.
I'll be sure to follow up on this.
For the moment, if you don't mind, I will close the issue as we maybe have worked around sufficiently in https://github.com/t0yv0/pulumi-helm-chart-unittest-issue
I'm trying to write some unit tests in C# for my stacks and I'm having trouble with a stack which contains a
Helm.V3.Chart
. Running unit tests which utilize charts currently throw System.NullReferenceException.Expected behavior
Unit tests execute without exceptions.
Current behavior
The following exception is thrown when executing a unit test with a stack containing
Helm.V3.Chart
.I did some digging and found that the trouble is caused by the implicit conversion operator invoked at https://github.com/pulumi/pulumi-kubernetes/blob/80d4c16c2240d77ca575273a9c167c739e7681a3/sdk/dotnet/Helm/V3/Chart.cs#L365
The actual problem seems to be that the
ImmutableArray
-parameter https://github.com/pulumi/pulumi-kubernetes/blob/80d4c16c2240d77ca575273a9c167c739e7681a3/sdk/dotnet/Helm/V3/Invokes.cs#L46 given toHelmTemplateResult
is uninitialized when running the tests. Since the ImmutableArray is uninitialized, theSystem.NullReferenceException
is thrown when trying to do the implicit conversion.Steps to reproduce
The problem can be produced by having the following stack:
And invoking it with
Deployment.TestAsync
in a C# test project.Context (Environment)
Trying to write unit tests for stacks which contain Helm-charts.
Proposed fix
Ensure that the
HelmTemplateResult.Result
array is always initialized.For example by adding the following toHelmTemplateResult
constructor https://github.com/pulumi/pulumi-kubernetes/blob/80d4c16c2240d77ca575273a9c167c739e7681a3/sdk/dotnet/Helm/V3/Invokes.cs#L48If this seems like a reasonable solution, I can submit a PR.NOTE I am not sure if similiar problem occurs anywhere else with
ImmutableArray
UPDATE The implicit conversion fix is in pulumi/pulumi#6544