Skip to content

Commit c0bf975

Browse files
authored
Provider support for dotnet (#76)
This adds a new namespace to the dotnet sdk "Experimental.Provider" with classes to support the writing of a native dotnet provider. The Provider class will internally deal with our grpc protocol and call into virtual methods given by the user which use only dotnet native domain models. It is a bit sad all this is manually written, I feel like there _might_ be a good way to codegen this from the protobuf specs such that if they ever change this file could stay in sync. Also the PropertyValue Marshal/Unmarshal duplicates a lot of the existing serialisation logic, and I think that logic could be rewritten as a layer ontop of this (that is we'd have "objects -> PropertyValues" and "PropetyValues -> grpc" rather than what we currently have which is "objects -> grpc" and now "PropertyValues -> grpc")
1 parent 89f7f70 commit c0bf975

24 files changed

+2655
-511
lines changed

CHANGELOG_PENDING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
### Improvements
22

3+
- [sdk] Add experimental support for writing custom resource providers. This is a preview release, code
4+
documentation and test coverage is known to be minimal, and all APIs are subject to change. However it is
5+
complete enough to try out, and we hope to get feedback on the interface to refine and stabilize this
6+
shortly.
7+
[#76](https://github.com/pulumi/pulumi-dotnet/pull/76)
8+
39
### Bug Fixes

integration_tests/integration_dotnet_smoke_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func TestConstructDotnet(t *testing.T) {
6363
expectedResourceCount := 8
6464

6565
localProviders := []integration.LocalDependency{
66-
{Package: "testprovider", Path: buildTestProvider(t, "testprovider")},
66+
{Package: "testprovider", Path: "testprovider"},
6767
{Package: "testcomponent", Path: filepath.Join(testDir, componentDir)},
6868
}
6969

integration_tests/integration_dotnet_test.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ func TestConstructPlainDotnet(t *testing.T) {
326326
expectedResourceCount := 8
327327

328328
localProviders := []integration.LocalDependency{
329-
{Package: "testprovider", Path: buildTestProvider(t, "testprovider")},
329+
{Package: "testprovider", Path: "testprovider"},
330330
{Package: "testcomponent", Path: filepath.Join(testDir, componentDir)},
331331
}
332332

@@ -481,3 +481,18 @@ func TestSlnMultipleNested(t *testing.T) {
481481
ExtraRuntimeValidation: validation,
482482
})
483483
}
484+
485+
func TestProvider(t *testing.T) {
486+
testDotnetProgram(t, &integration.ProgramTestOptions{
487+
Dir: filepath.Join("provider"),
488+
LocalProviders: []integration.LocalDependency{{Package: "testprovider", Path: "testprovider"}},
489+
Verbose: true,
490+
DebugLogLevel: 10,
491+
ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) {
492+
assert.NotNil(t, stack.Outputs)
493+
assert.Equal(t, float64(42), stack.Outputs["echoA"])
494+
assert.Equal(t, "hello", stack.Outputs["echoB"])
495+
assert.Equal(t, []interface{}{float64(1), "goodbye", true}, stack.Outputs["echoC"])
496+
},
497+
})
498+
}

integration_tests/integration_util_test.go

Lines changed: 4 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@ import (
2222
"bytes"
2323
"context"
2424
"encoding/json"
25-
"errors"
2625
"fmt"
2726
"io/fs"
2827
"os"
2928
"os/exec"
3029
"path/filepath"
3130
"regexp"
32-
"runtime"
3331
"strings"
3432
"testing"
3533
"time"
@@ -38,8 +36,6 @@ import (
3836

3937
"github.com/pulumi/pulumi/pkg/v3/testing/integration"
4038
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
41-
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
42-
"github.com/pulumi/pulumi/sdk/v3/go/common/util/fsutil"
4339
"github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil"
4440
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
4541
"github.com/stretchr/testify/assert"
@@ -230,7 +226,7 @@ func testConstructUnknown(t *testing.T, lang string) {
230226
componentDir := "testcomponent-go"
231227

232228
localProviders := []integration.LocalDependency{
233-
{Package: "testprovider", Path: buildTestProvider(t, "testprovider")},
229+
{Package: "testprovider", Path: "testprovider"},
234230
{Package: "testcomponent", Path: filepath.Join(testDir, componentDir)},
235231
}
236232

@@ -252,7 +248,7 @@ func testConstructMethodsUnknown(t *testing.T, lang string) {
252248
componentDir := "testcomponent-go"
253249

254250
localProviders := []integration.LocalDependency{
255-
{Package: "testprovider", Path: buildTestProvider(t, "testprovider")},
251+
{Package: "testprovider", Path: "testprovider"},
256252
{Package: "testcomponent", Path: filepath.Join(testDir, componentDir)},
257253
}
258254

@@ -268,73 +264,13 @@ func testConstructMethodsUnknown(t *testing.T, lang string) {
268264
})
269265
}
270266

271-
func buildTestProvider(t *testing.T, providerDir string) string {
272-
fn := func() {
273-
providerName := "pulumi-resource-testprovider"
274-
if runtime.GOOS == "windows" {
275-
providerName += ".exe"
276-
}
277-
278-
_, err := os.Stat(filepath.Join(providerDir, providerName))
279-
if err == nil {
280-
return
281-
} else if errors.Is(err, os.ErrNotExist) {
282-
// Not built yet, continue.
283-
} else {
284-
t.Fatalf("Unexpected error building test provider: %v", err)
285-
}
286-
287-
cmd := exec.Command("go", "build", "-o", providerName)
288-
cmd.Dir = providerDir
289-
output, err := cmd.CombinedOutput()
290-
if err != nil {
291-
contract.AssertNoErrorf(err, "failed to run setup script: %v", string(output))
292-
}
293-
}
294-
lockfile := filepath.Join(providerDir, ".lock")
295-
timeout := 10 * time.Minute
296-
synchronouslyDo(t, lockfile, timeout, fn)
297-
298-
// Allows us to drop this in in places where providerDir was used:
299-
return providerDir
300-
}
301-
302-
func synchronouslyDo(t *testing.T, lockfile string, timeout time.Duration, fn func()) {
303-
mutex := fsutil.NewFileMutex(lockfile)
304-
defer func() {
305-
assert.NoError(t, mutex.Unlock())
306-
}()
307-
308-
lockWait := make(chan struct{}, 1)
309-
go func() {
310-
for {
311-
if err := mutex.Lock(); err != nil {
312-
time.Sleep(1 * time.Second)
313-
continue
314-
} else {
315-
break
316-
}
317-
}
318-
319-
fn()
320-
lockWait <- struct{}{}
321-
}()
322-
323-
select {
324-
case <-time.After(timeout):
325-
t.Fatalf("timed out waiting for lock on %s", lockfile)
326-
case <-lockWait:
327-
// waited for fn, success.
328-
}
329-
}
330-
331267
// Test methods that create resources.
332268
func testConstructMethodsResources(t *testing.T, lang string) {
333269
const testDir = "construct_component_methods_resources"
334270
componentDir := "testcomponent-go"
335271

336272
localProviders := []integration.LocalDependency{
337-
{Package: "testprovider", Path: buildTestProvider(t, "testprovider")},
273+
{Package: "testprovider", Path: "testprovider"},
338274
{Package: "testcomponent", Path: filepath.Join(testDir, componentDir)},
339275
}
340276

@@ -392,7 +328,7 @@ func testConstructOutputValues(t *testing.T, lang string, dependencies ...string
392328
componentDir := "testcomponent-go"
393329

394330
localProviders := []integration.LocalDependency{
395-
{Package: "testprovider", Path: buildTestProvider(t, "testprovider")},
331+
{Package: "testprovider", Path: "testprovider"},
396332
{Package: "testcomponent", Path: filepath.Join(testDir, componentDir)},
397333
}
398334

0 commit comments

Comments
 (0)