pulumi-kubernetes
pulumi-kubernetes copied to clipboard
Document mocking capabilities for v4 Helm Chart
Users were previously able to inject resources into a v3 Helm Chart via the Call method, but the v4 Chart no longer invokes this client-side.
Let's put together an example of how to mock the v4 Chart resource.
If this works for the customer we can update docs to reflect it.
❯ cat main.go
package main
import (
helmv4 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v4"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func createChart(ctx *pulumi.Context) (*helmv4.Chart, error) {
return helmv4.NewChart(ctx, "nginx", &helmv4.ChartArgs{
Chart: pulumi.String("oci://registry-1.docker.io/bitnamicharts/nginx"),
Version: pulumi.String("16.0.7"),
})
}
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
chart, err := createChart(ctx)
if err != nil {
return err
}
ctx.Export("resources", chart.Resources)
return nil
})
}
❯ cat main_test.go
package main
import (
"fmt"
"testing"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/stretchr/testify/assert"
)
type HelmMocks []map[string]interface{}
func (m HelmMocks) NewResource(args pulumi.MockResourceArgs) (string, resource.PropertyMap, error) {
// Copy our inputs.
outputs := args.Inputs
// Convert our map into a PropertyValue Pulumi understands.
values := []resource.PropertyValue{}
for _, v := range m {
values = append(values, resource.NewObjectProperty(resource.NewPropertyMapFromMap(v)))
}
// Add our resources to the chart's output.
outputs["resources"] = resource.NewArrayProperty(values)
return args.Name + "_id", outputs, nil
}
func (HelmMocks) Call(args pulumi.MockCallArgs) (resource.PropertyMap, error) {
return nil, fmt.Errorf("not implemented")
}
func TestHelm(t *testing.T) {
mockResources := []map[string]any{
{"foo": "bar"},
}
pulumi.Run(
func(ctx *pulumi.Context) error {
chart, err := createChart(ctx)
if err != nil {
return err
}
pulumi.All(chart.Resources).ApplyT(func(all []any) error {
resources := all[0].([]any)
assert.Len(t, resources, 1)
return nil
})
return nil
},
pulumi.WithMocks("project", "stack", HelmMocks(mockResources)),
)
}
Here's a revised example that shows how to provide fake children, to be able to exercise more of a complex resource graph. In this example, the code to be tested is encapsulated as a component resource.
// main.go
package main
import (
"context"
"errors"
corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
helmv4 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v4"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
type NginxComponentArgs struct {
ServiceType pulumi.StringInput
}
// NginxComponent is a component that encapsulates an Nginx chart.
type nginxComponent struct {
pulumi.ResourceState
//Output properties of the component
Chart *helmv4.Chart `pulumi:"chart"`
IngressIp pulumi.StringPtrOutput `pulumi:"ingressIp"`
}
func NewNginxComponent(ctx *pulumi.Context, name string, args *NginxComponentArgs, opts ...pulumi.ResourceOption) (*nginxComponent, error) {
component := &nginxComponent{}
err := ctx.RegisterComponentResource("example:NginxComponent", name, component, opts...)
if err != nil {
return nil, err
}
chart, err := helmv4.NewChart(ctx, name, &helmv4.ChartArgs{
Chart: pulumi.String("oci://registry-1.docker.io/bitnamicharts/nginx"),
Version: pulumi.String("16.0.7"),
Values: pulumi.Map{
"serviceType": args.ServiceType,
},
}, pulumi.Parent(component))
if err != nil {
return nil, err
}
component.Chart = chart
ingressIp := chart.Resources.ApplyTWithContext(ctx.Context(), func(ctx context.Context, resources []any) (pulumi.StringPtrOutput, error) {
for _, r := range resources {
switch r := r.(type) {
case *corev1.Service:
return r.Status.LoadBalancer().Ingress().Index(pulumi.Int(0)).Ip(), nil
}
}
return pulumi.StringPtrOutput{}, errors.New("service not found")
}).(pulumi.StringPtrOutput)
component.IngressIp = ingressIp
return component, err
}
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
nginx, err := NewNginxComponent(ctx, "nginx", &NginxComponentArgs{
ServiceType: pulumi.String("LoadBalancer"),
})
if err != nil {
return err
}
ctx.Export("ingressIp", nginx.IngressIp)
return nil
})
}
// main_test.go
package main
import (
"context"
"fmt"
"testing"
corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/internals"
"github.com/stretchr/testify/assert"
)
type HelmMocks struct {
Context *pulumi.Context
}
func (m HelmMocks) NewResource(args pulumi.MockResourceArgs) (string, resource.PropertyMap, error) {
outputs := args.Inputs
switch {
case args.TypeToken == "kubernetes:helm.sh/v4:Chart" && args.RegisterRPC.Remote:
// mock the Chart component resource by registering some child resources
chart := &pulumi.ResourceState{}
err := m.Context.RegisterComponentResource("kubernetes:helm.sh/v4:Chart", "nginx", chart)
if err != nil {
return "", nil, err
}
values := args.Inputs["values"].ObjectValue()
svc, err := corev1.NewService(m.Context, "foo:default/nginx", &corev1.ServiceArgs{
Metadata: &metav1.ObjectMetaArgs{
Name: pulumi.String("nginx"),
Namespace: pulumi.String("default"),
},
Spec: &corev1.ServiceSpecArgs{
Type: pulumi.StringPtr(values["serviceType"].StringValue()),
},
}, pulumi.Parent(chart))
if err != nil {
return "", nil, err
}
outputs["resources"] = resource.NewArrayProperty([]resource.PropertyValue{
makeResourceReference(m.Context.Context(), svc),
})
return "", outputs, nil
case args.TypeToken == "kubernetes:core/v1:Service":
// mock the Service resource by returning a fake ingress IP address
outputs["status"] = resource.NewObjectProperty(resource.NewPropertyMapFromMap(map[string]interface{}{
"loadBalancer": map[string]interface{}{
"ingress": []map[string]interface{}{
{"ip": "127.0.0.1"},
},
},
}))
return "default/nginx", outputs, nil
default:
return args.ID, args.Inputs, nil
}
}
func (HelmMocks) Call(args pulumi.MockCallArgs) (resource.PropertyMap, error) {
return nil, fmt.Errorf("not implemented")
}
func makeResourceReference(ctx context.Context, v pulumi.Resource) resource.PropertyValue {
urn, err := internals.UnsafeAwaitOutput(ctx, v.URN())
contract.AssertNoErrorf(err, "Failed to await URN: %v", err)
contract.Assertf(urn.Known, "URN must be known")
contract.Assertf(!urn.Secret, "URN must not be secret")
if custom, ok := v.(pulumi.CustomResource); ok {
id, err := internals.UnsafeAwaitOutput(ctx, custom.ID())
contract.AssertNoErrorf(err, "Failed to await ID: %v", err)
contract.Assertf(!id.Secret, "CustomResource must not have a secret ID")
return resource.MakeCustomResourceReference(resource.URN(urn.Value.(pulumi.URN)), resource.ID(id.Value.(pulumi.ID)), "")
}
return resource.MakeComponentResourceReference(resource.URN(urn.Value.(pulumi.URN)), "")
}
func TestHelm(t *testing.T) {
mocks := &HelmMocks{}
err := pulumi.RunErr(
func(ctx *pulumi.Context) error {
mocks.Context = ctx
await := func(v pulumi.Output) any {
res, err := internals.UnsafeAwaitOutput(ctx.Context(), v)
contract.AssertNoErrorf(err, "failed to await value: %v", err)
return res.Value
}
// execute the code that is to be tested
nginx, err := NewNginxComponent(ctx, "foo", &NginxComponentArgs{
ServiceType: pulumi.String("LoadBalancer"),
})
if err != nil {
return err
}
// validate the chart and resources
assert.NotNil(t, nginx.Chart, "chart is nil")
resources := await(nginx.Chart.Resources).([]any)
assert.Len(t, resources, 1, "chart has wrong number of children")
svc := resources[0].(*corev1.Service)
assert.NotNil(t, svc, "service resource is nil")
svcType := await(svc.Spec.Type()).(*string)
assert.NotNil(t, svc, "service type is nil")
assert.Equal(t, "LoadBalancer", *svcType, "service type has unexpected value")
// validate the ingressIp
ingressIp := await(nginx.IngressIp).(*string)
assert.NotNil(t, ingressIp, "ingressIp is nil")
assert.Equal(t, "127.0.0.1", *ingressIp, "ingressIp has unexpected value")
return nil
},
pulumi.WithMocks("project", "stack", mocks),
)
assert.NoError(t, err, "expected run to succeed")
}
I posted a PR to add an example to the examples repository.