-
Notifications
You must be signed in to change notification settings - Fork 116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Document mocking capabilities for v4 Helm Chart #3183
Labels
area/helm
customer/lighthouse
Lighthouse customer bugs and enhancements
kind/enhancement
Improvements or new features
Milestone
Comments
blampe
added
area/helm
customer/lighthouse
Lighthouse customer bugs and enhancements
kind/enhancement
Improvements or new features
labels
Aug 21, 2024
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. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Labels
area/helm
customer/lighthouse
Lighthouse customer bugs and enhancements
kind/enhancement
Improvements or new features
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.
The text was updated successfully, but these errors were encountered: