Skip to content
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

Open
blampe opened this issue Aug 21, 2024 · 3 comments · May be fixed by pulumi/examples#1721
Open

Document mocking capabilities for v4 Helm Chart #3183

blampe opened this issue Aug 21, 2024 · 3 comments · May be fixed by pulumi/examples#1721
Assignees
Labels
area/helm customer/lighthouse Lighthouse customer bugs and enhancements kind/enhancement Improvements or new features
Milestone

Comments

@blampe
Copy link
Contributor

blampe commented Aug 21, 2024

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.

@blampe blampe added area/helm customer/lighthouse Lighthouse customer bugs and enhancements kind/enhancement Improvements or new features labels Aug 21, 2024
@blampe blampe added the awaiting-feedback Blocked on input from the author label Oct 1, 2024
@blampe
Copy link
Contributor Author

blampe commented Oct 1, 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)),
	)
}

@mjeffryes mjeffryes added this to the 0.111 milestone Oct 2, 2024
@mjeffryes mjeffryes assigned EronWright and unassigned blampe Oct 28, 2024
@mjeffryes mjeffryes modified the milestones: 0.111, 0.112 Oct 30, 2024
@EronWright
Copy link
Contributor

EronWright commented Nov 5, 2024

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")
}

@EronWright EronWright removed the awaiting-feedback Blocked on input from the author label Nov 5, 2024
@EronWright
Copy link
Contributor

I posted a PR to add an example to the examples repository.

@mjeffryes mjeffryes modified the milestones: 0.112, 0.113 Nov 13, 2024
@mjeffryes mjeffryes modified the milestones: 0.113, 0.114 Dec 3, 2024
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
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants