diff --git a/.gitignore b/.gitignore index 13f711024..8210ca214 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ **/node_modules/ **/bin **/.vscode/ +**/.vs/ coverage.cov *.coverprofile diff --git a/CHANGELOG.md b/CHANGELOG.md index a0d8c5d66..262cd09b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ CHANGELOG ## HEAD (Unreleased) +- Adds a **preview** of .NET support for Pulumi. This code is an preview state and is subject + to change at any point. + ## 1.4.0 (2019-10-24) - `FileAsset` in the Python SDK now accepts anything implementing `os.PathLike` in addition to `str`. diff --git a/Makefile b/Makefile index 2873c60ef..c58c0c0d5 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PROJECT_NAME := Pulumi SDK -SUB_PROJECTS := sdk/nodejs sdk/python sdk/go +SUB_PROJECTS := sdk/dotnet sdk/nodejs sdk/python sdk/go include build/common.mk PROJECT := github.com/pulumi/pulumi diff --git a/pkg/testing/integration/program.go b/pkg/testing/integration/program.go index ada5eb797..3817d3638 100644 --- a/pkg/testing/integration/program.go +++ b/pkg/testing/integration/program.go @@ -57,6 +57,7 @@ import ( const PythonRuntime = "python" const NodeJSRuntime = "nodejs" const GoRuntime = "go" +const DotNetRuntime = "dotnet" // RuntimeValidationStackInfo contains details related to the stack that runtime validation logic may want to use. type RuntimeValidationStackInfo struct { @@ -219,6 +220,8 @@ type ProgramTestOptions struct { GoBin string // PipenvBin is a location of a `pipenv` executable to run. Taken from the $PATH if missing. PipenvBin string + // DotNetBin is a location of a `dotnet` executable to be run. Taken from the $PATH if missing. + DotNetBin string // Additional environment variables to pass for each command we run. Env []string @@ -575,6 +578,7 @@ type programTester struct { yarnBin string // the `yarn` binary we are using. goBin string // the `go` binary we are using. pipenvBin string // The `pipenv` binary we are using. + dotNetBin string // the `dotnet` binary we are using. eventLog string // The path to the event log for this test. } @@ -604,6 +608,10 @@ func (pt *programTester) getPipenvBin() (string, error) { return getCmdBin(&pt.pipenvBin, "pipenv", pt.opts.PipenvBin) } +func (pt *programTester) getDotNetBin() (string, error) { + return getCmdBin(&pt.dotNetBin, "dotnet", pt.opts.DotNetBin) +} + func (pt *programTester) pulumiCmd(args []string) ([]string, error) { bin, err := pt.getBin() if err != nil { @@ -1373,6 +1381,8 @@ func (pt *programTester) prepareProject(projinfo *engine.Projinfo) error { return pt.preparePythonProject(projinfo) case GoRuntime: return pt.prepareGoProject(projinfo) + case DotNetRuntime: + return pt.prepareDotNetProject(projinfo) default: return errors.Errorf("unrecognized project runtime: %s", rt) } @@ -1574,3 +1584,40 @@ func (pt *programTester) prepareGoProject(projinfo *engine.Projinfo) error { outBin := filepath.Join(gopath, "bin", string(projinfo.Proj.Name)) return pt.runCommand("go-build", []string{goBin, "build", "-o", outBin, "."}, cwd) } + +// prepareDotNetProject runs setup necessary to get a .NET project ready for `pulumi` commands. +func (pt *programTester) prepareDotNetProject(projinfo *engine.Projinfo) error { + dotNetBin, err := pt.getDotNetBin() + if err != nil { + return errors.Wrap(err, "locating `dotnet` binary") + } + + cwd, _, err := projinfo.GetPwdMain() + if err != nil { + return err + } + + localNuget := os.Getenv("PULUMI_LOCAL_NUGET") + if localNuget == "" { + usr, err := user.Current() + if err != nil { + return errors.Wrap(err, "could not determine current user") + } + localNuget = filepath.Join(usr.HomeDir, ".nuget", "local") + } + + // dotnet add package requires a specific version in case of a pre-release, so we have to look it up. + matches, err := filepath.Glob(filepath.Join(localNuget, "Pulumi.?.?.*.nupkg")) + if err != nil { + return errors.Wrap(err, "failed to find a local Pulumi NuGet package") + } + if len(matches) != 1 { + return errors.New(fmt.Sprintf("attempting to find a local Pulumi NuGet package yielded %v results", matches)) + } + file := filepath.Base(matches[0]) + r := strings.NewReplacer("Pulumi.", "", ".nupkg", "") + version := r.Replace(file) + + return pt.runCommand("dotnet-add-package", + []string{dotNetBin, "add", "package", "Pulumi", "-s", localNuget, "-v", version}, cwd) +} diff --git a/sdk/dotnet/.editorconfig b/sdk/dotnet/.editorconfig new file mode 100644 index 000000000..5f67659df --- /dev/null +++ b/sdk/dotnet/.editorconfig @@ -0,0 +1,239 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = lf +insert_final_newline = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent + +# Expression-level preferences +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:suggestion + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:suggestion +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.namespace_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.namespace_should_be_pascal_case.symbols = namespace +dotnet_naming_rule.namespace_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.private_or_internal_field_should_be_start_with___camelcased.severity = suggestion +dotnet_naming_rule.private_or_internal_field_should_be_start_with___camelcased.symbols = private_or_internal_field +dotnet_naming_rule.private_or_internal_field_should_be_start_with___camelcased.style = start_with___camelcased + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field +dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private +dotnet_naming_symbols.private_or_internal_field.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal +dotnet_naming_symbols.non_field_members.required_modifiers = + +dotnet_naming_symbols.namespace.applicable_kinds = namespace +dotnet_naming_symbols.namespace.applicable_accessibilities = * +dotnet_naming_symbols.namespace.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.start_with___camelcased.required_prefix = _ +dotnet_naming_style.start_with___camelcased.required_suffix = +dotnet_naming_style.start_with___camelcased.word_separator = +dotnet_naming_style.start_with___camelcased.capitalization = camel_case + +# RS0016: Add public types and members to the declared API +dotnet_diagnostic.RS0016.severity = error + +# RS0017: Remove deleted types and members from the declared API +dotnet_diagnostic.RS0017.severity = error + +# RS0022: Constructor make noninheritable base class inheritable +dotnet_diagnostic.RS0022.severity = error + +# RS0024: The contents of the public API files are invalid +dotnet_diagnostic.RS0024.severity = error + +# RS0025: Do not duplicate symbols in public API files +dotnet_diagnostic.RS0025.severity = error + +# RS0026: Do not add multiple public overloads with optional parameters +dotnet_diagnostic.RS0026.severity = error + +# RS0027: Public API with optional parameter(s) should have the most parameters amongst its public overloads. +dotnet_diagnostic.RS0027.severity = error diff --git a/sdk/dotnet/.gitignore b/sdk/dotnet/.gitignore new file mode 100644 index 000000000..b7c7f5d1d --- /dev/null +++ b/sdk/dotnet/.gitignore @@ -0,0 +1,4 @@ +[Bb]in/ +[Oo]bj/ +.leu +Pulumi/Pulumi.xml \ No newline at end of file diff --git a/sdk/dotnet/Makefile b/sdk/dotnet/Makefile new file mode 100644 index 000000000..a394914cc --- /dev/null +++ b/sdk/dotnet/Makefile @@ -0,0 +1,39 @@ +PROJECT_NAME := Pulumi .NET Core SDK +LANGHOST_PKG := github.com/pulumi/pulumi/sdk/dotnet/cmd/pulumi-language-dotnet +VERSION := $(shell ../../scripts/get-version) +VERSION_DOTNET := ${VERSION:v%=%} # strip v from the beginning +VERSION_PREFIX := $(firstword $(subst -, ,${VERSION_DOTNET})) # e.g. 1.5.0 +VERSION_SUFFIX := $(word 2,$(subst -, ,${VERSION_DOTNET})) # e.g. alpha.1 +PROJECT_PKGS := $(shell go list ./cmd...) +PULUMI_LOCAL_NUGET ?= ~/.nuget/local + +TESTPARALLELISM := 10 + +include ../../build/common.mk + +build:: + dotnet build dotnet.sln /p:VersionPrefix=${VERSION_PREFIX} /p:VersionSuffix=${VERSION_SUFFIX} + echo "Copying NuGet packages to ${PULUMI_LOCAL_NUGET}" + mkdir -p ${PULUMI_LOCAL_NUGET} + find . -name '*.nupkg' -exec cp -p {} ${PULUMI_LOCAL_NUGET} \; + go install -ldflags "-X github.com/pulumi/pulumi/pkg/version.Version=${VERSION}" ${LANGHOST_PKG} + +install_plugin:: + GOBIN=$(PULUMI_BIN) go install -ldflags "-X github.com/pulumi/pulumi/pkg/version.Version=${VERSION}" ${LANGHOST_PKG} + +install:: install_plugin + +lint:: + golangci-lint run + +dotnet_test:: + dotnet test + +test_fast:: dotnet_test + $(GO_TEST_FAST) ${PROJECT_PKGS} + +test_all:: dotnet_test + $(GO_TEST) ${PROJECT_PKGS} + +dist:: + go install -ldflags "-X github.com/pulumi/pulumi/pkg/version.Version=${VERSION}" ${LANGHOST_PKG} diff --git a/sdk/dotnet/Pulumi.FSharp/Library.fs b/sdk/dotnet/Pulumi.FSharp/Library.fs new file mode 100644 index 000000000..e2819dc3e --- /dev/null +++ b/sdk/dotnet/Pulumi.FSharp/Library.fs @@ -0,0 +1,15 @@ +namespace Pulumi.FSharp + +open System.Collections.Generic +open Pulumi + +[] +module Ops = + let input<'a> (v: 'a): Input<'a> = Input.op_Implicit v + let io<'a> (v: Output<'a>): Input<'a> = Input.op_Implicit v + +module Deployment = + let run (f: unit -> IDictionary) = + Deployment.RunAsync (fun () -> f()) + |> Async.AwaitTask + |> Async.RunSynchronously \ No newline at end of file diff --git a/sdk/dotnet/Pulumi.FSharp/Pulumi.FSharp.fsproj b/sdk/dotnet/Pulumi.FSharp/Pulumi.FSharp.fsproj new file mode 100644 index 000000000..6b7dc98d4 --- /dev/null +++ b/sdk/dotnet/Pulumi.FSharp/Pulumi.FSharp.fsproj @@ -0,0 +1,34 @@ + + + + netcoreapp3.0 + true + Pulumi + Pulumi Corp. + F#-specific helpers for the Pulumi .NET SDK. + https://www.pulumi.com + https://github.com/pulumi/pulumi + Apache-2.0 + pulumi_logo_64x64.png + + + + NU5105 + + + + + + + + + + + + + True + + + + + diff --git a/sdk/dotnet/Pulumi.Tests/AssemblyAttributes.cs b/sdk/dotnet/Pulumi.Tests/AssemblyAttributes.cs new file mode 100644 index 000000000..93085d07d --- /dev/null +++ b/sdk/dotnet/Pulumi.Tests/AssemblyAttributes.cs @@ -0,0 +1,4 @@ +using Xunit; + +// Unfortunately, we depend on static state. So for now disable parallelization. +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/sdk/dotnet/Pulumi.Tests/AssertEx.cs b/sdk/dotnet/Pulumi.Tests/AssertEx.cs new file mode 100644 index 000000000..7135a9315 --- /dev/null +++ b/sdk/dotnet/Pulumi.Tests/AssertEx.cs @@ -0,0 +1,25 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System.Collections.Generic; +using Xunit; + +namespace Pulumi.Tests +{ + public static class AssertEx + { + public static void SequenceEqual(IEnumerable expected, IEnumerable actual) + => Assert.Equal(expected, actual); + + public static void MapEqual(IDictionary expected, IDictionary actual) where TKey : notnull + { + Assert.Equal(expected.Count, actual.Count); + foreach (var (key, actualValue) in actual) + { +#pragma warning disable CS8717 // A member returning a [MaybeNull] value introduces a null value for a type parameter. + Assert.True(expected.TryGetValue(key, out var expectedValue)); +#pragma warning restore CS8717 // A member returning a [MaybeNull] value introduces a null value for a type parameter. + Assert.Equal(expectedValue, actualValue); + } + } + } +} diff --git a/sdk/dotnet/Pulumi.Tests/Core/OutputTests.cs b/sdk/dotnet/Pulumi.Tests/Core/OutputTests.cs new file mode 100644 index 000000000..8a05e5ab6 --- /dev/null +++ b/sdk/dotnet/Pulumi.Tests/Core/OutputTests.cs @@ -0,0 +1,114 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System.Collections.Immutable; +using System.Threading.Tasks; +using Pulumi.Serialization; +using Xunit; + +namespace Pulumi.Tests.Core +{ + public partial class OutputTests : PulumiTest + { + private static Output CreateOutput(T value, bool isKnown, bool isSecret = false) + => new Output(ImmutableHashSet.Empty, + Task.FromResult(OutputData.Create(value, isKnown, isSecret))); + + public class PreviewTests + { + [Fact] + public Task ApplyCanRunOnKnownValue() + => RunInPreview(async () => + { + var o1 = CreateOutput(0, isKnown: true); + var o2 = o1.Apply(a => a + 1); + var data = await o2.DataTask.ConfigureAwait(false); + Assert.True(data.IsKnown); + Assert.Equal(1, data.Value); + }); + + [Fact] + public Task ApplyProducesUnknownDefaultOnUnknown() + => RunInPreview(async () => + { + var o1 = CreateOutput(0, isKnown: false); + var o2 = o1.Apply(a => a + 1); + var data = await o2.DataTask.ConfigureAwait(false); + Assert.False(data.IsKnown); + Assert.Equal(0, data.Value); + }); + + [Fact] + public Task ApplyPreservesSecretOnKnown() + => RunInPreview(async () => + { + var o1 = CreateOutput(0, isKnown: true, isSecret: true); + var o2 = o1.Apply(a => a + 1); + var data = await o2.DataTask.ConfigureAwait(false); + Assert.True(data.IsKnown); + Assert.True(data.IsSecret); + Assert.Equal(1, data.Value); + }); + + [Fact] + public Task ApplyPreservesSecretOnUnknown() + => RunInPreview(async () => + { + var o1 = CreateOutput(0, isKnown: false, isSecret: true); + var o2 = o1.Apply(a => a + 1); + var data = await o2.DataTask.ConfigureAwait(false); + Assert.False(data.IsKnown); + Assert.True(data.IsSecret); + Assert.Equal(0, data.Value); + }); + } + + public class NormalTests + { + [Fact] + public Task ApplyCanRunOnKnownValue() + => RunInNormal(async () => + { + var o1 = CreateOutput(0, isKnown: true); + var o2 = o1.Apply(a => a + 1); + var data = await o2.DataTask.ConfigureAwait(false); + Assert.True(data.IsKnown); + Assert.Equal(1, data.Value); + }); + + [Fact] + public Task ApplyProducesKnownOnUnknown() + => RunInNormal(async () => + { + var o1 = CreateOutput(0, isKnown: false); + var o2 = o1.Apply(a => a + 1); + var data = await o2.DataTask.ConfigureAwait(false); + Assert.False(data.IsKnown); + Assert.Equal(1, data.Value); + }); + + [Fact] + public Task ApplyPreservesSecretOnKnown() + => RunInNormal(async () => + { + var o1 = CreateOutput(0, isKnown: true, isSecret: true); + var o2 = o1.Apply(a => a + 1); + var data = await o2.DataTask.ConfigureAwait(false); + Assert.True(data.IsKnown); + Assert.True(data.IsSecret); + Assert.Equal(1, data.Value); + }); + + [Fact] + public Task ApplyPreservesSecretOnUnknown() + => RunInNormal(async () => + { + var o1 = CreateOutput(0, isKnown: false, isSecret: true); + var o2 = o1.Apply(a => a + 1); + var data = await o2.DataTask.ConfigureAwait(false); + Assert.False(data.IsKnown); + Assert.True(data.IsSecret); + Assert.Equal(1, data.Value); + }); + } + } +} diff --git a/sdk/dotnet/Pulumi.Tests/Core/ResourceArgsTests.cs b/sdk/dotnet/Pulumi.Tests/Core/ResourceArgsTests.cs new file mode 100644 index 000000000..41cb1ea8b --- /dev/null +++ b/sdk/dotnet/Pulumi.Tests/Core/ResourceArgsTests.cs @@ -0,0 +1,134 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System.Collections.Immutable; +using System.Threading.Tasks; +using Pulumi.Serialization; +using Xunit; + +namespace Pulumi.Tests.Core +{ + public class ResourceArgsTests : PulumiTest + { + #region ComplexResourceArgs1 + + public class ComplexResourceArgs1 : ResourceArgs + { + [Input("s")] public Input S { get; set; } = null!; + [Input("array")] private InputList _array = null!; + public InputList Array + { + get => _array ?? (_array = new InputList()); + set => _array = value; + } + } + + [Fact] + public async Task TestComplexResourceArgs1_NullValues() + { + var args = new ComplexResourceArgs1(); + var dictionary = await args.ToDictionaryAsync(); + + Assert.True(dictionary.TryGetValue("s", out var sValue)); + Assert.True(dictionary.TryGetValue("array", out var arrayValue)); + + Assert.Null(sValue); + Assert.Null(arrayValue); + } + + [Fact] + public async Task TestComplexResourceArgs1_SetField() + { + var args = new ComplexResourceArgs1 + { + S = "val", + }; + + var dictionary = await args.ToDictionaryAsync().ConfigureAwait(false); + + Assert.True(dictionary.TryGetValue("s", out var sValue)); + Assert.True(dictionary.TryGetValue("array", out var arrayValue)); + + Assert.NotNull(sValue); + Assert.Null(arrayValue); + + var output = sValue!.ToOutput(); + var data = await output.GetDataAsync(); + Assert.Equal("val", data.Value); + } + + [Fact] + public Task TestComplexResourceArgs1_SetProperty() + { + return RunInNormal(async () => + { + var args = new ComplexResourceArgs1 + { + Array = { true }, + }; + + var dictionary = await args.ToDictionaryAsync().ConfigureAwait(false); + + Assert.True(dictionary.TryGetValue("s", out var sValue)); + Assert.True(dictionary.TryGetValue("array", out var arrayValue)); + + Assert.Null(sValue); + Assert.NotNull(arrayValue); + + var output = arrayValue!.ToOutput(); + var data = await output.GetDataAsync(); + AssertEx.SequenceEqual( + ImmutableArray.Empty.Add(true), (ImmutableArray)data.Value!); + }); + } + + #endregion + + #region JsonResourceArgs1 + + public class JsonResourceArgs1 : ResourceArgs + { + [Input("array", json: true)] private InputList _array = null!; + public InputList Array + { + get => _array ?? (_array = new InputList()); + set => _array = value; + } + + [Input("map", json: true)] private InputMap _map = null!; + public InputMap Map + { + get => _map ?? (_map = new InputMap()); + set => _map = value; + } + } + + [Fact] + public async Task TestJsonMap() + { + var args = new JsonResourceArgs1 + { + Array = { true, false }, + Map = + { + { "k1", 1 }, + { "k2", 2 }, + }, + }; + var dictionary = await args.ToDictionaryAsync(); + + Assert.True(dictionary.TryGetValue("array", out var arrayValue)); + Assert.True(dictionary.TryGetValue("map", out var mapValue)); + + Assert.NotNull(arrayValue); + Assert.NotNull(mapValue); + + var arrayVal = (await arrayValue!.ToOutput().GetDataAsync()).Value; + Assert.Equal("[ true, false ]", arrayVal); + + var mapVal = (await mapValue!.ToOutput().GetDataAsync()).Value; + Assert.Equal("{ \"k1\": 1, \"k2\": 2 }", mapVal); + } + + #endregion + } +} diff --git a/sdk/dotnet/Pulumi.Tests/Pulumi.Tests.csproj b/sdk/dotnet/Pulumi.Tests/Pulumi.Tests.csproj new file mode 100644 index 000000000..d65438049 --- /dev/null +++ b/sdk/dotnet/Pulumi.Tests/Pulumi.Tests.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.0 + enable + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sdk/dotnet/Pulumi.Tests/PulumiTest.cs b/sdk/dotnet/Pulumi.Tests/PulumiTest.cs new file mode 100644 index 000000000..a57cd5206 --- /dev/null +++ b/sdk/dotnet/Pulumi.Tests/PulumiTest.cs @@ -0,0 +1,39 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Threading.Tasks; +using Moq; + +namespace Pulumi.Tests +{ + public abstract class PulumiTest + { + private static Task Run(Action action, bool dryRun) + => Run(() => + { + action(); + return Task.CompletedTask; + }, dryRun); + + private static async Task Run(Func func, bool dryRun) + { + var mock = new Mock(MockBehavior.Strict); + mock.Setup(d => d.IsDryRun).Returns(dryRun); + + Deployment.Instance = mock.Object; + await func().ConfigureAwait(false); + } + + protected static Task RunInPreview(Action action) + => Run(action, dryRun: true); + + protected static Task RunInNormal(Action action) + => Run(action, dryRun: false); + + protected static Task RunInPreview(Func func) + => Run(func, dryRun: true); + + protected static Task RunInNormal(Func func) + => Run(func, dryRun: false); + } +} diff --git a/sdk/dotnet/Pulumi.Tests/Serialization/BooleanConverterTests.cs b/sdk/dotnet/Pulumi.Tests/Serialization/BooleanConverterTests.cs new file mode 100644 index 000000000..1e35fe792 --- /dev/null +++ b/sdk/dotnet/Pulumi.Tests/Serialization/BooleanConverterTests.cs @@ -0,0 +1,127 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Threading.Tasks; +using Google.Protobuf.WellKnownTypes; +using Pulumi.Serialization; +using Xunit; + +namespace Pulumi.Tests.Serialization +{ + public class BooleanConverterTests : ConverterTests + { + [Fact] + public void True() + { + var data = Converter.ConvertValue("", new Value { BoolValue = true }); + Assert.True(data.Value); + Assert.True(data.IsKnown); + } + + [Fact] + public void False() + { + var data = Converter.ConvertValue("", new Value { BoolValue = false }); + + Assert.False(data.Value); + Assert.True(data.IsKnown); + } + + [Fact] + public void SecretTrue() + { + var data = Converter.ConvertValue("", CreateSecretValue(new Value { BoolValue = true })); + + Assert.True(data.Value); + Assert.True(data.IsKnown); + Assert.True(data.IsSecret); + } + + [Fact] + public void SecretFalse() + { + var data = Converter.ConvertValue("", CreateSecretValue(new Value { BoolValue = false })); + + Assert.False(data.Value); + Assert.True(data.IsKnown); + Assert.True(data.IsSecret); + } + + [Fact] + public void NonBooleanThrows() + { + Assert.Throws(() => + { + var data = Converter.ConvertValue("", new Value { StringValue = "" }); + }); + } + + [Fact] + public Task NullInPreviewProducesFalseKnown() + { + return RunInPreview(() => + { + var data = Converter.ConvertValue("", new Value { NullValue = NullValue.NullValue }); + + Assert.False(data.Value); + Assert.True(data.IsKnown); + }); + } + + [Fact] + public Task NullInNormalProducesFalseKnown() + { + return RunInNormal(() => + { + var data = Converter.ConvertValue("", new Value { NullValue = NullValue.NullValue }); + + Assert.False(data.Value); + Assert.True(data.IsKnown); + }); + } + + [Fact] + public void UnknownProducesFalseUnknown() + { + var data = Converter.ConvertValue("", UnknownValue); + + Assert.False(data.Value); + Assert.False(data.IsKnown); + } + + [Fact] + public void StringTest() + { + Assert.Throws(() => + { + var data = Converter.ConvertValue("", new Value { StringValue = "" }); + }); + } + + [Fact] + public void NullableTrue() + { + var data = Converter.ConvertValue("", new Value { BoolValue = true }); + Assert.True(data.Value); + Assert.True(data.IsKnown); + } + + [Fact] + public void NullableFalse() + { + var data = Converter.ConvertValue("", new Value { BoolValue = false }); + + Assert.False(data.Value); + Assert.True(data.IsKnown); + } + + [Fact] + public void NullableNull() + { + var data = Converter.ConvertValue("", new Value { NullValue = NullValue.NullValue }); + + Assert.Null(data.Value); + Assert.True(data.IsKnown); + } + } +} diff --git a/sdk/dotnet/Pulumi.Tests/Serialization/ComplexTypeConverterTests.cs b/sdk/dotnet/Pulumi.Tests/Serialization/ComplexTypeConverterTests.cs new file mode 100644 index 000000000..2292764b2 --- /dev/null +++ b/sdk/dotnet/Pulumi.Tests/Serialization/ComplexTypeConverterTests.cs @@ -0,0 +1,170 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Pulumi.Serialization; +using Xunit; + +namespace Pulumi.Tests.Serialization +{ + public class ComplexTypeConverterTests : ConverterTests + { + #region Simple case + + [OutputType] + public class ComplexType1 + { + public readonly string S; + public readonly bool B; + public readonly int I; + public readonly double D; + public readonly ImmutableArray Array; + public readonly ImmutableDictionary Dict; + + [OutputConstructor] + public ComplexType1( + string s, bool b, int i, double d, + ImmutableArray array, ImmutableDictionary dict) + { + S = s; + B = b; + I = i; + D = d; + Array = array; + Dict = dict; + } + } + + [Fact] + public async Task TestComplexType1() + { + var data = Converter.ConvertValue("", await SerializeToValueAsync(new Dictionary + { + { "s", "str" }, + { "b", true }, + { "i", 42 }, + { "d", 1.5 }, + { "array", new List { true, false } }, + { "dict", new Dictionary { { "k", 10 } } }, + })); + + Assert.Equal("str", data.Value.S); + Assert.Equal((object)true, data.Value.B); + Assert.Equal(42, data.Value.I); + Assert.Equal(1.5, data.Value.D); + AssertEx.SequenceEqual(ImmutableArray.Empty.Add(true).Add(false), data.Value.Array); + AssertEx.MapEqual(ImmutableDictionary.Empty.Add("k", 10), data.Value.Dict); + + Assert.True(data.IsKnown); + } + + #endregion + + #region Nested case + + [OutputType] + public class ComplexType2 + { + public readonly ComplexType1 C; + public readonly ImmutableArray C2Array; + public readonly ImmutableDictionary C2Map; + + [OutputConstructor] + public ComplexType2( + ComplexType1 c, + ImmutableArray c2Array, + ImmutableDictionary c2Map) + { + C = c; + C2Array = c2Array; + C2Map = c2Map; + } + } + + [Fact] + public async Task TestComplexType2() + { + var data = Converter.ConvertValue("", await SerializeToValueAsync(new Dictionary + { + { + "c", + new Dictionary + { + { "s", "str1" }, + { "b", false }, + { "i", 1 }, + { "d", 1.1 }, + { "array", new List { false, false } }, + { "dict", new Dictionary { { "k", 1 } } }, + } + }, + { + "c2Array", + new List + { + new Dictionary + { + { "s", "str2" }, + { "b", true }, + { "i", 2 }, + { "d", 2.2 }, + { "array", new List { false, true } }, + { "dict", new Dictionary { { "k", 2 } } }, + } + } + }, + { + "c2Map", + new Dictionary + { + { + "someKey", + new Dictionary + { + { "s", "str3" }, + { "b", false }, + { "i", 3 }, + { "d", 3.3 }, + { "array", new List { true, false } }, + { "dict", new Dictionary { { "k", 3 } } }, + } + } + } + } + })).Value; + + var value = data.C; + Assert.Equal("str1", value.S); + Assert.Equal((object)false, value.B); + Assert.Equal(1, value.I); + Assert.Equal(1.1, value.D); + AssertEx.SequenceEqual(ImmutableArray.Empty.Add(false).Add(false), value.Array); + AssertEx.MapEqual(ImmutableDictionary.Empty.Add("k", 1), value.Dict); + + Assert.Single(data.C2Array); + value = data.C2Array[0]; + Assert.Equal("str2", value.S); + Assert.Equal((object)true, value.B); + Assert.Equal(2, value.I); + Assert.Equal(2.2, value.D); + AssertEx.SequenceEqual(ImmutableArray.Empty.Add(false).Add(true), value.Array); + AssertEx.MapEqual(ImmutableDictionary.Empty.Add("k", 2), value.Dict); + + Assert.Single(data.C2Map); + var (key, val) = data.C2Map.Single(); + Assert.Equal("someKey", key); + value = val; + + Assert.Equal("str3", value.S); + Assert.Equal((object)false, value.B); + Assert.Equal(3, value.I); + Assert.Equal(3.3, value.D); + AssertEx.SequenceEqual(ImmutableArray.Empty.Add(true).Add(false), value.Array); + AssertEx.MapEqual(ImmutableDictionary.Empty.Add("k", 3), value.Dict); + } + + #endregion + } +} diff --git a/sdk/dotnet/Pulumi.Tests/Serialization/ConverterTests.cs b/sdk/dotnet/Pulumi.Tests/Serialization/ConverterTests.cs new file mode 100644 index 000000000..d6c9ed519 --- /dev/null +++ b/sdk/dotnet/Pulumi.Tests/Serialization/ConverterTests.cs @@ -0,0 +1,37 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System.Collections.Immutable; +using System.Threading.Tasks; +using Google.Protobuf.WellKnownTypes; +using Pulumi.Serialization; + +namespace Pulumi.Tests.Serialization +{ + public abstract class ConverterTests : PulumiTest + { + protected static readonly Value UnknownValue = new Value { StringValue = Constants.UnknownValue }; + + protected static Value CreateSecretValue(Value value) + => new Value + { + StructValue = new Struct + { + Fields = + { + { Constants.SpecialSigKey, new Value { StringValue = Constants.SpecialSecretSig } }, + { Constants.SecretValueName, value }, + } + } + }; + + protected Output CreateUnknownOutput(T value) + => new Output(ImmutableHashSet.Empty, Task.FromResult(new OutputData(value, isKnown: false, isSecret: false))); + + protected async Task SerializeToValueAsync(object? value) + { + var serializer = new Serializer(excessiveDebugOutput: false); + return Serializer.CreateValue( + await serializer.SerializeAsync(ctx: "", value).ConfigureAwait(false)); + } + } +} diff --git a/sdk/dotnet/Pulumi.Tests/Serialization/ListOutputCompletionSourceTests.cs b/sdk/dotnet/Pulumi.Tests/Serialization/ListOutputCompletionSourceTests.cs new file mode 100644 index 000000000..7763a7611 --- /dev/null +++ b/sdk/dotnet/Pulumi.Tests/Serialization/ListOutputCompletionSourceTests.cs @@ -0,0 +1,61 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Pulumi.Serialization; +using Xunit; + +namespace Pulumi.Tests.Serialization +{ + public class ListConverterTests : ConverterTests + { + [Fact] + public async Task EmptyList() + { + var data = Converter.ConvertValue>("", await SerializeToValueAsync(new List())); + + Assert.Equal(ImmutableArray.Empty, data.Value); + Assert.True(data.IsKnown); + } + + [Fact] + public async Task ListWithElement() + { + var data = Converter.ConvertValue>("", await SerializeToValueAsync(new List { true })); + + AssertEx.SequenceEqual(ImmutableArray.Empty.Add(true), data.Value); + Assert.True(data.IsKnown); + } + + [Fact] + public async Task SecretListWithElement() + { + var data = Converter.ConvertValue>("", await SerializeToValueAsync(Output.CreateSecret(new List { true }))); + + AssertEx.SequenceEqual(ImmutableArray.Empty.Add(true), data.Value); + Assert.True(data.IsKnown); + Assert.True(data.IsSecret); + } + + [Fact] + public async Task ListWithSecretElement() + { + var data = Converter.ConvertValue>("", await SerializeToValueAsync(new List { Output.CreateSecret(true) })); + + AssertEx.SequenceEqual(ImmutableArray.Empty.Add(true), data.Value); + Assert.True(data.IsKnown); + Assert.True(data.IsSecret); + } + + [Fact] + public async Task ListWithUnknownElement() + { + var data = Converter.ConvertValue>("", await SerializeToValueAsync(new List { CreateUnknownOutput(true) })); + + AssertEx.SequenceEqual(ImmutableArray.Empty.Add(false), data.Value); + Assert.False(data.IsKnown); + Assert.False(data.IsSecret); + } + } +} diff --git a/sdk/dotnet/Pulumi/AssemblyAttributes.cs b/sdk/dotnet/Pulumi/AssemblyAttributes.cs new file mode 100644 index 000000000..9a44e1a8d --- /dev/null +++ b/sdk/dotnet/Pulumi/AssemblyAttributes.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Pulumi.Tests")] \ No newline at end of file diff --git a/sdk/dotnet/Pulumi/Config.cs b/sdk/dotnet/Pulumi/Config.cs new file mode 100644 index 000000000..49aea5f03 --- /dev/null +++ b/sdk/dotnet/Pulumi/Config.cs @@ -0,0 +1,217 @@ +// Copyright 2016-2018, Pulumi Corporation + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; + +namespace Pulumi +{ + /// + /// is a bag of related configuration state. Each bag contains any number + /// of configuration variables, indexed by simple keys, and each has a name that uniquely + /// identifies it; two bags with different names do not share values for variables that + /// otherwise share the same key. For example, a bag whose name is pulumi:foo, with keys + /// a, b, and c, is entirely separate from a bag whose name is + /// pulumi:bar with the same simple key names. Each key has a fully qualified names, + /// such as pulumi:foo:a, ..., and pulumi:bar:a, respectively. + /// + public sealed partial class Config + { + /// + /// is the configuration bag's logical name and uniquely identifies it. + /// The default is the name of the current project. + /// + private readonly string _name; + + /// + /// Creates a new instance. is the + /// configuration bag's logical name and uniquely identifies it. The default is the name of + /// the current project. + /// + public Config(string? name = null) + { + if (name == null) + { + name = Deployment.Instance.ProjectName; + } + + if (name.EndsWith(":config", StringComparison.Ordinal)) + { + name = name[0..^":config".Length]; + } + + _name = name; + } + + [return: NotNullIfNotNull("value")] + private static Output? MakeClassSecret(T? value) where T : class + => value == null ? null : Output.CreateSecret(value); + + private static Output? MakeStructSecret(T? value) where T : struct + => value == null ? null : MakeStructSecret(value.Value); + + private static Output MakeStructSecret(T value) where T : struct + => Output.CreateSecret(value); + + + /// + /// Loads an optional configuration value by its key, or if it doesn't exist. + /// + public string? Get(string key) + => Deployment.InternalInstance.GetConfig(FullKey(key)); + + /// + /// Loads an optional configuration value by its key, marking it as a secret, or if it doesn't exist. + /// + public Output? GetSecret(string key) + => MakeClassSecret(Get(key)); + + /// + /// Loads an optional configuration value, as a boolean, by its key, or null if it doesn't exist. + /// If the configuration value isn't a legal boolean, this function will throw an error. + /// + public bool? GetBoolean(string key) + { + var v = Get(key); + return v == null ? default(bool?) : + v == "true" ? true : + v == "false" ? false : throw new ConfigTypeException(FullKey(key), v, nameof(Boolean)); + } + + /// + /// Loads an optional configuration value, as a boolean, by its key, making it as a secret or + /// null if it doesn't exist. If the configuration value isn't a legal boolean, this + /// function will throw an error. + /// + public Output? GetSecretBoolean(string key) + => MakeStructSecret(GetBoolean(key)); + + /// + /// Loads an optional configuration value, as a number, by its key, or null if it doesn't exist. + /// If the configuration value isn't a legal number, this function will throw an error. + /// + public int? GetInt32(string key) + { + var v = Get(key); + return v == null + ? default(int?) + : int.TryParse(v, out var result) + ? result + : throw new ConfigTypeException(FullKey(key), v, nameof(Int32)); + } + + /// + /// Loads an optional configuration value, as a number, by its key, marking it as a secret + /// or null if it doesn't exist. + /// If the configuration value isn't a legal number, this function will throw an error. + /// + public Output? GetSecretInt32(string key) + => MakeStructSecret(GetInt32(key)); + + /// + /// Loads an optional configuration value, as an object, by its key, or null if it doesn't + /// exist. This works by taking the value associated with and passing + /// it to . + /// + [return: MaybeNull] + public T GetObject(string key) + { + var v = Get(key); + try + { + return v == null ? default : JsonSerializer.Deserialize(v); + } + catch (JsonException ex) + { + throw new ConfigTypeException(FullKey(key), v, typeof(T).FullName!, ex); + } + } + + /// + /// Loads an optional configuration value, as an object, by its key, marking it as a secret + /// or null if it doesn't exist. This works by taking the value associated with and passing it to . + /// + public Output? GetSecretObject(string key) + { + var v = Get(key); + if (v == null) + return null; + + return Output.CreateSecret(GetObject(key)!); + } + + /// + /// Loads a configuration value by its given key. If it doesn't exist, an error is thrown. + /// + public string Require(string key) + => Get(key) ?? throw new ConfigMissingException(FullKey(key)); + + /// + /// Loads a configuration value by its given key, marking it as a secret. If it doesn't exist, an error + /// is thrown. + /// + public Output RequireSecret(string key) + => MakeClassSecret(Require(key)); + + /// + /// Loads a configuration value, as a boolean, by its given key. If it doesn't exist, or the + /// configuration value is not a legal boolean, an error is thrown. + /// + public bool RequireBoolean(string key) + => GetBoolean(key) ?? throw new ConfigMissingException(FullKey(key)); + + /// + /// Loads a configuration value, as a boolean, by its given key, marking it as a secret. + /// If it doesn't exist, or the configuration value is not a legal boolean, an error is thrown. + /// + public Output RequireSecretBoolean(string key) + => MakeStructSecret(RequireBoolean(key)); + + /// + /// Loads a configuration value, as a number, by its given key. If it doesn't exist, or the + /// configuration value is not a legal number, an error is thrown. + /// + public int RequireInt32(string key) + => GetInt32(key) ?? throw new ConfigMissingException(FullKey(key)); + + /// + /// Loads a configuration value, as a number, by its given key, marking it as a secret. + /// If it doesn't exist, or the configuration value is not a legal number, an error is thrown. + /// + public Output RequireSecretInt32(string key) + => MakeStructSecret(RequireInt32(key)); + + /// + /// Loads a configuration value as a JSON string and deserializes the JSON into an object. + /// object. If it doesn't exist, or the configuration value cannot be converted using , an error is + /// thrown. + /// + public T RequireObject(string key) + { + var v = Get(key); + if (v == null) + throw new ConfigMissingException(FullKey(key)); + + return GetObject(key)!; + } + + /// + /// Loads a configuration value as a JSON string and deserializes the JSON into a JavaScript + /// object, marking it as a secret. If it doesn't exist, or the configuration value cannot + /// be converted using . an error is thrown. + /// + public Output RequireSecretObject(string key) + => Output.CreateSecret(RequireObject(key)); + + /// + /// Turns a simple configuration key into a fully resolved one, by prepending the bag's name. + /// + private string FullKey(string key) + => $"{_name}:{key}"; + } +} diff --git a/sdk/dotnet/Pulumi/Config_Exceptions.cs b/sdk/dotnet/Pulumi/Config_Exceptions.cs new file mode 100644 index 000000000..931ded788 --- /dev/null +++ b/sdk/dotnet/Pulumi/Config_Exceptions.cs @@ -0,0 +1,37 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; + +namespace Pulumi +{ + public partial class Config + { + /// + /// ConfigTypeException is used when a configuration value is of the wrong type. + /// + private class ConfigTypeException : RunException + { + public ConfigTypeException(string key, object? v, string expectedType) + : this(key, v, expectedType, innerException: null) + { + } + + public ConfigTypeException(string key, object? v, string expectedType, Exception? innerException) + : base($"Configuration '{key}' value '{v}' is not a valid {expectedType}", innerException) + { + } + } + + /// + /// ConfigMissingException is used when a configuration value is completely missing. + /// + private class ConfigMissingException : RunException + { + public ConfigMissingException(string key) + : base($"Missing Required configuration variable '{key}'\n" + + $"\tplease set a value using the command `pulumi config set {key} `") + { + } + } + } +} diff --git a/sdk/dotnet/Pulumi/Core/Alias.cs b/sdk/dotnet/Pulumi/Core/Alias.cs new file mode 100644 index 000000000..fdb75bc81 --- /dev/null +++ b/sdk/dotnet/Pulumi/Core/Alias.cs @@ -0,0 +1,85 @@ +// Copyright 2016-2019, Pulumi Corporation + +namespace Pulumi +{ + /// + /// Alias is a description of prior named used for a resource. It can be processed in the + /// context of a resource creation to determine what the full aliased URN would be. + /// + /// Use in the case where a prior URN is known and can just be specified in + /// full. Otherwise, provide some subset of the other properties in this type to generate an + /// appropriate urn from the pre-existing values of the with certain + /// parts overridden. + /// + /// The presence of a property indicates if its value should be used. If absent (i.e. + /// ), then the value is not used. + /// + /// Note: because of the above, there needs to be special handling to indicate that the previous + /// of a was . Specifically, + /// pass in: + /// + /// Aliases = { new Alias { NoParent = true } } + /// + public sealed class Alias + { + /// + /// The previous urn to alias to. If this is provided, no other properties in this type + /// should be provided. + /// + public string? Urn { get; set; } + + /// + /// The previous name of the resource. If , the current name of the + /// resource is used. + /// + public Input? Name { get; set; } + + /// + /// The previous type of the resource. If , the current type of the + /// resource is used. + /// + public Input? Type { get; set; } + + /// + /// The previous stack of the resource. If , defaults to the value of + /// . + /// + public Input? Stack { get; set; } + + /// + /// The previous project of the resource. If , defaults to the value + /// of . + /// + public Input? Project { get; set; } + + /// + /// The previous parent of the resource. If , the current parent of + /// the resource is used. + /// + /// To specify no original parent, use new Alias { NoParent = true }. + /// + /// Only specify one of or or . + /// + public Resource? Parent { get; set; } + + /// + /// The previous parent of the resource. If , the current parent of + /// the resource is used. + /// + /// To specify no original parent, use new Alias { NoParent = true }. + /// + /// Only specify one of or or . + /// + public Input? ParentUrn { get; set; } + + /// + /// Used to indicate the resource previously had no parent. If this + /// property is ignored. + /// + /// To specify no original parent, use new Alias { NoParent = true }. + /// + /// Only specify one of or or . + /// + public bool NoParent { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi/Core/Archive.cs b/sdk/dotnet/Pulumi/Core/Archive.cs new file mode 100644 index 000000000..1868ca4ed --- /dev/null +++ b/sdk/dotnet/Pulumi/Core/Archive.cs @@ -0,0 +1,55 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System.Collections.Immutable; +using Pulumi.Serialization; + +namespace Pulumi +{ + /// + /// An Archive represents a collection of named assets. + /// + public abstract class Archive : AssetOrArchive + { + private protected Archive(string propName, object value) + : base(Constants.SpecialArchiveSig, propName, value) + { + } + } + + /// + /// An AssetArchive is an archive created from an in-memory collection of named assets or other + /// archives. + /// + public sealed class AssetArchive : Archive + { + public AssetArchive(ImmutableDictionary assets) + : base(Constants.ArchiveAssetsName, assets) + { + } + } + + /// + /// A FileArchive is a file-based archive, or a collection of file-based assets. This can be a + /// raw directory or a single archive file in one of the supported formats(.tar, .tar.gz, + /// or.zip). + /// + public sealed class FileArchive : Archive + { + public FileArchive(string path) : base(Constants.AssetOrArchivePathName, path) + { + } + } + + /// + /// A RemoteArchive is a file-based archive fetched from a remote location. The URI's scheme + /// dictates the protocol for fetching the archive's contents: file:// is a local file + /// (just like a FileArchive), http:// and https:// specify HTTP and HTTPS, + /// respectively, and specific providers may recognize custom schemes. + /// + public sealed class RemoteArchive : Archive + { + public RemoteArchive(string uri) : base(Constants.AssetOrArchiveUriName, uri) + { + } + } +} diff --git a/sdk/dotnet/Pulumi/Core/Asset.cs b/sdk/dotnet/Pulumi/Core/Asset.cs new file mode 100644 index 000000000..df36eb29c --- /dev/null +++ b/sdk/dotnet/Pulumi/Core/Asset.cs @@ -0,0 +1,51 @@ +// Copyright 2016-2019, Pulumi Corporation + +using Pulumi.Serialization; + +namespace Pulumi +{ + /// + /// Asset represents a single blob of text or data that is managed as a first class entity. + /// + public abstract class Asset : AssetOrArchive + { + private protected Asset(string propName, object value) + : base(Constants.SpecialAssetSig, propName, value) + { + } + } + + /// + /// FileAsset is a kind of asset produced from a given path to a file on the local filesystem. + /// + public sealed class FileAsset : Asset + { + public FileAsset(string path) : base(Constants.AssetOrArchivePathName, path) + { + } + } + + + /// + /// StringAsset is a kind of asset produced from an in-memory UTF8-encoded string. + /// + public sealed class StringAsset : Asset + { + public StringAsset(string text) : base(Constants.AssetTextName, text) + { + } + } + + /// + /// RemoteAsset is a kind of asset produced from a given URI string. The URI's scheme dictates + /// the protocol for fetching contents: file:// specifies a local file, http:// + /// and https:// specify HTTP and HTTPS, respectively. Note that specific providers may + /// recognize alternative schemes; this is merely the base-most set that all providers support. + /// + public sealed class RemoteAsset : Asset + { + public RemoteAsset(string uri) : base(Constants.AssetOrArchiveUriName, uri) + { + } + } +} diff --git a/sdk/dotnet/Pulumi/Core/AssetOrArchive.cs b/sdk/dotnet/Pulumi/Core/AssetOrArchive.cs new file mode 100644 index 000000000..cc0508e77 --- /dev/null +++ b/sdk/dotnet/Pulumi/Core/AssetOrArchive.cs @@ -0,0 +1,21 @@ +// Copyright 2016-2019, Pulumi Corporation + +namespace Pulumi +{ + /// + /// Base class of s and s. + /// + public abstract class AssetOrArchive + { + internal string SigKey { get; } + internal string PropName { get; } + internal object Value { get; } + + private protected AssetOrArchive(string sigKey, string propName, object value) + { + SigKey = sigKey ?? throw new System.ArgumentNullException(nameof(sigKey)); + PropName = propName ?? throw new System.ArgumentNullException(nameof(propName)); + Value = value ?? throw new System.ArgumentNullException(nameof(value)); + } + } +} diff --git a/sdk/dotnet/Pulumi/Core/Input.cs b/sdk/dotnet/Pulumi/Core/Input.cs new file mode 100644 index 000000000..fc1365bcd --- /dev/null +++ b/sdk/dotnet/Pulumi/Core/Input.cs @@ -0,0 +1,51 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Pulumi +{ + /// + /// Internal interface to allow our code to operate on inputs in an untyped manner. Necessary as + /// there is no reasonable way to write algorithms over heterogeneous instantiations of generic + /// types. + /// + internal interface IInput + { + IOutput ToOutput(); + } + + /// + /// is a property input for a . It may be a promptly + /// available T, or the output from a existing . + /// + public class Input : IInput + { + /// + /// Technically, in .net we can represent Inputs entirely using the Output type (since + /// Outputs can wrap values and promises). However, it would look very weird to state that + /// the inputs to a resource *had* to be Outputs. So we basically just come up with this + /// wrapper type so things look sensible, even though under the covers we implement things + /// using the exact same type + /// + private protected Output _outputValue; + + private protected Input(Output outputValue) + => _outputValue = outputValue ?? throw new ArgumentNullException(nameof(outputValue)); + + public static implicit operator Input([MaybeNull]T value) + => Output.Create(value); + + public static implicit operator Input(Output value) + => new Input(value); + + public static implicit operator Output(Input input) + => input._outputValue; + + public Output ToOutput() + => this; + + IOutput IInput.ToOutput() + => ToOutput(); + } +} diff --git a/sdk/dotnet/Pulumi/Core/InputList.cs b/sdk/dotnet/Pulumi/Core/InputList.cs new file mode 100644 index 000000000..0dac7a303 --- /dev/null +++ b/sdk/dotnet/Pulumi/Core/InputList.cs @@ -0,0 +1,159 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; + +namespace Pulumi +{ + /// + /// A list of values that can be passed in as the arguments to a . + /// The individual values are themselves s. i.e. the individual values + /// can be concrete values or s. + /// + /// differs from a normal in that it is itself + /// an . For example, a that accepts an will accept not just a list but an + /// of a list. This is important for cases where the + /// list from some needs to be passed into another . Or for cases where creating the list invariably produces an because its resultant value is dependent on other s. + /// + /// This benefit of is also a limitation. Because it represents a + /// list of values that may eventually be created, there is no way to simply iterate over, or + /// access the elements of the list synchronously. + /// + /// is designed to be easily used in object and collection + /// initializers. For example, a resource that accepts a list of inputs can be written in + /// either of these forms: + /// + /// + /// new SomeResource("name", new SomeResourceArgs { + /// ListProperty = { Value1, Value2, Value3 }, + /// }); + /// + /// + /// or + /// + /// new SomeResource("name", new SomeResourceArgs { + /// ListProperty = new [] { Value1, Value2, Value3 }, + /// }); + /// + /// + public sealed class InputList : Input>, IEnumerable, IAsyncEnumerable> + { + public InputList() : this(Output.Create(ImmutableArray.Empty)) + { + } + + private InputList(Output> values) + : base(values) + { + } + + public void Add(params Input[] inputs) + { + // Make an Output from the values passed in, mix in with our own Output, and combine + // both to produce the final array that we will now point at. + _outputValue = Output.Concat(_outputValue, Output.All(inputs)); + } + + /// + /// Concatenates the values in this list with the values in , + /// returning the concatenated sequence in a new . + /// + public InputList Concat(InputList other) + => Output.Concat(_outputValue, other._outputValue); + + internal InputList Clone() + => new InputList(_outputValue); + + #region construct from unary + + public static implicit operator InputList(T value) + => ImmutableArray.Create>(value); + + public static implicit operator InputList(Output value) + => ImmutableArray.Create>(value); + + public static implicit operator InputList(Input value) + => ImmutableArray.Create(value); + + #endregion + + #region construct from array + + public static implicit operator InputList(T[] values) + => ImmutableArray.CreateRange(values.Select(v => (Input)v)); + + public static implicit operator InputList(Output[] values) + => ImmutableArray.CreateRange(values.Select(v => (Input)v)); + + public static implicit operator InputList(Input[] values) + => ImmutableArray.CreateRange(values); + + #endregion + + #region construct from list + + public static implicit operator InputList(List values) + => ImmutableArray.CreateRange(values); + + public static implicit operator InputList(List> values) + => ImmutableArray.CreateRange(values); + + public static implicit operator InputList(List> values) + => ImmutableArray.CreateRange(values); + + #endregion + + #region construct from immutable array + + public static implicit operator InputList(ImmutableArray values) + => values.SelectAsArray(v => (Input)v); + + public static implicit operator InputList(ImmutableArray> values) + => values.SelectAsArray(v => (Input)v); + + public static implicit operator InputList(ImmutableArray> values) + => Output.All(values); + + #endregion + + #region construct from Output of some list type. + + public static implicit operator InputList(Output values) + => values.Apply(a => ImmutableArray.CreateRange(a)); + + public static implicit operator InputList(Output> values) + => values.Apply(a => ImmutableArray.CreateRange(a)); + + public static implicit operator InputList(Output> values) + => values.Apply(a => ImmutableArray.CreateRange(a)); + + public static implicit operator InputList(Output> values) + => new InputList(values); + + #endregion + + #region IEnumerable + + IEnumerator IEnumerable.GetEnumerator() + => throw new NotSupportedException($"A {GetType().FullName} cannot be synchronously enumerated. Use {nameof(GetAsyncEnumerator)} instead."); + + public async IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken) + { + var data = await _outputValue.GetValueAsync().ConfigureAwait(false); + foreach (var value in data) + { + yield return value; + } + } + + #endregion + } +} diff --git a/sdk/dotnet/Pulumi/Core/InputMap.cs b/sdk/dotnet/Pulumi/Core/InputMap.cs new file mode 100644 index 000000000..e15f27d37 --- /dev/null +++ b/sdk/dotnet/Pulumi/Core/InputMap.cs @@ -0,0 +1,100 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; + +namespace Pulumi +{ + /// + /// A mapping of s to values that can be passed in as the arguments to a + /// . The individual values are themselves s. i.e. + /// the individual values can be concrete values or s. + /// + /// differs from a normal in that it is + /// itself an . For example, a that accepts an + /// will accept not just a dictionary but an + /// of a dictionary as well. This is important for cases where the + /// map from some needs to be passed into another . + /// Or for cases where creating the map invariably produces an because + /// its resultant value is dependent on other s. + /// + /// This benefit of is also a limitation. Because it represents a + /// list of values that may eventually be created, there is no way to simply iterate over, or + /// access the elements of the map synchronously. + /// + /// is designed to be easily used in object and collection + /// initializers. For example, a resource that accepts a map of values can be written easily in + /// this form: + /// + /// + /// new SomeResource("name", new SomeResourceArgs { + /// MapProperty = { + /// { Key1, Value1 }, + /// { Key2, Value2 }, + /// { Key3, Value3 }, + /// }, + /// }); + /// + /// + public sealed class InputMap : Input>, IEnumerable, IAsyncEnumerable>> + { + public InputMap() : this(Output.Create(ImmutableDictionary.Empty)) + { + } + + private InputMap(Output> values) + : base(values) + { + } + + public void Add(string key, Input value) + { + var inputDictionary = (Input>)_outputValue; + _outputValue = Output.Tuple(inputDictionary, value) + .Apply(x => x.Item1.Add(key, x.Item2)); + } + + public Input this[string key] + { + set => Add(key, value); + } + + #region construct from dictionary types + + public static implicit operator InputMap(Dictionary values) + => Output.Create(values); + + public static implicit operator InputMap(ImmutableDictionary values) + => Output.Create(values); + + public static implicit operator InputMap(Output> values) + => values.Apply(d => ImmutableDictionary.CreateRange(d)); + + public static implicit operator InputMap(Output> values) + => values.Apply(d => ImmutableDictionary.CreateRange(d)); + + public static implicit operator InputMap(Output> values) + => new InputMap(values); + + #endregion + + #region IEnumerable + + IEnumerator IEnumerable.GetEnumerator() + => throw new NotSupportedException($"A {GetType().FullName} cannot be synchronously enumerated. Use {nameof(GetAsyncEnumerator)} instead."); + + public async IAsyncEnumerator>> GetAsyncEnumerator(CancellationToken cancellationToken) + { + var data = await _outputValue.GetValueAsync().ConfigureAwait(false); + foreach (var value in data) + { + yield return value; + } + } + + #endregion + } +} diff --git a/sdk/dotnet/Pulumi/Core/Options.cs b/sdk/dotnet/Pulumi/Core/Options.cs new file mode 100644 index 000000000..62de2fcbe --- /dev/null +++ b/sdk/dotnet/Pulumi/Core/Options.cs @@ -0,0 +1,24 @@ +// Copyright 2016-2019, Pulumi Corporation + +namespace Pulumi +{ + internal class Options + { + public readonly bool QueryMode; + public readonly int Parallel; + public readonly string? Pwd; + public readonly string Monitor; + public readonly string Engine; + public readonly string? Tracing; + + public Options(bool queryMode, int parallel, string? pwd, string monitor, string engine, string? tracing) + { + QueryMode = queryMode; + Parallel = parallel; + Pwd = pwd; + Monitor = monitor; + Engine = engine; + Tracing = tracing; + } + } +} diff --git a/sdk/dotnet/Pulumi/Core/Output.cs b/sdk/dotnet/Pulumi/Core/Output.cs new file mode 100644 index 000000000..deb05ad93 --- /dev/null +++ b/sdk/dotnet/Pulumi/Core/Output.cs @@ -0,0 +1,289 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using Pulumi.Serialization; + +namespace Pulumi +{ + /// + /// Useful static utility methods for both creating and working wit s. + /// + public static class Output + { + public static Output Create([MaybeNull]T value) + => Create(Task.FromResult(value)); + + public static Output Create(Task value) + => Output.Create(value); + + public static Output CreateSecret([MaybeNull]T value) + => CreateSecret(Task.FromResult(value)); + + public static Output CreateSecret(Task value) + => Output.CreateSecret(value); + + /// + /// Combines all the values in and combines + /// them all into a single with an + /// containing all their underlying values. If any of the s are not + /// known, the final result will be not known. Similarly, if any of the s are secrets, then the final result will be a secret. + /// + public static Output> All(params Input[] inputs) + => All(ImmutableArray.CreateRange(inputs)); + + /// + /// for more details. + /// + public static Output> All(ImmutableArray> inputs) + => Output.All(inputs); + + /// + /// for more details. + /// + public static Output<(X, Y)> Tuple(Output item1, Output item2) + => Tuple((Input)item1, (Input)item2); + + /// + /// for more details. + /// + public static Output<(X, Y)> Tuple(Input item1, Input item2) + => Tuple(item1, item2, 0).Apply(v => (v.Item1, v.Item2)); + + /// + /// Combines all the values in the provided parameters and combines + /// them all into a single tuple containing each of their underlying values. If any of the + /// s are not known, the final result will be not known. Similarly, + /// if any of the s are secrets, then the final result will be a + /// secret. + /// + public static Output<(X, Y, Z)> Tuple(Input item1, Input item2, Input item3) + => Output<(X, Y, Z)>.Tuple(item1, item2, item3); + + /// + /// Takes in a with potential s or + /// in the 'placeholder holes'. Conceptually, this method unwraps + /// all the underlying values in the holes, combines them appropriately with the string, and produces an + /// containing the final result. + /// + /// If any of the s or s are not known, the + /// final result will be not known. Similarly, if any of the s or + /// s are secrets, then the final result will be a secret. + /// + public static Output Format(FormattableString formattableString) + { + var arguments = formattableString.GetArguments(); + var inputs = new Input[arguments.Length]; + + for (var i = 0; i < arguments.Length; i++) + { + var arg = arguments[i]; + inputs[i] = arg.ToObjectOutput(); + } + + return All(inputs).Apply(objs => + string.Format(formattableString.Format, objs.ToArray())); + } + + internal static Output> Concat(Output> values1, Output> values2) + => Tuple(values1, values2).Apply(a => a.Item1.AddRange(a.Item2)); + } + + /// + /// Internal interface to allow our code to operate on outputs in an untyped manner. Necessary + /// as there is no reasonable way to write algorithms over heterogeneous instantiations of + /// generic types. + /// + internal interface IOutput + { + ImmutableHashSet Resources { get; } + + /// + /// Returns an equivalent to this, except with our + /// casted to an object. + /// + Task> GetDataAsync(); + } + + /// + /// s are a key part of how Pulumi tracks dependencies between s. Because the values of outputs are not available until resources are + /// created, these are represented using the special s type, which + /// internally represents two things: + /// + /// An eventually available value of the output + /// The dependency on the source(s) of the output value + /// + /// In fact, s is quite similar to . + /// Additionally, they carry along dependency information. + /// + /// The output properties of all resource objects in Pulumi have type . + /// + public sealed class Output : IOutput + { + internal ImmutableHashSet Resources { get; private set; } + internal Task> DataTask { get; private set; } + + internal Output(ImmutableHashSet resources, Task> dataTask) + { + Resources = resources; + DataTask = dataTask; + } + + internal async Task GetValueAsync() + { + var data = await DataTask.ConfigureAwait(false); + return data.Value; + } + + ImmutableHashSet IOutput.Resources => this.Resources; + + async Task> IOutput.GetDataAsync() + => await DataTask.ConfigureAwait(false); + + public static Output Create(Task value) + => Create(value, isSecret: false); + + internal static Output CreateSecret(Task value) + => Create(value, isSecret: true); + + private static Output Create(Task value, bool isSecret) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + var tcs = new TaskCompletionSource>(); + value.Assign(tcs, t => OutputData.Create(t, isKnown: true, isSecret: isSecret)); + return new Output(ImmutableHashSet.Empty, tcs.Task); + } + + /// + /// for more details. + /// + public Output Apply(Func func) + => Apply(t => Output.Create(func(t))); + + /// + /// for more details. + /// + public Output Apply(Func> func) + => Apply(t => Output.Create(func(t))); + + /// + /// Transforms the data of this with the provided . The result remains an so that dependent resources + /// can be properly tracked. + /// + /// is not allowed to make resources. + /// + /// can return other s. This can be handy if + /// you have an Output<SomeType> and you want to get a transitive dependency of + /// it. i.e.: + /// + /// + /// Output<SomeType> d1 = ...; + /// Output<OtherType> d2 = d1.Apply(v => v.OtherOutput); // getting an output off of 'v'. + /// + /// + /// In this example, taking a dependency on d2 means a resource will depend on all the resources + /// of d1. It will not depend on the resources of v.x.y.OtherDep. + /// + /// Importantly, the Resources that d2 feels like it will depend on are the same resources + /// as d1. If you need have multiple s and a single is needed that combines both set of resources, then or + /// should be used instead. + /// + /// This function will only be called execution of a pulumi up request. It will not + /// run during pulumi preview (as the values of resources are of course not known + /// then). + /// + public Output Apply(Func> func) + => new Output(Resources, ApplyHelperAsync(DataTask, func)); + + private static async Task> ApplyHelperAsync( + Task> dataTask, Func> func) + { + var data = await dataTask.ConfigureAwait(false); + + // During previews only perform the apply if the engine was able to + // give us an actual value for this Output. + if (!data.IsKnown && Deployment.Instance.IsDryRun) + { + return new OutputData(default!, isKnown: false, data.IsSecret); + } + + var inner = func(data.Value); + var innerData = await inner.DataTask.ConfigureAwait(false); + + return OutputData.Create( + innerData.Value, data.IsKnown && innerData.IsKnown, data.IsSecret || innerData.IsSecret); + } + + internal static Output> All(ImmutableArray> inputs) + => new Output>(GetAllResources(inputs), AllHelperAsync(inputs)); + + private static async Task>> AllHelperAsync(ImmutableArray> inputs) + { + var values = ImmutableArray.CreateBuilder(inputs.Length); + var isKnown = true; + var isSecret = false; + foreach (var input in inputs) + { + var output = (Output)input; + var data = await output.DataTask.ConfigureAwait(false); + + values.Add(data.Value); + (isKnown, isSecret) = OutputData.Combine(data, isKnown, isSecret); + } + + return OutputData.Create(values.MoveToImmutable(), isKnown, isSecret); + } + + internal static Output<(X, Y, Z)> Tuple(Input item1, Input item2, Input item3) + => new Output<(X, Y, Z)>( + GetAllResources(new IInput[] { item1, item2, item3 }), + TupleHelperAsync(item1, item2, item3)); + + private static ImmutableHashSet GetAllResources(IEnumerable inputs) + => ImmutableHashSet.CreateRange(inputs.SelectMany(i => i.ToOutput().Resources)); + + private static async Task> TupleHelperAsync(Input item1, Input item2, Input item3) + { + (X, Y, Z) tuple; + var isKnown = true; + var isSecret = false; + + { + var output = (Output)item1; + var data = await output.DataTask.ConfigureAwait(false); + tuple.Item1 = data.Value; + (isKnown, isSecret) = OutputData.Combine(data, isKnown, isSecret); + } + + { + var output = (Output)item2; + var data = await output.DataTask.ConfigureAwait(false); + tuple.Item2 = data.Value; + (isKnown, isSecret) = OutputData.Combine(data, isKnown, isSecret); + } + + { + var output = (Output)item3; + var data = await output.DataTask.ConfigureAwait(false); + tuple.Item3 = data.Value; + (isKnown, isSecret) = OutputData.Combine(data, isKnown, isSecret); + } + + return OutputData.Create(tuple, isKnown, isSecret); + } + } +} diff --git a/sdk/dotnet/Pulumi/Core/Urn.cs b/sdk/dotnet/Pulumi/Core/Urn.cs new file mode 100644 index 000000000..59246d216 --- /dev/null +++ b/sdk/dotnet/Pulumi/Core/Urn.cs @@ -0,0 +1,81 @@ +// Copyright 2016-2018, Pulumi Corporation + +using System; + +namespace Pulumi +{ + /// + /// An automatically generated logical URN, used to stably identify resources. These are created + /// automatically by Pulumi to identify resources. They cannot be manually constructed. + /// + internal static class Urn + { + /// + /// Computes a URN from the combination of a resource name, resource type, optional parent, + /// optional project and optional stack. + /// + /// + internal static Output Create( + Input name, Input type, + Resource? parent, Input? parentUrn, + Input? project, Input? stack) + { + if (parent != null && parentUrn != null) + throw new ArgumentException("Only one of 'parent' and 'parentUrn' can be non-null."); + + Output parentPrefix; + if (parent != null || parentUrn != null) + { + var parentUrnOutput = parent != null + ? parent.Urn + : parentUrn!.ToOutput(); + + parentPrefix = parentUrnOutput.Apply( + parentUrnString => parentUrnString.Substring( + 0, parentUrnString.LastIndexOf("::", StringComparison.Ordinal)) + "$"); + } + else + { + parentPrefix = Output.Create($"urn:pulumi:{stack ?? Deployment.Instance.StackName}::{project ?? Deployment.Instance.ProjectName}::"); + } + + return Output.Format($"{parentPrefix}{type}::{name}"); + } + + /// + /// computes the alias that should be applied to a child + /// based on an alias applied to it's parent. This may involve changing the name of the + /// resource in cases where the resource has a named derived from the name of the parent, + /// and the parent name changed. + /// + internal static Output InheritedChildAlias(string childName, string parentName, Input parentAlias, string childType) + { + // If the child name has the parent name as a prefix, then we make the assumption that + // it was constructed from the convention of using '{name}-details' as the name of the + // child resource. To ensure this is aliased correctly, we must then also replace the + // parent aliases name in the prefix of the child resource name. + // + // For example: + // * name: "newapp-function" + // * options.parent.__name: "newapp" + // * parentAlias: "urn:pulumi:stackname::projectname::awsx:ec2:Vpc::app" + // * parentAliasName: "app" + // * aliasName: "app-function" + // * childAlias: "urn:pulumi:stackname::projectname::aws:s3/bucket:Bucket::app-function" + var aliasName = Output.Create(childName); + if (childName!.StartsWith(parentName, StringComparison.Ordinal)) + { + aliasName = parentAlias.ToOutput().Apply(parentAliasUrn => + { + return parentAliasUrn.Substring(parentAliasUrn.LastIndexOf("::", StringComparison.Ordinal) + 2) + + childName.Substring(parentName.Length); + }); + } + + var urn = Create( + aliasName, childType, parent: null, + parentUrn: parentAlias, project: null, stack: null); + return urn.Apply(u => new Alias { Urn = u }); + } + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment.Logger.cs b/sdk/dotnet/Pulumi/Deployment/Deployment.Logger.cs new file mode 100644 index 000000000..346c2a8ac --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/Deployment.Logger.cs @@ -0,0 +1,155 @@ +// Copyright 2016-2018, Pulumi Corporation + +using System; +using System.Threading; +using System.Threading.Tasks; +using Pulumirpc; + +namespace Pulumi +{ + public sealed partial class Deployment + { + private class Logger : ILogger + { + private readonly object _logGate = new object(); + private readonly IDeploymentInternal _deployment; + private readonly Engine.EngineClient _engine; + + // We serialize all logging tasks so that the engine doesn't hear about them out of order. + // This is necessary for streaming logs to be maintained in the right order. + private Task _lastLogTask = Task.CompletedTask; + private int _errorCount; + + public Logger(IDeploymentInternal deployment, Engine.EngineClient engine) + { + _deployment = deployment; + _engine = engine; + } + + public bool LoggedErrors + { + get + { + lock (_logGate) + { + return _errorCount > 0; + } + } + } + + /// + /// Logs a debug-level message that is generally hidden from end-users. + /// + Task ILogger.DebugAsync(string message, Resource? resource, int? streamId, bool? ephemeral) + { + Serilog.Log.Debug(message); + return LogImplAsync(LogSeverity.Debug, message, resource, streamId, ephemeral); + } + + /// + /// Logs an informational message that is generally printed to stdout during resource + /// operations. + /// + Task ILogger.InfoAsync(string message, Resource? resource, int? streamId, bool? ephemeral) + { + Serilog.Log.Information(message); + return LogImplAsync(LogSeverity.Info, message, resource, streamId, ephemeral); + } + + /// + /// Warn logs a warning to indicate that something went wrong, but not catastrophically so. + /// + Task ILogger.WarnAsync(string message, Resource? resource, int? streamId, bool? ephemeral) + { + Serilog.Log.Warning(message); + return LogImplAsync(LogSeverity.Warning, message, resource, streamId, ephemeral); + } + + /// + /// Error logs a fatal error to indicate that the tool should stop processing resource + /// operations immediately. + /// + Task ILogger.ErrorAsync(string message, Resource? resource, int? streamId, bool? ephemeral) + => ErrorAsync(message, resource, streamId, ephemeral); + + private Task ErrorAsync(string message, Resource? resource = null, int? streamId = null, bool? ephemeral = null) + { + Serilog.Log.Error(message); + return LogImplAsync(LogSeverity.Error, message, resource, streamId, ephemeral); + } + + private Task LogImplAsync(LogSeverity severity, string message, Resource? resource, int? streamId, bool? ephemeral) + { + // Serialize our logging tasks so that streaming logs appear in order. + Task task; + lock (_logGate) + { + if (severity == LogSeverity.Error) + _errorCount++; + + // Use a Task.Run here so that we don't end up aggressively running the actual + // logging while holding this lock. + _lastLogTask = _lastLogTask.ContinueWith( + _ => Task.Run(() => LogAsync(severity, message, resource, streamId, ephemeral)), + CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default).Unwrap(); + task = _lastLogTask; + } + + _deployment.Runner.RegisterTask($"Log: {severity}: {message}", task); + return task; + } + + private async Task LogAsync(LogSeverity severity, string message, Resource? resource, int? streamId, bool? ephemeral) + { + try + { + var urn = await TryGetResourceUrnAsync(resource).ConfigureAwait(false); + + await _engine.LogAsync(new LogRequest + { + Severity = severity, + Message = message, + Urn = urn, + StreamId = streamId ?? 0, + Ephemeral = ephemeral ?? false, + }); + } + catch (Exception e) + { + lock (_logGate) + { + // mark that we had an error so that our top level process quits with an error + // code. + _errorCount++; + } + + // we have a potential pathological case with logging. Consider if logging a + // message itself throws an error. If we then allow the error to bubble up, our top + // level handler will try to log that error, which can potentially lead to an error + // repeating unendingly. So, to prevent that from happening, we report a very specific + // exception that the top level can know about and handle specially. + throw new LogException(e); + } + } + + private static async Task TryGetResourceUrnAsync(Resource? resource) + { + if (resource != null) + { + try + { + return await resource.Urn.GetValueAsync().ConfigureAwait(false); + } + catch + { + // getting the urn for a resource may itself fail. in that case we don't want to + // fail to send an logging message. we'll just send the logging message unassociated + // with any resource. + } + } + + return ""; + } + } + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment.Runner.cs b/sdk/dotnet/Pulumi/Deployment/Deployment.Runner.cs new file mode 100644 index 000000000..3fc5825db --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/Deployment.Runner.cs @@ -0,0 +1,132 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Pulumi +{ + public partial class Deployment + { + private class Runner : IRunner + { + private readonly IDeploymentInternal _deployment; + + public Runner(IDeploymentInternal deployment) + => _deployment = deployment; + + public Task RunAsync(Func>> func) + { + var stack = new Stack(func); + RegisterTask("User program code.", stack.Outputs.DataTask); + return WhileRunningAsync(); + } + + public void RegisterTask(string description, Task task) + { + lock (_taskToDescription) + { + _taskToDescription.Add(task, description); + } + } + + // Keep track if we already logged the information about an unhandled error to the user. If + // so, we end with a different exit code. The language host recognizes this and will not print + // any further messages to the user since we already took care of it. + // + // 32 was picked so as to be very unlikely to collide with any other error codes. + private const int _processExitedAfterLoggingUserActionableMessage = 32; + + private readonly Dictionary _taskToDescription = new Dictionary(); + + private async Task WhileRunningAsync() + { + var tasks = new List(); + + // Keep looping as long as there are outstanding tasks that are still running. + while (true) + { + tasks.Clear(); + lock (_taskToDescription) + { + if (_taskToDescription.Count == 0) + { + break; + } + + // grab all the tasks we currently have running. + tasks.AddRange(_taskToDescription.Keys); + } + + // Now, wait for one of them to finish. + var task = await Task.WhenAny(tasks).ConfigureAwait(false); + string description; + lock (_taskToDescription) + { + // once finished, remove it from the set of tasks that are running. + description = _taskToDescription[task]; + _taskToDescription.Remove(task); + } + + try + { + // Now actually await that completed task so that we will realize any exceptions + // is may have thrown. + await task.ConfigureAwait(false); + } + catch (Exception e) + { + // if it threw, report it as necessary, then quit. + return await HandleExceptionAsync(e).ConfigureAwait(false); + } + } + + // there were no more tasks we were waiting on. Quit out, reporting if we had any + // errors or not. + return _deployment.Logger.LoggedErrors ? 1 : 0; + } + + private async Task HandleExceptionAsync(Exception exception) + { + if (exception is LogException) + { + // We got an error while logging itself. Nothing to do here but print some errors + // and fail entirely. + Serilog.Log.Error(exception, "Error occurred trying to send logging message to engine."); + await Console.Error.WriteLineAsync("Error occurred trying to send logging message to engine:\n" + exception).ConfigureAwait(false); + return 1; + } + + // For the rest of the issue we encounter log the problem to the error stream. if we + // successfully do this, then return with a special error code stating as such so that + // our host doesn't print out another set of errors. + // + // Note: if these logging calls fail, they will just end up bubbling up an exception + // that will be caught by nothing. This will tear down the actual process with a + // non-zero error which our host will handle properly. + if (exception is RunException) + { + // Always hide the stack for RunErrors. + await _deployment.Logger.ErrorAsync(exception.Message).ConfigureAwait(false); + } + else if (exception is ResourceException resourceEx) + { + var message = resourceEx.HideStack + ? resourceEx.Message + : resourceEx.ToString(); + await _deployment.Logger.ErrorAsync(message, resourceEx.Resource).ConfigureAwait(false); + } + else + { + var location = System.Reflection.Assembly.GetEntryAssembly()?.Location; + await _deployment.Logger.ErrorAsync( + $@"Running program '{location}' failed with an unhandled exception: +{exception.ToString()}").ConfigureAwait(false); + } + + Serilog.Log.Debug("Wrote last error. Returning from program."); + return _processExitedAfterLoggingUserActionableMessage; + } + } + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment.cs b/sdk/dotnet/Pulumi/Deployment/Deployment.cs new file mode 100644 index 000000000..940ae4cb5 --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/Deployment.cs @@ -0,0 +1,136 @@ +// Copyright 2016-2018, Pulumi Corporation + +using System; +using System.Threading.Tasks; +using Grpc.Core; +using Microsoft.Win32.SafeHandles; +using Pulumirpc; + +namespace Pulumi +{ + /// + /// is the entry-point to a Pulumi application. .NET applications + /// should perform all startup logic they need in their Main method and then end with: + /// + /// + /// static Task<int> Main(string[] args) + /// { + /// // program initialization code ... + /// + /// return Deployment.Run(async () => + /// { + /// // Code that creates resources. + /// }); + /// } + /// + /// + /// Importantly: Cloud resources cannot be created outside of the lambda passed to any of the + /// overloads. Because cloud Resource construction is + /// inherently asynchronous, the result of this function is a which should + /// then be returned or awaited. This will ensure that any problems that are encountered during + /// the running of the program are properly reported. Failure to do this may lead to the + /// program ending early before all resources are properly registered. + /// + public sealed partial class Deployment : IDeploymentInternal + { + private static IDeployment? _instance; + + /// + /// The current running deployment instance. This is only available from inside the function + /// passed to (or its overloads). + /// + public static IDeployment Instance + { + get => _instance ?? throw new InvalidOperationException("Trying to acquire Deployment.Instance before 'Run' was called."); + internal set => _instance = (value ?? throw new ArgumentNullException(nameof(value))); + } + + internal static IDeploymentInternal InternalInstance + => (IDeploymentInternal)Instance; + + private readonly Options _options; + private readonly string _projectName; + private readonly string _stackName; + private readonly bool _isDryRun; + + private readonly ILogger _logger; + private readonly IRunner _runner; + + internal Engine.EngineClient Engine { get; } + internal ResourceMonitor.ResourceMonitorClient Monitor { get; } + + internal Stack? _stack; + internal Stack Stack + { + get => _stack ?? throw new InvalidOperationException("Trying to acquire Deployment.Stack before 'Run' was called."); + set => _stack = (value ?? throw new ArgumentNullException(nameof(value))); + } + + private Deployment() + { + var monitor = Environment.GetEnvironmentVariable("PULUMI_MONITOR"); + var engine = Environment.GetEnvironmentVariable("PULUMI_ENGINE"); + var project = Environment.GetEnvironmentVariable("PULUMI_PROJECT"); + var stack = Environment.GetEnvironmentVariable("PULUMI_STACK"); + var pwd = Environment.GetEnvironmentVariable("PULUMI_PWD"); + var dryRun = Environment.GetEnvironmentVariable("PULUMI_DRY_RUN"); + var queryMode = Environment.GetEnvironmentVariable("PULUMI_QUERY_MODE"); + var parallel = Environment.GetEnvironmentVariable("PULUMI_PARALLEL"); + var tracing = Environment.GetEnvironmentVariable("PULUMI_TRACING"); + + if (string.IsNullOrEmpty(monitor)) + throw new InvalidOperationException("Environment did not contain: PULUMI_MONITOR"); + + if (string.IsNullOrEmpty(engine)) + throw new InvalidOperationException("Environment did not contain: PULUMI_ENGINE"); + + if (string.IsNullOrEmpty(project)) + throw new InvalidOperationException("Environment did not contain: PULUMI_PROJECT"); + + if (string.IsNullOrEmpty(stack)) + throw new InvalidOperationException("Environment did not contain: PULUMI_STACK"); + + if (!bool.TryParse(dryRun, out var dryRunValue)) + throw new InvalidOperationException("Environment did not contain a valid bool value for: PULUMI_DRY_RUN"); + + if (!bool.TryParse(queryMode, out var queryModeValue)) + throw new InvalidOperationException("Environment did not contain a valid bool value for: PULUMI_QUERY_MODE"); + + if (!int.TryParse(parallel, out var parallelValue)) + throw new InvalidOperationException("Environment did not contain a valid int value for: PULUMI_PARALLEL"); + + _isDryRun = dryRunValue; + _stackName = stack; + _projectName = project; + + _options = new Options( + queryMode: queryModeValue, parallel: parallelValue, pwd: pwd, + monitor: monitor, engine: engine, tracing: tracing); + + Serilog.Log.Debug("Creating Deployment Engine."); + this.Engine = new Engine.EngineClient(new Channel(engine, ChannelCredentials.Insecure)); + Serilog.Log.Debug("Created Deployment Engine."); + + Serilog.Log.Debug("Creating Deployment Monitor."); + this.Monitor = new ResourceMonitor.ResourceMonitorClient(new Channel(monitor, ChannelCredentials.Insecure)); + Serilog.Log.Debug("Created Deployment Monitor."); + + _runner = new Runner(this); + _logger = new Logger(this, this.Engine); + } + + string IDeployment.ProjectName => _projectName; + string IDeployment.StackName => _stackName; + bool IDeployment.IsDryRun => _isDryRun; + + Options IDeploymentInternal.Options => _options; + ILogger IDeploymentInternal.Logger => _logger; + IRunner IDeploymentInternal.Runner => _runner; + + Stack IDeploymentInternal.Stack + { + get => Stack; + set => Stack = value; + } + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_Complete.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_Complete.cs new file mode 100644 index 000000000..75e29cf23 --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_Complete.cs @@ -0,0 +1,95 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Google.Protobuf.WellKnownTypes; +using Pulumi.Serialization; + +namespace Pulumi +{ + public partial class Deployment + { + /// + /// Executes the provided and then completes all the + /// sources on the with + /// the results of it. + /// + private Task CompleteResourceAsync( + Resource resource, Func> action) + { + // IMPORTANT! This function is Task-returning, but must not actually be `async` itself. + // We have to make sure we run 'OutputCompletionSource.GetSources' synchronously + // directly when `resource`'s constructor runs since this will set all of the + // `[Output(...)] Output` properties. We need those properties assigned by the time + // the base 'Resource' constructor finishes so that both derived classes and external + // consumers can use the Output properties of `resource`. + var completionSources = OutputCompletionSource.GetSources(resource); + + return CompleteResourceAsync(resource, action, completionSources); + } + + private async Task CompleteResourceAsync( + Resource resource, Func> action, + ImmutableDictionary completionSources) + { + // Run in a try/catch/finally so that we always resolve all the outputs of the resource + // regardless of whether we encounter an errors computing the action. + try + { + var response = await action().ConfigureAwait(false); + completionSources[Constants.UrnPropertyName].SetStringValue(response.urn, isKnown: true); + if (resource is CustomResource customResource) + { + var id = response.id ?? ""; + completionSources[Constants.IdPropertyName].SetStringValue(id, isKnown: id != ""); + } + + // Go through all our output fields and lookup a corresponding value in the response + // object. Allow the output field to deserialize the response. + foreach (var (fieldName, completionSource) in completionSources) + { + if (fieldName == Constants.UrnPropertyName || fieldName == Constants.IdPropertyName) + { + // Already handled specially above. + continue; + } + + // We process and deserialize each field (instead of bulk processing + // 'response.data' so that each field can have independent isKnown/isSecret + // values. We do not want to bubble up isKnown/isSecret from one field to the + // rest. + if (response.data.Fields.TryGetValue(fieldName, out var value)) + { + var converted = Converter.ConvertValue( + $"{resource.GetType().FullName}.{fieldName}", value, completionSource.TargetType); + completionSource.SetValue(converted); + } + } + } + catch (Exception e) + { + // Mark any unresolved output properties with this exception. That way we don't + // leave any outstanding tasks sitting around which might cause hangs. + foreach (var source in completionSources.Values) + { + source.TrySetException(e); + } + + throw; + } + finally + { + // ensure that we've at least resolved all our completion sources. That way we + // don't leave any outstanding tasks sitting around which might cause hangs. + foreach (var source in completionSources.Values) + { + // Didn't get a value for this field. Resolve it with a default value. + // If we're in preview, we'll consider this unknown and in a normal + // update we'll consider it known. + source.TrySetDefaultResult(isKnown: !_isDryRun); + } + } + } + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_Config.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_Config.cs new file mode 100644 index 000000000..355e573f0 --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_Config.cs @@ -0,0 +1,70 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections.Immutable; +using System.Text.Json; + +namespace Pulumi +{ + public partial class Deployment + { + /// + /// The environment variable key that the language plugin uses to set configuration values. + /// + private const string _configEnvKey = "PULUMI_CONFIG"; + + /// + /// Returns a copy of the full config map. + /// + internal ImmutableDictionary AllConfig { get; private set; } = ParseConfig(); + + /// + /// Sets a configuration variable. + /// + internal void SetConfig(string key, string value) + => AllConfig = AllConfig.Add(key, value); + + /// + /// Returns a configuration variable's value or if it is unset. + /// + string? IDeploymentInternal.GetConfig(string key) + => AllConfig.TryGetValue(key, out var value) ? value : null; + + private static ImmutableDictionary ParseConfig() + { + var parsedConfig = ImmutableDictionary.CreateBuilder(); + var envConfig = Environment.GetEnvironmentVariable(_configEnvKey); + + if (envConfig != null) + { + var envObject = JsonDocument.Parse(envConfig); + foreach (var prop in envObject.RootElement.EnumerateObject()) + { + parsedConfig[CleanKey(prop.Name)] = prop.Value.ToString(); + } + } + + return parsedConfig.ToImmutable(); + } + + /// + /// CleanKey takes a configuration key, and if it is of the form "(string):config:(string)" + /// removes the ":config:" portion. Previously, our keys always had the string ":config:" in + /// them, and we'd like to remove it. However, the language host needs to continue to set it + /// so we can be compatible with older versions of our packages. Once we stop supporting + /// older packages, we can change the language host to not add this :config: thing and + /// remove this function. + /// + private static string CleanKey(string key) + { + var idx = key.IndexOf(":", StringComparison.Ordinal); + + if (idx > 0 && key.Substring(idx + 1).StartsWith("config:", StringComparison.Ordinal)) + { + return key.Substring(0, idx) + ":" + key.Substring(idx + 1 + "config:".Length); + } + + return key; + } + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_Invoke.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_Invoke.cs new file mode 100644 index 000000000..a755cf685 --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_Invoke.cs @@ -0,0 +1,77 @@ +// Copyright 2016-2018, Pulumi Corporation + +using System; +using System.Threading.Tasks; +using Google.Protobuf.WellKnownTypes; +using Pulumi.Serialization; +using Pulumirpc; + +namespace Pulumi +{ + public sealed partial class Deployment + { + Task IDeployment.InvokeAsync(string token, ResourceArgs args, InvokeOptions? options) + => InvokeAsync(token, args, options, convertResult: false); + + Task IDeployment.InvokeAsync(string token, ResourceArgs args, InvokeOptions? options) + => InvokeAsync(token, args, options, convertResult: true); + + private async Task InvokeAsync( + string token, ResourceArgs args, InvokeOptions? options, bool convertResult) + { + var label = $"Invoking function: token={token} asynchronously"; + Log.Debug(label); + + // Wait for all values to be available, and then perform the RPC. + var argsDict = await args.ToDictionaryAsync().ConfigureAwait(false); + var serialized = await SerializeAllPropertiesAsync($"invoke:{token}", argsDict); + Log.Debug($"Invoke RPC prepared: token={token}" + + (_excessiveDebugOutput ? $", obj={serialized}" : "")); + + var provider = await ProviderResource.RegisterAsync(GetProvider(token, options)).ConfigureAwait(false); + + var result = await this.Monitor.InvokeAsync(new InvokeRequest + { + Tok = token, + Provider = provider ?? "", + Version = options?.Version ?? "", + Args = serialized, + }); + + if (result.Failures.Count > 0) + { + var reasons = ""; + foreach (var reason in result.Failures) + { + if (reasons != "") + { + reasons += "; "; + } + + reasons += $"{reason.Reason} ({reason.Property})"; + } + + throw new InvokeException($"Invoke of '{token}' failed: {reasons}"); + } + + if (!convertResult) + { + return default!; + } + + var data = Converter.ConvertValue($"{token} result", new Value { StructValue = result.Return }); + return data.Value; + } + + private static ProviderResource? GetProvider(string token, InvokeOptions? options) + => options?.Provider ?? options?.Parent?.GetProvider(token); + + private class InvokeException : Exception + { + public InvokeException(string error) + : base(error) + { + } + } + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_Prepare.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_Prepare.cs new file mode 100644 index 000000000..489a2a061 --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_Prepare.cs @@ -0,0 +1,173 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Google.Protobuf.WellKnownTypes; + +namespace Pulumi +{ + public partial class Deployment + { + private async Task PrepareResourceAsync( + string label, Resource res, bool custom, + ResourceArgs args, ResourceOptions options) + { + /* IMPORTANT! We should never await prior to this line, otherwise the Resource will be partly uninitialized. */ + + // Before we can proceed, all our dependencies must be finished. + var type = res.GetResourceType(); + var name = res.GetResourceName(); + + Log.Debug($"Gathering explicit dependencies: t={type}, name={name}, custom={custom}"); + var explicitDirectDependencies = new HashSet( + await GatherExplicitDependenciesAsync(options.DependsOn).ConfigureAwait(false)); + Log.Debug($"Gathered explicit dependencies: t={type}, name={name}, custom={custom}"); + + // Serialize out all our props to their final values. In doing so, we'll also collect all + // the Resources pointed to by any Dependency objects we encounter, adding them to 'propertyDependencies'. + Log.Debug($"Serializing properties: t={type}, name={name}, custom={custom}"); + var dictionary = await args.ToDictionaryAsync().ConfigureAwait(false); + var (serializedProps, propertyToDirectDependencies) = + await SerializeResourcePropertiesAsync(label, dictionary).ConfigureAwait(false); + Log.Debug($"Serialized properties: t={type}, name={name}, custom={custom}"); + + // Wait for the parent to complete. + // If no parent was provided, parent to the root resource. + Log.Debug($"Getting parent urn: t={type}, name={name}, custom={custom}"); + var parentURN = options.Parent != null + ? await options.Parent.Urn.GetValueAsync().ConfigureAwait(false) + : await GetRootResourceAsync(type).ConfigureAwait(false); + Log.Debug($"Got parent urn: t={type}, name={name}, custom={custom}"); + + string? providerRef = null; + if (custom) + { + var customOpts = options as CustomResourceOptions; + providerRef = await ProviderResource.RegisterAsync(customOpts?.Provider).ConfigureAwait(false); + } + + // Collect the URNs for explicit/implicit dependencies for the engine so that it can understand + // the dependency graph and optimize operations accordingly. + + // The list of all dependencies (implicit or explicit). + var allDirectDependencies = new HashSet(explicitDirectDependencies); + + var allDirectDependencyURNs = await GetAllTransitivelyReferencedCustomResourceURNsAsync(explicitDirectDependencies).ConfigureAwait(false); + var propertyToDirectDependencyURNs = new Dictionary>(); + + foreach (var (propertyName, directDependencies) in propertyToDirectDependencies) + { + allDirectDependencies.AddRange(directDependencies); + + var urns = await GetAllTransitivelyReferencedCustomResourceURNsAsync(directDependencies).ConfigureAwait(false); + allDirectDependencyURNs.AddRange(urns); + propertyToDirectDependencyURNs[propertyName] = urns; + } + + // Wait for all aliases. Note that we use 'res._aliases' instead of 'options.aliases' as + // the former has been processed in the Resource constructor prior to calling + // 'registerResource' - both adding new inherited aliases and simplifying aliases down + // to URNs. + var aliases = new List(); + var uniqueAliases = new HashSet(); + foreach (var alias in res._aliases) + { + var aliasVal = await alias.ToOutput().GetValueAsync().ConfigureAwait(false); + if (!uniqueAliases.Add(aliasVal)) + { + aliases.Add(aliasVal); + } + } + + return new PrepareResult( + serializedProps, + parentURN, + providerRef, + allDirectDependencyURNs, + propertyToDirectDependencyURNs, + aliases); + } + + private static Task> GatherExplicitDependenciesAsync(InputList resources) + => resources.ToOutput().GetValueAsync(); + + private static async Task> GetAllTransitivelyReferencedCustomResourceURNsAsync( + HashSet resources) + { + // Go through 'resources', but transitively walk through **Component** resources, + // collecting any of their child resources. This way, a Component acts as an + // aggregation really of all the reachable custom resources it parents. This walking + // will transitively walk through other child ComponentResources, but will stop when it + // hits custom resources. in other words, if we had: + // + // Comp1 + // / \ + // Cust1 Comp2 + // / \ + // Cust2 Cust3 + // / + // Cust4 + // + // Then the transitively reachable custom resources of Comp1 will be [Cust1, Cust2, + // Cust3]. It will *not* include 'Cust4'. + + // To do this, first we just get the transitively reachable set of resources (not diving + // into custom resources). In the above picture, if we start with 'Comp1', this will be + // [Comp1, Cust1, Comp2, Cust2, Cust3] + var transitivelyReachableResources = GetTransitivelyReferencedChildResourcesOfComponentResources(resources); + + var transitivelyReachableCustomResources = transitivelyReachableResources.OfType(); + var tasks = transitivelyReachableCustomResources.Select(r => r.Urn.GetValueAsync()); + var urns = await Task.WhenAll(tasks).ConfigureAwait(false); + return new HashSet(urns); + } + + /// + /// Recursively walk the resources passed in, returning them and all resources reachable from + /// through any **Component** resources we encounter. + /// + private static HashSet GetTransitivelyReferencedChildResourcesOfComponentResources(HashSet resources) + { + // Recursively walk the dependent resources through their children, adding them to the result set. + var result = new HashSet(); + AddTransitivelyReferencedChildResourcesOfComponentResources(resources, result); + return result; + } + + private static void AddTransitivelyReferencedChildResourcesOfComponentResources(HashSet resources, HashSet result) + { + foreach (var resource in resources) + { + if (result.Add(resource)) + { + if (resource is ComponentResource) + { + AddTransitivelyReferencedChildResourcesOfComponentResources(resource.ChildResources, result); + } + } + } + } + + private struct PrepareResult + { + public readonly Struct SerializedProps; + public readonly string? ParentUrn; + public readonly string? ProviderRef; + public readonly HashSet AllDirectDependencyURNs; + public readonly Dictionary> PropertyToDirectDependencyURNs; + public readonly List Aliases; + + public PrepareResult(Struct serializedProps, string? parentUrn, string? providerRef, HashSet allDirectDependencyURNs, Dictionary> propertyToDirectDependencyURNs, List aliases) + { + SerializedProps = serializedProps; + ParentUrn = parentUrn; + ProviderRef = providerRef; + AllDirectDependencyURNs = allDirectDependencyURNs; + PropertyToDirectDependencyURNs = propertyToDirectDependencyURNs; + Aliases = aliases; + } + } + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_ReadResource.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_ReadResource.cs new file mode 100644 index 000000000..a52469f8e --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_ReadResource.cs @@ -0,0 +1,70 @@ +// Copyright 2016-2018, Pulumi Corporation + +using System; +using System.Threading.Tasks; +using Google.Protobuf.WellKnownTypes; +using Pulumi.Serialization; +using Pulumirpc; + +namespace Pulumi +{ + public partial class Deployment + { + void IDeploymentInternal.ReadResource( + Resource resource, ResourceArgs args, ResourceOptions options) + { + // ReadResource is called in a fire-and-forget manner. Make sure we keep track of + // this task so that the application will not quit until this async work completes. + // + // Also, we can only do our work once the constructor for the resource has actually + // finished. Otherwise, we might actually read and get the result back *prior* to + // the object finishing initializing. Note: this is not a speculative concern. This is + // something that does happen and has to be accounted for. + _runner.RegisterTask( + $"{nameof(IDeploymentInternal.ReadResource)}: {resource.GetResourceType()}-{resource.GetResourceName()}", + CompleteResourceAsync(resource, () => ReadResourceAsync(resource, args, options))); + } + + private async Task<(string urn, string id, Struct data)> ReadResourceAsync(Resource resource, ResourceArgs args, ResourceOptions options) + { + var id = options.Id; + if (options.Id == null) + { + throw new InvalidOperationException("Cannot read resource whose options are lacking an ID value"); + } + + var name = resource.GetResourceName(); + var type = resource.GetResourceType(); + var label = $"resource:{name}[{type}]#..."; + Log.Debug($"Reading resource: id={(id is IOutput ? "Output" : id)}, t=${type}, name=${name}"); + + var prepareResult = await this.PrepareResourceAsync( + label, resource, custom: true, args, options).ConfigureAwait(false); + + var serializer = new Serializer(_excessiveDebugOutput); + var resolvedID = (string)(await serializer.SerializeAsync(label, id).ConfigureAwait(false))!; + Log.Debug($"ReadResource RPC prepared: id={resolvedID}, t={type}, name={name}" + + (_excessiveDebugOutput ? $", obj={prepareResult.SerializedProps}" : "")); + + // Create a resource request and do the RPC. + var request = new ReadResourceRequest + { + Type = type, + Name = name, + Id = resolvedID, + Parent = prepareResult.ParentUrn, + Provider = prepareResult.ProviderRef, + Properties = prepareResult.SerializedProps, + Version = options?.Version ?? "", + AcceptSecrets = true, + }; + + request.Dependencies.AddRange(prepareResult.AllDirectDependencyURNs); + + // Now run the operation, serializing the invocation if necessary. + var response = await this.Monitor.ReadResourceAsync(request); + + return (response.Urn, resolvedID, response.Properties); + } + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_RegisterResource.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_RegisterResource.cs new file mode 100644 index 000000000..1a0bb52ae --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_RegisterResource.cs @@ -0,0 +1,118 @@ +// Copyright 2016-2018, Pulumi Corporation + +using System; +using System.Threading.Tasks; +using Google.Protobuf.WellKnownTypes; +using Pulumirpc; + +namespace Pulumi +{ + public partial class Deployment + { + void IDeploymentInternal.RegisterResource( + Resource resource, bool custom, ResourceArgs args, ResourceOptions options) + { + // RegisterResource is called in a fire-and-forget manner. Make sure we keep track of + // this task so that the application will not quit until this async work completes. + // + // Also, we can only do our work once the constructor for the resource has actually + // finished. Otherwise, we might actually register and get the result back *prior* to + // the object finishing initializing. Note: this is not a speculative concern. This is + // something that does happen and has to be accounted for. + this._runner.RegisterTask( + $"{nameof(IDeploymentInternal.RegisterResource)}: {resource.GetResourceType()}-{resource.GetResourceName()}", + CompleteResourceAsync(resource, () => RegisterResourceAsync(resource, custom, args, options))); + } + + private async Task<(string urn, string id, Struct data)> RegisterResourceAsync( + Resource resource, bool custom, + ResourceArgs args, ResourceOptions options) + { + var name = resource.GetResourceName(); + var type = resource.GetResourceType(); + + var label = $"resource:{name}[{type}]"; + Log.Debug($"Registering resource start: t={type}, name={name}, custom={custom}"); + + var request = CreateRegisterResourceRequest(type, name, custom, options); + + Log.Debug($"Preparing resource: t={type}, name={name}, custom={custom}"); + var prepareResult = await PrepareResourceAsync(label, resource, custom, args, options).ConfigureAwait(false); + Log.Debug($"Prepared resource: t={type}, name={name}, custom={custom}"); + + PopulateRequest(request, prepareResult); + + Log.Debug($"Registering resource monitor start: t={type}, name={name}, custom={custom}"); + var result = await this.Monitor.RegisterResourceAsync(request); + Log.Debug($"Registering resource monitor end: t={type}, name={name}, custom={custom}"); + return (result.Urn, result.Id, result.Object); + } + + private static void PopulateRequest(RegisterResourceRequest request, PrepareResult prepareResult) + { + request.Object = prepareResult.SerializedProps; + + request.Parent = prepareResult.ParentUrn ?? ""; + request.Provider = prepareResult.ProviderRef ?? ""; + + request.Aliases.AddRange(prepareResult.Aliases); + request.Dependencies.AddRange(prepareResult.AllDirectDependencyURNs); + + foreach (var (key, resourceURNs) in prepareResult.PropertyToDirectDependencyURNs) + { + var deps = new RegisterResourceRequest.Types.PropertyDependencies(); + deps.Urns.AddRange(resourceURNs); + request.PropertyDependencies.Add(key, deps); + } + } + + private static RegisterResourceRequest CreateRegisterResourceRequest(string type, string name, bool custom, ResourceOptions options) + { + var customOpts = options as CustomResourceOptions; + var deleteBeforeReplace = customOpts?.DeleteBeforeReplace; + + var request = new RegisterResourceRequest() + { + Type = type, + Name = name, + Custom = custom, + Protect = options.Protect ?? false, + Version = options.Version ?? "", + ImportId = customOpts?.ImportId ?? "", + AcceptSecrets = true, + DeleteBeforeReplace = deleteBeforeReplace ?? false, + DeleteBeforeReplaceDefined = deleteBeforeReplace != null, + CustomTimeouts = new RegisterResourceRequest.Types.CustomTimeouts + { + Create = TimeoutString(options.CustomTimeouts?.Create), + Delete = TimeoutString(options.CustomTimeouts?.Delete), + Update = TimeoutString(options.CustomTimeouts?.Update), + }, + }; + + if (customOpts != null) + request.AdditionalSecretOutputs.AddRange(customOpts.AdditionalSecretOutputs); + + request.IgnoreChanges.AddRange(options.IgnoreChanges); + + return request; + } + + private static string TimeoutString(TimeSpan? timeSpan) + { + if (timeSpan == null) + return ""; + + // This will eventually be parsed by go's ParseDuration function here: + // https://github.com/pulumi/pulumi/blob/06d4dde8898b2a0de2c3c7ff8e45f97495b89d82/pkg/resource/deploy/source_eval.go#L967 + // + // So we generate a legal duration as allowed by + // https://golang.org/pkg/time/#ParseDuration. + // + // Simply put, we simply convert our ticks to the integral number of nanoseconds + // corresponding to it. Since each tick is 100ns, this can trivialy be done just by + // appending "00" to it. + return timeSpan.Value.Ticks.ToString() + "00ns"; + } + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_RegisterResourceOutputs.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_RegisterResourceOutputs.cs new file mode 100644 index 000000000..57ae5804f --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_RegisterResourceOutputs.cs @@ -0,0 +1,44 @@ +// Copyright 2016-2018, Pulumi Corporation + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Google.Protobuf; +using Pulumirpc; + +namespace Pulumi +{ + public partial class Deployment + { + void IDeploymentInternal.RegisterResourceOutputs(Resource resource, Output> outputs) + { + // RegisterResourceOutputs is called in a fire-and-forget manner. Make sure we keep track of + // this task so that the application will not quit until this async work completes. + _runner.RegisterTask( + $"{nameof(IDeploymentInternal.RegisterResourceOutputs)}: {resource.GetResourceType()}-{resource.GetResourceName()}", + RegisterResourceOutputsAsync(resource, outputs)); + } + + private async Task RegisterResourceOutputsAsync( + Resource resource, Output> outputs) + { + var opLabel = $"monitor.registerResourceOutputs(...)"; + + // The registration could very well still be taking place, so we will need to wait for its URN. + // Additionally, the output properties might have come from other resources, so we must await those too. + var urn = await resource.Urn.GetValueAsync().ConfigureAwait(false); + var props = await outputs.GetValueAsync().ConfigureAwait(false); + var propInputs = props.ToDictionary(kvp => kvp.Key, kvp => (IInput?)(Input)kvp.Value.ToObjectOutput()); + + var serialized = await SerializeAllPropertiesAsync(opLabel, propInputs).ConfigureAwait(false); + Log.Debug($"RegisterResourceOutputs RPC prepared: urn={urn}" + + (_excessiveDebugOutput ? $", outputs ={JsonFormatter.Default.Format(serialized)}" : "")); + + await Monitor.RegisterResourceOutputsAsync(new RegisterResourceOutputsRequest() + { + Urn = urn, + Outputs = serialized, + }); + } + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_RootResource.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_RootResource.cs new file mode 100644 index 000000000..8b3b5ed89 --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_RootResource.cs @@ -0,0 +1,53 @@ +// Copyright 2016-2018, Pulumi Corporation + +using System; +using System.Threading.Tasks; +using Pulumirpc; + +namespace Pulumi +{ + public partial class Deployment + { + private Task? _rootResource; + + /// + /// Returns a root resource URN that will automatically become the default parent of all + /// resources. This can be used to ensure that all resources without explicit parents are + /// parented to a common parent resource. + /// + /// + internal async Task GetRootResourceAsync(string type) + { + // If we're calling this while creating the stack itself. No way to know its urn at + // this point. + if (type == Stack._rootPulumiStackTypeName) + return null; + + if (_rootResource == null) + throw new InvalidOperationException($"Calling {nameof(GetRootResourceAsync)} before the root resource was registered!"); + + return await _rootResource.ConfigureAwait(false); + } + + Task IDeploymentInternal.SetRootResourceAsync(Stack stack) + { + if (_rootResource != null) + throw new InvalidOperationException("Tried to set the root resource more than once!"); + + _rootResource = SetRootResourceWorkerAsync(stack); + return _rootResource; + } + + private async Task SetRootResourceWorkerAsync(Stack stack) + { + var resUrn = await stack.Urn.GetValueAsync().ConfigureAwait(false); + await this.Engine.SetRootResourceAsync(new SetRootResourceRequest + { + Urn = resUrn, + }); + + var getResponse = await this.Engine.GetRootResourceAsync(new GetRootResourceRequest()); + return getResponse.Urn; + } + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_Run.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_Run.cs new file mode 100644 index 000000000..5bff33fb8 --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_Run.cs @@ -0,0 +1,74 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; + +namespace Pulumi +{ + public partial class Deployment + { + /// + /// for more details. + /// + public static Task RunAsync(Action action) + => RunAsync(() => + { + action(); + return ImmutableDictionary.Empty; + }); + + /// + /// for more details. + /// + /// + /// + public static Task RunAsync(Func> func) + => RunAsync(() => Task.FromResult(func())); + + /// + /// is the + /// entry-point to a Pulumi application. .NET applications should perform all startup logic + /// they need in their Main method and then end with: + /// + /// + /// static Task<int> Main(string[] args) + /// { + /// // program initialization code ... + /// + /// return Deployment.Run(async () => + /// { + /// // Code that creates resources. + /// }); + /// } + /// + /// + /// Importantly: Cloud resources cannot be created outside of the lambda passed to any of the + /// overloads. Because cloud Resource construction is + /// inherently asynchronous, the result of this function is a which should + /// then be returned or awaited. This will ensure that any problems that are encountered during + /// the running of the program are properly reported. Failure to do this may lead to the + /// program ending early before all resources are properly registered. + /// + /// The function passed to + /// can optionally return an . The keys and values + /// in this dictionary will become the outputs for the Pulumi Stack that is created. + /// + public static Task RunAsync(Func>> func) + { + // Serilog.Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().CreateLogger(); + + Serilog.Log.Debug("Deployment.Run called."); + if (_instance != null) + { + throw new NotSupportedException("Deployment.Run can only be called a single time."); + } + + Serilog.Log.Debug("Creating new Deployment."); + var deployment = new Deployment(); + Instance = deployment; + return deployment._runner.RunAsync(func); + } + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_Serialization.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_Serialization.cs new file mode 100644 index 000000000..e190b721c --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_Serialization.cs @@ -0,0 +1,92 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Google.Protobuf.WellKnownTypes; +using Pulumi.Serialization; + +namespace Pulumi +{ + public partial class Deployment + { + internal static bool _excessiveDebugOutput = true; + + /// + /// walks the props object passed in, + /// awaiting all interior promises besides those for and , creating a reasonable POCO object that can be remoted over + /// to registerResource. + /// + private static Task SerializeResourcePropertiesAsync( + string label, IDictionary args) + { + return SerializeFilteredPropertiesAsync( + label, Output.Create(args), key => key != Constants.IdPropertyName && key != Constants.UrnPropertyName); + } + + private static async Task SerializeAllPropertiesAsync( + string label, Input> args) + { + var result = await SerializeFilteredPropertiesAsync(label, args, _ => true).ConfigureAwait(false); + return result.Serialized; + } + + /// + /// walks the props object passed in, + /// awaiting all interior promises for properties with keys that match the provided filter, + /// creating a reasonable POCO object that can be remoted over to registerResource. + /// + private static async Task SerializeFilteredPropertiesAsync( + string label, Input> args, Predicate acceptKey) + { + var props = await args.ToOutput().GetValueAsync().ConfigureAwait(false); + + var propertyToDependentResources = ImmutableDictionary.CreateBuilder>(); + var result = ImmutableDictionary.CreateBuilder(); + + foreach (var (key, input) in props) + { + if (acceptKey(key)) + { + // We treat properties with null values as if they do not exist. + var serializer = new Serializer(_excessiveDebugOutput); + var v = await serializer.SerializeAsync($"{label}.{key}", input).ConfigureAwait(false); + if (v != null) + { + result[key] = v; + propertyToDependentResources[key] = serializer.DependentResources; + } + } + } + + return new SerializationResult( + Serializer.CreateStruct(result.ToImmutable()), + propertyToDependentResources.ToImmutable()); + } + + private struct SerializationResult + { + public readonly Struct Serialized; + public readonly ImmutableDictionary> PropertyToDependentResources; + + public SerializationResult( + Struct result, + ImmutableDictionary> propertyToDependentResources) + { + Serialized = result; + PropertyToDependentResources = propertyToDependentResources; + } + + public void Deconstruct( + out Struct serialized, + out ImmutableDictionary> propertyToDependentResources) + { + serialized = Serialized; + propertyToDependentResources = PropertyToDependentResources; + } + } + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/IDeployment.cs b/sdk/dotnet/Pulumi/Deployment/IDeployment.cs new file mode 100644 index 000000000..218e1a68c --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/IDeployment.cs @@ -0,0 +1,42 @@ +// Copyright 2016-2018, Pulumi Corporation + +using System.Threading.Tasks; + +namespace Pulumi +{ + public interface IDeployment + { + /// + /// Returns the current stack name. + /// + string StackName { get; } + + /// + /// Returns the current project name. + /// + string ProjectName { get; } + + /// + /// Whether or not the application is currently being previewed or actually applied. + /// + bool IsDryRun { get; } + + /// + /// Dynamically invokes the function '', which is offered by a + /// provider plugin. + /// + /// The result of will be a resolved to the + /// result value of the provider plugin. + /// + /// The inputs can be a bag of computed values(including, `T`s, + /// s, s etc.). + /// + Task InvokeAsync(string token, ResourceArgs args, InvokeOptions? options = null); + + /// + /// Same as , however the + /// return value is ignored. + /// + Task InvokeAsync(string token, ResourceArgs args, InvokeOptions? options = null); + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/IDeploymentInternal.cs b/sdk/dotnet/Pulumi/Deployment/IDeploymentInternal.cs new file mode 100644 index 000000000..100f4402b --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/IDeploymentInternal.cs @@ -0,0 +1,25 @@ +// Copyright 2016-2018, Pulumi Corporation + +using System.Collections.Generic; +using System.Threading.Tasks; +using Pulumirpc; + +namespace Pulumi +{ + internal interface IDeploymentInternal : IDeployment + { + Options Options { get; } + string? GetConfig(string fullKey); + + Stack Stack { get; set; } + + ILogger Logger { get; } + IRunner Runner { get; } + + Task SetRootResourceAsync(Stack stack); + + void ReadResource(Resource resource, ResourceArgs args, ResourceOptions opts); + void RegisterResource(Resource resource, bool custom, ResourceArgs args, ResourceOptions opts); + void RegisterResourceOutputs(Resource resource, Output> outputs); + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/ILogger.cs b/sdk/dotnet/Pulumi/Deployment/ILogger.cs new file mode 100644 index 000000000..14103995c --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/ILogger.cs @@ -0,0 +1,16 @@ +// Copyright 2016-2018, Pulumi Corporation + +using System.Threading.Tasks; + +namespace Pulumi +{ + internal interface ILogger + { + bool LoggedErrors { get; } + + Task DebugAsync(string message, Resource? resource = null, int? streamId = null, bool? ephemeral = null); + Task InfoAsync(string message, Resource? resource = null, int? streamId = null, bool? ephemeral = null); + Task WarnAsync(string message, Resource? resource = null, int? streamId = null, bool? ephemeral = null); + Task ErrorAsync(string message, Resource? resource = null, int? streamId = null, bool? ephemeral = null); + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/IRunner.cs b/sdk/dotnet/Pulumi/Deployment/IRunner.cs new file mode 100644 index 000000000..171b2e5ba --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/IRunner.cs @@ -0,0 +1,14 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Pulumi +{ + internal interface IRunner + { + void RegisterTask(string description, Task task); + Task RunAsync(Func>> func); + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/InvokeOptions.cs b/sdk/dotnet/Pulumi/Deployment/InvokeOptions.cs new file mode 100644 index 000000000..c8148da22 --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/InvokeOptions.cs @@ -0,0 +1,29 @@ +// Copyright 2016-2018, Pulumi Corporation + +namespace Pulumi +{ + /// + /// Options to help control the behavior of . + /// + public class InvokeOptions + { + /// + /// An optional parent to use for default options for this invoke (e.g. the default provider + /// to use). + /// + public Resource? Parent { get; set; } + + /// + /// An optional provider to use for this invocation. If no provider is supplied, the default + /// provider for the invoked function's package will be used. + /// + public ProviderResource? Provider { get; set; } + + /// + /// An optional version, corresponding to the version of the provider plugin that should be + /// used when performing this invoke. + /// + public string? Version { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi/Exceptions/LogException.cs b/sdk/dotnet/Pulumi/Exceptions/LogException.cs new file mode 100644 index 000000000..7d94fb91e --- /dev/null +++ b/sdk/dotnet/Pulumi/Exceptions/LogException.cs @@ -0,0 +1,19 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; + +namespace Pulumi +{ + /// + /// Special exception we throw if we had a problem actually logging a message to the engine + /// error rpc endpoint. In this case, we have no choice but to tear ourselves down reporting + /// whatever we can to the console instead. + /// + internal class LogException : Exception + { + public LogException(Exception exception) + : base("Error occurred during logging", exception) + { + } + } +} diff --git a/sdk/dotnet/Pulumi/Exceptions/ResourceException.cs b/sdk/dotnet/Pulumi/Exceptions/ResourceException.cs new file mode 100644 index 000000000..c6b98dd56 --- /dev/null +++ b/sdk/dotnet/Pulumi/Exceptions/ResourceException.cs @@ -0,0 +1,24 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; + +namespace Pulumi +{ + /// + /// ResourceException can be used for terminating a program abruptly, specifically associating the + /// problem with a Resource.Depending on the nature of the problem, clients can choose whether + /// or not a call stack should be returned as well. This should be very rare, and would only + /// indicate no usefulness of presenting that stack to the user. + /// + public class ResourceException : Exception + { + internal readonly Resource? Resource; + internal readonly bool HideStack; + + public ResourceException(string message, Resource? resource, bool hideStack = false) : base(message) + { + this.Resource = resource; + this.HideStack = hideStack; + } + } +} diff --git a/sdk/dotnet/Pulumi/Exceptions/RunException.cs b/sdk/dotnet/Pulumi/Exceptions/RunException.cs new file mode 100644 index 000000000..1d1f7e17e --- /dev/null +++ b/sdk/dotnet/Pulumi/Exceptions/RunException.cs @@ -0,0 +1,26 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; + +namespace Pulumi +{ + /// + /// RunException can be used for terminating a program abruptly, but resulting in a clean exit + /// rather than the usual verbose unhandled error logic which emits the source program text and + /// complete stack trace. This type should be rarely used. Ideally should always be used so that as many errors as possible can be + /// associated with a Resource. + /// + internal class RunException : Exception + { + public RunException(string message) + : base(message) + { + } + + public RunException(string message, Exception? innerException) + : base(message, innerException) + { + } + } +} diff --git a/sdk/dotnet/Pulumi/Extensions.cs b/sdk/dotnet/Pulumi/Extensions.cs new file mode 100644 index 000000000..19105bc34 --- /dev/null +++ b/sdk/dotnet/Pulumi/Extensions.cs @@ -0,0 +1,65 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; + +namespace Pulumi +{ + internal static class Extensions + { + public static bool AddRange(this HashSet to, IEnumerable values) + { + var result = false; + foreach (var value in values) + { + result |= to.Add(value); + } + + return result; + } + + public static void Deconstruct(this KeyValuePair pair, out K key, out V value) + { + key = pair.Key; + value = pair.Value; + } + + public static Output ToObjectOutput(this object? obj) + { + var output = obj is IInput input ? input.ToOutput() : obj as IOutput; + return output != null + ? new Output(output.Resources, output.GetDataAsync()) + : Output.Create(obj); + } + + public static ImmutableArray SelectAsArray(this ImmutableArray items, Func map) + => ImmutableArray.CreateRange(items, map); + + public static void Assign( + this Task response, TaskCompletionSource tcs, Func extract) + { + _ = response.ContinueWith(t => + { + switch (t.Status) + { + default: throw new InvalidOperationException("Task was not complete: " + t.Status); + case TaskStatus.Canceled: tcs.SetCanceled(); return; + case TaskStatus.Faulted: tcs.SetException(t.Exception!.InnerExceptions); return; + case TaskStatus.RanToCompletion: + try + { + tcs.SetResult(extract(t.Result)); + } + catch (Exception e) + { + tcs.TrySetException(e); + } + return; + } + }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); + } + } +} diff --git a/sdk/dotnet/Pulumi/Log.cs b/sdk/dotnet/Pulumi/Log.cs new file mode 100644 index 000000000..3426c0a39 --- /dev/null +++ b/sdk/dotnet/Pulumi/Log.cs @@ -0,0 +1,38 @@ +// Copyright 2016-2019, Pulumi Corporation + +namespace Pulumi +{ + /// + /// Logging functions that can be called from a .NET application that will be logged to the + /// Pulumi log stream. These events will be printed in the terminal while the Pulumi app + /// runs, and will be available from the Web console afterwards. + /// + public static class Log + { + /// + /// Logs a debug-level message that is generally hidden from end-users. + /// + public static void Debug(string message, Resource? resource = null, int? streamId = null, bool? ephemeral = null) + => Deployment.InternalInstance.Logger.DebugAsync(message, resource, streamId, ephemeral); + + /// + /// Logs an informational message that is generally printed to stdout during resource + /// operations. + /// + public static void Info(string message, Resource? resource = null, int? streamId = null, bool? ephemeral = null) + => Deployment.InternalInstance.Logger.InfoAsync(message, resource, streamId, ephemeral); + + /// + /// Warn logs a warning to indicate that something went wrong, but not catastrophically so. + /// + public static void Warn(string message, Resource? resource = null, int? streamId = null, bool? ephemeral = null) + => Deployment.InternalInstance.Logger.WarnAsync(message, resource, streamId, ephemeral); + + /// + /// Error logs a fatal error to indicate that the tool should stop processing resource + /// operations immediately. + /// + public static void Error(string message, Resource? resource = null, int? streamId = null, bool? ephemeral = null) + => Deployment.InternalInstance.Logger.ErrorAsync(message, resource, streamId, ephemeral); + } +} diff --git a/sdk/dotnet/Pulumi/PublicAPI.Shipped.txt b/sdk/dotnet/Pulumi/PublicAPI.Shipped.txt new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/sdk/dotnet/Pulumi/PublicAPI.Shipped.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/dotnet/Pulumi/PublicAPI.Unshipped.txt b/sdk/dotnet/Pulumi/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..f1ff7b8a5 --- /dev/null +++ b/sdk/dotnet/Pulumi/PublicAPI.Unshipped.txt @@ -0,0 +1,209 @@ +Pulumi.Alias +Pulumi.Alias.Alias() -> void +Pulumi.Alias.Name.get -> Pulumi.Input +Pulumi.Alias.Name.set -> void +Pulumi.Alias.NoParent.get -> bool +Pulumi.Alias.NoParent.set -> void +Pulumi.Alias.Parent.get -> Pulumi.Resource +Pulumi.Alias.Parent.set -> void +Pulumi.Alias.ParentUrn.get -> Pulumi.Input +Pulumi.Alias.ParentUrn.set -> void +Pulumi.Alias.Project.get -> Pulumi.Input +Pulumi.Alias.Project.set -> void +Pulumi.Alias.Stack.get -> Pulumi.Input +Pulumi.Alias.Stack.set -> void +Pulumi.Alias.Type.get -> Pulumi.Input +Pulumi.Alias.Type.set -> void +Pulumi.Alias.Urn.get -> string +Pulumi.Alias.Urn.set -> void +Pulumi.Archive +Pulumi.Asset +Pulumi.AssetArchive +Pulumi.AssetArchive.AssetArchive(System.Collections.Immutable.ImmutableDictionary assets) -> void +Pulumi.AssetOrArchive +Pulumi.ComponentResource +Pulumi.ComponentResource.ComponentResource(string type, string name, Pulumi.ResourceOptions options = null) -> void +Pulumi.ComponentResource.RegisterOutputs() -> void +Pulumi.ComponentResource.RegisterOutputs(Pulumi.Output> outputs) -> void +Pulumi.ComponentResource.RegisterOutputs(System.Collections.Generic.IDictionary outputs) -> void +Pulumi.ComponentResource.RegisterOutputs(System.Threading.Tasks.Task> outputs) -> void +Pulumi.ComponentResourceOptions +Pulumi.ComponentResourceOptions.ComponentResourceOptions() -> void +Pulumi.ComponentResourceOptions.Providers.get -> System.Collections.Generic.List +Pulumi.ComponentResourceOptions.Providers.set -> void +Pulumi.Config +Pulumi.Config.Config(string name = null) -> void +Pulumi.Config.Get(string key) -> string +Pulumi.Config.GetBoolean(string key) -> bool? +Pulumi.Config.GetInt32(string key) -> int? +Pulumi.Config.GetObject(string key) -> T +Pulumi.Config.GetSecret(string key) -> Pulumi.Output +Pulumi.Config.GetSecretBoolean(string key) -> Pulumi.Output +Pulumi.Config.GetSecretInt32(string key) -> Pulumi.Output +Pulumi.Config.GetSecretObject(string key) -> Pulumi.Output +Pulumi.Config.Require(string key) -> string +Pulumi.Config.RequireBoolean(string key) -> bool +Pulumi.Config.RequireInt32(string key) -> int +Pulumi.Config.RequireObject(string key) -> T +Pulumi.Config.RequireSecret(string key) -> Pulumi.Output +Pulumi.Config.RequireSecretBoolean(string key) -> Pulumi.Output +Pulumi.Config.RequireSecretInt32(string key) -> Pulumi.Output +Pulumi.Config.RequireSecretObject(string key) -> Pulumi.Output +Pulumi.CustomResource +Pulumi.CustomResource.CustomResource(string type, string name, Pulumi.ResourceArgs args, Pulumi.ResourceOptions options = null) -> void +Pulumi.CustomResource.Id.get -> Pulumi.Output +Pulumi.CustomResourceOptions +Pulumi.CustomResourceOptions.AdditionalSecretOutputs.get -> System.Collections.Generic.List +Pulumi.CustomResourceOptions.AdditionalSecretOutputs.set -> void +Pulumi.CustomResourceOptions.CustomResourceOptions() -> void +Pulumi.CustomResourceOptions.DeleteBeforeReplace.get -> bool? +Pulumi.CustomResourceOptions.DeleteBeforeReplace.set -> void +Pulumi.CustomResourceOptions.ImportId.get -> string +Pulumi.CustomResourceOptions.ImportId.set -> void +Pulumi.CustomTimeouts +Pulumi.CustomTimeouts.Create.get -> System.TimeSpan? +Pulumi.CustomTimeouts.Create.set -> void +Pulumi.CustomTimeouts.CustomTimeouts() -> void +Pulumi.CustomTimeouts.Delete.get -> System.TimeSpan? +Pulumi.CustomTimeouts.Delete.set -> void +Pulumi.CustomTimeouts.Update.get -> System.TimeSpan? +Pulumi.CustomTimeouts.Update.set -> void +Pulumi.Deployment +Pulumi.FileArchive +Pulumi.FileArchive.FileArchive(string path) -> void +Pulumi.FileAsset +Pulumi.FileAsset.FileAsset(string path) -> void +Pulumi.IDeployment +Pulumi.IDeployment.InvokeAsync(string token, Pulumi.ResourceArgs args, Pulumi.InvokeOptions options = null) -> System.Threading.Tasks.Task +Pulumi.IDeployment.InvokeAsync(string token, Pulumi.ResourceArgs args, Pulumi.InvokeOptions options = null) -> System.Threading.Tasks.Task +Pulumi.IDeployment.IsDryRun.get -> bool +Pulumi.IDeployment.ProjectName.get -> string +Pulumi.IDeployment.StackName.get -> string +Pulumi.Input +Pulumi.Input.ToOutput() -> Pulumi.Output +Pulumi.InputList +Pulumi.InputList.Add(params Pulumi.Input[] inputs) -> void +Pulumi.InputList.Concat(Pulumi.InputList other) -> Pulumi.InputList +Pulumi.InputList.GetAsyncEnumerator(System.Threading.CancellationToken cancellationToken) -> System.Collections.Generic.IAsyncEnumerator> +Pulumi.InputList.InputList() -> void +Pulumi.InputMap +Pulumi.InputMap.Add(string key, Pulumi.Input value) -> void +Pulumi.InputMap.GetAsyncEnumerator(System.Threading.CancellationToken cancellationToken) -> System.Collections.Generic.IAsyncEnumerator>> +Pulumi.InputMap.InputMap() -> void +Pulumi.InputMap.this[string key].set -> void +Pulumi.InvokeOptions +Pulumi.InvokeOptions.InvokeOptions() -> void +Pulumi.InvokeOptions.Parent.get -> Pulumi.Resource +Pulumi.InvokeOptions.Parent.set -> void +Pulumi.InvokeOptions.Provider.get -> Pulumi.ProviderResource +Pulumi.InvokeOptions.Provider.set -> void +Pulumi.InvokeOptions.Version.get -> string +Pulumi.InvokeOptions.Version.set -> void +Pulumi.Log +Pulumi.Output +Pulumi.Output +Pulumi.Output.Apply(System.Func> func) -> Pulumi.Output +Pulumi.Output.Apply(System.Func> func) -> Pulumi.Output +Pulumi.Output.Apply(System.Func func) -> Pulumi.Output +Pulumi.ProviderResource +Pulumi.ProviderResource.ProviderResource(string package, string name, Pulumi.ResourceArgs args, Pulumi.ResourceOptions options = null) -> void +Pulumi.RemoteArchive +Pulumi.RemoteArchive.RemoteArchive(string uri) -> void +Pulumi.RemoteAsset +Pulumi.RemoteAsset.RemoteAsset(string uri) -> void +Pulumi.Resource +Pulumi.Resource.GetResourceName() -> string +Pulumi.Resource.GetResourceType() -> string +Pulumi.Resource.Urn.get -> Pulumi.Output +Pulumi.ResourceArgs +Pulumi.ResourceArgs.ResourceArgs() -> void +Pulumi.ResourceException +Pulumi.ResourceException.ResourceException(string message, Pulumi.Resource resource, bool hideStack = false) -> void +Pulumi.ResourceOptions +Pulumi.ResourceOptions.Aliases.get -> System.Collections.Generic.List> +Pulumi.ResourceOptions.Aliases.set -> void +Pulumi.ResourceOptions.CustomTimeouts.get -> Pulumi.CustomTimeouts +Pulumi.ResourceOptions.CustomTimeouts.set -> void +Pulumi.ResourceOptions.DependsOn.get -> Pulumi.InputList +Pulumi.ResourceOptions.DependsOn.set -> void +Pulumi.ResourceOptions.Id.get -> Pulumi.Input +Pulumi.ResourceOptions.Id.set -> void +Pulumi.ResourceOptions.IgnoreChanges.get -> System.Collections.Generic.List +Pulumi.ResourceOptions.IgnoreChanges.set -> void +Pulumi.ResourceOptions.Parent.get -> Pulumi.Resource +Pulumi.ResourceOptions.Parent.set -> void +Pulumi.ResourceOptions.Protect.get -> bool? +Pulumi.ResourceOptions.Protect.set -> void +Pulumi.ResourceOptions.Provider.get -> Pulumi.ProviderResource +Pulumi.ResourceOptions.Provider.set -> void +Pulumi.ResourceOptions.ResourceOptions() -> void +Pulumi.ResourceOptions.ResourceTransformations.get -> System.Collections.Generic.List +Pulumi.ResourceOptions.ResourceTransformations.set -> void +Pulumi.ResourceOptions.Version.get -> string +Pulumi.ResourceOptions.Version.set -> void +Pulumi.ResourceTransformation +Pulumi.ResourceTransformationArgs +Pulumi.ResourceTransformationArgs.Args.get -> Pulumi.ResourceArgs +Pulumi.ResourceTransformationArgs.Options.get -> Pulumi.ResourceOptions +Pulumi.ResourceTransformationArgs.Resource.get -> Pulumi.Resource +Pulumi.ResourceTransformationArgs.ResourceTransformationArgs(Pulumi.Resource resource, Pulumi.ResourceArgs args, Pulumi.ResourceOptions options) -> void +Pulumi.ResourceTransformationResult +Pulumi.ResourceTransformationResult.Args.get -> Pulumi.ResourceArgs +Pulumi.ResourceTransformationResult.Options.get -> Pulumi.ResourceOptions +Pulumi.ResourceTransformationResult.ResourceTransformationResult(Pulumi.ResourceArgs args, Pulumi.ResourceOptions options) -> void +Pulumi.Serialization.InputAttribute +Pulumi.Serialization.InputAttribute.InputAttribute(string name, bool required = false, bool json = false) -> void +Pulumi.Serialization.OutputAttribute +Pulumi.Serialization.OutputAttribute.Name.get -> string +Pulumi.Serialization.OutputAttribute.OutputAttribute(string name) -> void +Pulumi.Serialization.OutputConstructorAttribute +Pulumi.Serialization.OutputConstructorAttribute.OutputConstructorAttribute() -> void +Pulumi.Serialization.OutputTypeAttribute +Pulumi.Serialization.OutputTypeAttribute.OutputTypeAttribute() -> void +Pulumi.StringAsset +Pulumi.StringAsset.StringAsset(string text) -> void +static Pulumi.Deployment.Instance.get -> Pulumi.IDeployment +static Pulumi.Deployment.RunAsync(System.Action action) -> System.Threading.Tasks.Task +static Pulumi.Deployment.RunAsync(System.Func> func) -> System.Threading.Tasks.Task +static Pulumi.Deployment.RunAsync(System.Func>> func) -> System.Threading.Tasks.Task +static Pulumi.Input.implicit operator Pulumi.Input(Pulumi.Output value) -> Pulumi.Input +static Pulumi.Input.implicit operator Pulumi.Input(T value) -> Pulumi.Input +static Pulumi.Input.implicit operator Pulumi.Output(Pulumi.Input input) -> Pulumi.Output +static Pulumi.InputList.implicit operator Pulumi.InputList(Pulumi.Input value) -> Pulumi.InputList +static Pulumi.InputList.implicit operator Pulumi.InputList(Pulumi.Input[] values) -> Pulumi.InputList +static Pulumi.InputList.implicit operator Pulumi.InputList(Pulumi.Output> values) -> Pulumi.InputList +static Pulumi.InputList.implicit operator Pulumi.InputList(Pulumi.Output> values) -> Pulumi.InputList +static Pulumi.InputList.implicit operator Pulumi.InputList(Pulumi.Output> values) -> Pulumi.InputList +static Pulumi.InputList.implicit operator Pulumi.InputList(Pulumi.Output value) -> Pulumi.InputList +static Pulumi.InputList.implicit operator Pulumi.InputList(Pulumi.Output[] values) -> Pulumi.InputList +static Pulumi.InputList.implicit operator Pulumi.InputList(Pulumi.Output values) -> Pulumi.InputList +static Pulumi.InputList.implicit operator Pulumi.InputList(System.Collections.Generic.List> values) -> Pulumi.InputList +static Pulumi.InputList.implicit operator Pulumi.InputList(System.Collections.Generic.List> values) -> Pulumi.InputList +static Pulumi.InputList.implicit operator Pulumi.InputList(System.Collections.Generic.List values) -> Pulumi.InputList +static Pulumi.InputList.implicit operator Pulumi.InputList(System.Collections.Immutable.ImmutableArray> values) -> Pulumi.InputList +static Pulumi.InputList.implicit operator Pulumi.InputList(System.Collections.Immutable.ImmutableArray> values) -> Pulumi.InputList +static Pulumi.InputList.implicit operator Pulumi.InputList(System.Collections.Immutable.ImmutableArray values) -> Pulumi.InputList +static Pulumi.InputList.implicit operator Pulumi.InputList(T value) -> Pulumi.InputList +static Pulumi.InputList.implicit operator Pulumi.InputList(T[] values) -> Pulumi.InputList +static Pulumi.InputMap.implicit operator Pulumi.InputMap(Pulumi.Output> values) -> Pulumi.InputMap +static Pulumi.InputMap.implicit operator Pulumi.InputMap(Pulumi.Output> values) -> Pulumi.InputMap +static Pulumi.InputMap.implicit operator Pulumi.InputMap(Pulumi.Output> values) -> Pulumi.InputMap +static Pulumi.InputMap.implicit operator Pulumi.InputMap(System.Collections.Generic.Dictionary values) -> Pulumi.InputMap +static Pulumi.InputMap.implicit operator Pulumi.InputMap(System.Collections.Immutable.ImmutableDictionary values) -> Pulumi.InputMap +static Pulumi.Log.Debug(string message, Pulumi.Resource resource = null, int? streamId = null, bool? ephemeral = null) -> void +static Pulumi.Log.Error(string message, Pulumi.Resource resource = null, int? streamId = null, bool? ephemeral = null) -> void +static Pulumi.Log.Info(string message, Pulumi.Resource resource = null, int? streamId = null, bool? ephemeral = null) -> void +static Pulumi.Log.Warn(string message, Pulumi.Resource resource = null, int? streamId = null, bool? ephemeral = null) -> void +static Pulumi.Output.All(System.Collections.Immutable.ImmutableArray> inputs) -> Pulumi.Output> +static Pulumi.Output.All(params Pulumi.Input[] inputs) -> Pulumi.Output> +static Pulumi.Output.Create(System.Threading.Tasks.Task value) -> Pulumi.Output +static Pulumi.Output.Create(T value) -> Pulumi.Output +static Pulumi.Output.CreateSecret(System.Threading.Tasks.Task value) -> Pulumi.Output +static Pulumi.Output.CreateSecret(T value) -> Pulumi.Output +static Pulumi.Output.Format(System.FormattableString formattableString) -> Pulumi.Output +static Pulumi.Output.Tuple(Pulumi.Input item1, Pulumi.Input item2, Pulumi.Input item3) -> Pulumi.Output<(X, Y, Z)> +static Pulumi.Output.Tuple(Pulumi.Input item1, Pulumi.Input item2) -> Pulumi.Output<(X, Y)> +static Pulumi.Output.Tuple(Pulumi.Output item1, Pulumi.Output item2) -> Pulumi.Output<(X, Y)> +static Pulumi.Output.Create(System.Threading.Tasks.Task value) -> Pulumi.Output +static Pulumi.ResourceOptions.Merge(Pulumi.ResourceOptions options1, Pulumi.ResourceOptions options2) -> Pulumi.ResourceOptions +static readonly Pulumi.ResourceArgs.Empty -> Pulumi.ResourceArgs \ No newline at end of file diff --git a/sdk/dotnet/Pulumi/Pulumi.csproj b/sdk/dotnet/Pulumi/Pulumi.csproj new file mode 100644 index 000000000..1a1a7136a --- /dev/null +++ b/sdk/dotnet/Pulumi/Pulumi.csproj @@ -0,0 +1,62 @@ + + + + true + Pulumi + Pulumi Corp. + The Pulumi .NET SDK lets you write cloud programs in C#, F#, and VB.NET. + https://www.pulumi.com + https://github.com/pulumi/pulumi + Apache-2.0 + pulumi_logo_64x64.png + + netcoreapp3.0 + enable + + + + .\Pulumi.xml + 1701;1702;1591;NU5105 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + True + + + + + diff --git a/sdk/dotnet/Pulumi/Resources/ComponentResource.cs b/sdk/dotnet/Pulumi/Resources/ComponentResource.cs new file mode 100644 index 000000000..56ef4a54d --- /dev/null +++ b/sdk/dotnet/Pulumi/Resources/ComponentResource.cs @@ -0,0 +1,53 @@ +// Copyright 2016-2018, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; + +namespace Pulumi +{ + /// + /// A that aggregates one or more other child resources into a higher + /// level abstraction.The component resource itself is a resource, but does not require custom + /// CRUD operations for provisioning. + /// + public class ComponentResource : Resource + { + /// + /// Creates and registers a new component resource. is the fully + /// qualified type token and is the "name" part to use in creating a + /// stable and globally unique URN for the object. options.parent is the optional parent + /// for this component, and [options.dependsOn] is an optional list of other resources that + /// this resource depends on, controlling the order in which we perform resource operations. + /// +#pragma warning disable RS0022 // Constructor make noninheritable base class inheritable + public ComponentResource(string type, string name, ResourceOptions? options = null) + : base(type, name, custom: false, + args: ResourceArgs.Empty, + options ?? new ComponentResourceOptions()) +#pragma warning restore RS0022 // Constructor make noninheritable base class inheritable + { + } + + /// + /// RegisterOutputs registers synthetic outputs that a component has initialized, usually by + /// allocating other child sub-resources and propagating their resulting property values. + /// ComponentResources should always call this at the end of their constructor to indicate + /// that they are done creating child resources. While not strictly necessary, this helps + /// the experience by ensuring the UI transitions the ComponentResource to the 'complete' + /// state as quickly as possible (instead of waiting until the entire application completes). + /// + protected void RegisterOutputs() + => RegisterOutputs(ImmutableDictionary.Empty); + + protected void RegisterOutputs(IDictionary outputs) + => RegisterOutputs(Task.FromResult(outputs ?? throw new ArgumentNullException(nameof(outputs)))); + + protected void RegisterOutputs(Task> outputs) + => RegisterOutputs(Output.Create(outputs ?? throw new ArgumentNullException(nameof(outputs)))); + + protected void RegisterOutputs(Output> outputs) + => Deployment.InternalInstance.RegisterResourceOutputs(this, outputs ?? throw new ArgumentNullException(nameof(outputs))); + } +} diff --git a/sdk/dotnet/Pulumi/Resources/ComponentResourceOptions.cs b/sdk/dotnet/Pulumi/Resources/ComponentResourceOptions.cs new file mode 100644 index 000000000..ab1684b8b --- /dev/null +++ b/sdk/dotnet/Pulumi/Resources/ComponentResourceOptions.cs @@ -0,0 +1,29 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System.Collections.Generic; + +namespace Pulumi +{ + /// + /// A bag of optional settings that control a 's behavior. + /// + public sealed class ComponentResourceOptions : ResourceOptions + { + private List? _providers; + + /// + /// An optional set of providers to use for child resources. + /// + /// Note: do not provide both and . + /// + public List Providers + { + get => _providers ?? (_providers = new List()); + set => _providers = value; + } + + internal override ResourceOptions Clone() + => CreateComponentResourceOptionsCopy(this); + } +} diff --git a/sdk/dotnet/Pulumi/Resources/CustomResource.cs b/sdk/dotnet/Pulumi/Resources/CustomResource.cs new file mode 100644 index 000000000..99c99bc4d --- /dev/null +++ b/sdk/dotnet/Pulumi/Resources/CustomResource.cs @@ -0,0 +1,43 @@ +// Copyright 2016-2019, Pulumi Corporation + +using Pulumi.Serialization; + +namespace Pulumi +{ + /// + /// CustomResource is a resource whose create, read, update, and delete(CRUD) operations are + /// managed by performing external operations on some physical entity. The engine understands + /// how to diff and perform partial updates of them, and these CRUD operations are implemented + /// in a dynamically loaded plugin for the defining package. + /// + public class CustomResource : Resource + { + /// + /// Id is the provider-assigned unique ID for this managed resource. It is set during + /// deployments and may be missing (unknown) during planning phases. + /// + // Set using reflection, so we silence the NRT warnings with `null!`. + [Output(Constants.IdPropertyName)] + public Output Id { get; private set; } = null!; + + /// + /// Creates and registers a new managed resource. t is the fully qualified type token and + /// name is the "name" part to use in creating a stable and globally unique URN for the + /// object. dependsOn is an optional list of other resources that this resource depends on, + /// controlling the order in which we perform resource operations.Creating an instance does + /// not necessarily perform a create on the physical entity which it represents, and + /// instead, this is dependent upon the diffing of the new goal state compared to the + /// current known resource state. + /// +#pragma warning disable RS0022 // Constructor make noninheritable base class inheritable + public CustomResource(string type, string name, ResourceArgs? args, ResourceOptions? options = null) + : base(type, name, custom: true, args ?? ResourceArgs.Empty, options ?? new ResourceOptions()) +#pragma warning restore RS0022 // Constructor make noninheritable base class inheritable + { + if (options is ComponentResourceOptions componentOpts && componentOpts.Providers != null) + { + throw new ResourceException("Do not supply 'providers' option to a CustomResource. Did you mean 'provider' instead?", this); + } + } + } +} diff --git a/sdk/dotnet/Pulumi/Resources/CustomResourceOptions.cs b/sdk/dotnet/Pulumi/Resources/CustomResourceOptions.cs new file mode 100644 index 000000000..b18ab537d --- /dev/null +++ b/sdk/dotnet/Pulumi/Resources/CustomResourceOptions.cs @@ -0,0 +1,44 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System.Collections.Generic; + +namespace Pulumi +{ + /// + /// is a bag of optional settings that control a 's behavior. + /// + public sealed class CustomResourceOptions : ResourceOptions + { + /// + /// When set to true, indicates that this resource should be deleted before its + /// replacement is created when replacement is necessary. + /// + public bool? DeleteBeforeReplace { get; set; } + + private List? _additionalSecretOutputs; + + /// + /// The names of outputs for this resource that should be treated as secrets. This augments + /// the list that the resource provider and pulumi engine already determine based on inputs + /// to your resource. It can be used to mark certain outputs as a secrets on a per resource + /// basis. + /// + public List AdditionalSecretOutputs + { + get => _additionalSecretOutputs ?? (_additionalSecretOutputs = new List()); + set => _additionalSecretOutputs = value; + } + + /// + /// When provided with a resource ID, import indicates that this resource's provider should + /// import its state from the cloud resource with the given ID.The inputs to the resource's + /// constructor must align with the resource's current state.Once a resource has been + /// imported, the import property must be removed from the resource's options. + /// + public string? ImportId { get; set; } + + internal override ResourceOptions Clone() + => CreateCustomResourceOptionsCopy(this); + } +} diff --git a/sdk/dotnet/Pulumi/Resources/CustomTimeouts.cs b/sdk/dotnet/Pulumi/Resources/CustomTimeouts.cs new file mode 100644 index 000000000..47da165df --- /dev/null +++ b/sdk/dotnet/Pulumi/Resources/CustomTimeouts.cs @@ -0,0 +1,35 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; + +namespace Pulumi +{ + /// + /// Optional timeouts to supply in . + /// + public sealed class CustomTimeouts + { + /// + /// The optional create timeout. + /// + public TimeSpan? Create { get; set; } + + /// + /// The optional update timeout. + /// + public TimeSpan? Update { get; set; } + + /// + /// The optional delete timeout. + /// + public TimeSpan? Delete { get; set; } + + internal static CustomTimeouts? Clone(CustomTimeouts? timeouts) + => timeouts == null ? null : new CustomTimeouts + { + Create = timeouts.Create, + Delete = timeouts.Delete, + Update = timeouts.Update, + }; + } +} diff --git a/sdk/dotnet/Pulumi/Resources/ProviderResource.cs b/sdk/dotnet/Pulumi/Resources/ProviderResource.cs new file mode 100644 index 000000000..c5c65cc9a --- /dev/null +++ b/sdk/dotnet/Pulumi/Resources/ProviderResource.cs @@ -0,0 +1,52 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System.Threading.Tasks; +using Pulumi.Serialization; + +namespace Pulumi +{ + /// + /// is a that implements CRUD operations + /// for other custom resources. These resources are managed similarly to other resources, + /// including the usual diffing and update semantics. + /// + public class ProviderResource : CustomResource + { + internal readonly string Package; + + private string? _registrationId; + + /// + /// Creates and registers a new provider resource for a particular package. + /// + public ProviderResource( + string package, string name, + ResourceArgs args, ResourceOptions? options = null) + : base($"pulumi:providers:{package}", name, args, options) + { + this.Package = package; + } + + internal static async Task RegisterAsync(ProviderResource? provider) + { + if (provider == null) + { + return null; + } + + if (provider._registrationId == null) + { + var providerURN = await provider.Urn.GetValueAsync().ConfigureAwait(false); + var providerID = await provider.Id.GetValueAsync().ConfigureAwait(false); + if (string.IsNullOrEmpty(providerID)) + { + providerID = Constants.UnknownValue; + } + + provider._registrationId = $"{providerURN}::{providerID}"; + } + + return provider._registrationId; + } + } +} diff --git a/sdk/dotnet/Pulumi/Resources/Resource.cs b/sdk/dotnet/Pulumi/Resources/Resource.cs new file mode 100644 index 000000000..127d94118 --- /dev/null +++ b/sdk/dotnet/Pulumi/Resources/Resource.cs @@ -0,0 +1,372 @@ +// Copyright 2016-2018, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Pulumi.Serialization; + +namespace Pulumi +{ + /// + /// Resource represents a class whose CRUD operations are implemented by a provider plugin. + /// + public class Resource + { + private readonly string _type; + private readonly string _name; + + /// + /// The optional parent of this resource. + /// + private readonly Resource? _parentResource; + + /// + /// The child resources of this resource. We use these (only from a ComponentResource) to + /// allow code to dependOn a ComponentResource and have that effectively mean that it is + /// depending on all the CustomResource children of that component. + /// + /// Important! We only walk through ComponentResources.They're the only resources that + /// serve as an aggregation of other primitive(i.e.custom) resources.While a custom resource + /// can be a parent of other resources, we don't want to ever depend on those child + /// resource. If we do, it's simple to end up in a situation where we end up depending on a + /// child resource that has a data cycle dependency due to the data passed into it. An + /// example of how this would be bad is: + /// + /// + /// var c1 = new CustomResource("c1"); + /// var c2 = new CustomResource("c2", { parentId = c1.id }, { parent = c1 }); + /// var c3 = new CustomResource("c3", { parentId = c1.id }, { parent = c1 }); + /// + /// + /// The problem here is that 'c2' has a data dependency on 'c1'. If it tries to wait on + /// 'c1' it will walk to the children and wait on them.This will mean it will wait on 'c3'. + /// But 'c3' will be waiting in the same manner on 'c2', and a cycle forms. This normally + /// does not happen with ComponentResources as they do not have any data flowing into + /// them.The only way you would be able to have a problem is if you had this sort of coding + /// pattern: + /// + /// + /// var c1 = new ComponentResource("c1"); + /// var c2 = new CustomResource("c2", { parentId = c1.urn }, { parent: c1 }); + /// var c3 = new CustomResource("c3", { parentId = c1.urn }, { parent: c1 }); + /// + /// + /// However, this would be pretty nonsensical as there is zero need for a custom resource to + /// ever need to reference the urn of a component resource. So it's acceptable if that sort + /// of pattern failed in practice. + /// + internal readonly HashSet ChildResources = new HashSet(); + + /// + /// Urn is the stable logical URN used to distinctly address a resource, both before and + /// after deployments. + /// + // Set using reflection, so we silence the NRT warnings with `null!`. + [Output(Constants.UrnPropertyName)] + public Output Urn { get; private set; } = null!; + + /// + /// When set to true, protect ensures this resource cannot be deleted. + /// + private readonly bool _protect; + + /// + /// A collection of transformations to apply as part of resource registration. + /// + private readonly ImmutableArray _transformations; + + /// + /// A list of aliases applied to this resource. + /// + internal readonly ImmutableArray> _aliases; + + /// + /// The type assigned to the resource at construction. + /// + // This is a method and not a property to not collide with potential subclass property names. + public string GetResourceType() => _type; + + /// + /// The name assigned to the resource at construction. + /// + // This is a method and not a property to not collide with potential subclass property names. + public string GetResourceName() => _name; + + /// + /// The set of providers to use for child resources. Keyed by package name (e.g. "aws"). + /// + private readonly ImmutableDictionary _providers; + + /// + /// Creates and registers a new resource object. is the fully + /// qualified type token and is the "name" part to use in creating a + /// stable and globally unique URN for the object. dependsOn is an optional list of other + /// resources that this resource depends on, controlling the order in which we perform + /// resource operations. + /// + /// The type of the resource. + /// The unique name of the resource. + /// True to indicate that this is a custom resource, managed by a plugin. + /// The arguments to use to populate the new resource. + /// A bag of options that control this resource's behavior. + private protected Resource( + string type, string name, bool custom, + ResourceArgs args, ResourceOptions options) + { + if (string.IsNullOrEmpty(type)) + throw new ArgumentException("'type' cannot be null or empty.", nameof(type)); + + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("'name' cannot be null or empty.", nameof(name)); + + // Before anything else - if there are transformations registered, invoke them in order + // to transform the properties and options assigned to this resource. + var parent = type == Stack._rootPulumiStackTypeName + ? null + : (options.Parent ?? Deployment.InternalInstance.Stack); + + _type = type; + _name = name; + + var transformations = ImmutableArray.CreateBuilder(); + transformations.AddRange(options.ResourceTransformations); + if (parent != null) + { + transformations.AddRange(parent._transformations); + } + this._transformations = transformations.ToImmutable(); + + foreach (var transformation in this._transformations) + { + var tres = transformation(new ResourceTransformationArgs(this, args, options)); + if (tres != null) + { + if (tres.Value.Options.Parent != options.Parent) + { + // This is currently not allowed because the parent tree is needed to + // establish what transformation to apply in the first place, and to compute + // inheritance of other resource options in the Resource constructor before + // transformations are run (so modifying it here would only even partially + // take affect). It's theoretically possible this restriction could be + // lifted in the future, but for now just disallow re-parenting resources in + // transformations to be safe. + throw new ArgumentException("Transformations cannot currently be used to change the 'parent' of a resource."); + } + + args = tres.Value.Args; + options = tres.Value.Options; + } + } + + // Make a shallow clone of options to ensure we don't modify the value passed in. + options = options.Clone(); + var componentOpts = options as ComponentResourceOptions; + var customOpts = options as CustomResourceOptions; + + if (options.Provider != null && + componentOpts?.Providers.Count > 0) + { + throw new ResourceException("Do not supply both 'provider' and 'providers' options to a ComponentResource.", options.Parent); + } + + // Check the parent type if one exists and fill in any default options. + this._providers = ImmutableDictionary.Empty; + + if (options.Parent != null) + { + this._parentResource = options.Parent; + this._parentResource.ChildResources.Add(this); + + if (options.Protect == null) + options.Protect = options.Parent._protect; + + // Make a copy of the aliases array, and add to it any implicit aliases inherited from its parent + options.Aliases = options.Aliases.ToList(); + foreach (var parentAlias in options.Parent._aliases) + { + options.Aliases.Add(Pulumi.Urn.InheritedChildAlias(name, options.Parent.GetResourceName(), parentAlias, type)); + } + + this._providers = options.Parent._providers; + } + + if (custom) + { + var provider = customOpts?.Provider; + if (provider == null) + { + if (options.Parent != null) + { + // If no provider was given, but we have a parent, then inherit the + // provider from our parent. + + options.Provider = options.Parent.GetProvider(type); + } + } + else + { + // If a provider was specified, add it to the providers map under this type's package so that + // any children of this resource inherit its provider. + var typeComponents = type.Split(":"); + if (typeComponents.Length == 3) + { + var pkg = typeComponents[0]; + this._providers = this._providers.SetItem(pkg, provider); + } + } + } + else + { + // Note: we checked above that at most one of options.provider or options.providers + // is set. + + // If options.provider is set, treat that as if we were given a array of provider + // with that single value in it. Otherwise, take the array of providers, convert it + // to a map and combine with any providers we've already set from our parent. + var providerList = options.Provider != null + ? new List { options.Provider } + : componentOpts?.Providers; + + this._providers = this._providers.AddRange(ConvertToProvidersMap(providerList)); + } + + this._protect = options.Protect == true; + + // Collapse any 'Alias'es down to URNs. We have to wait until this point to do so + // because we do not know the default 'name' and 'type' to apply until we are inside the + // resource constructor. + var aliases = ImmutableArray.CreateBuilder>(); + foreach (var alias in options.Aliases) + { + aliases.Add(CollapseAliasToUrn(alias, name, type, options.Parent)); + } + this._aliases = aliases.ToImmutable(); + + if (options.Id != null) + { + // If this resource already exists, read its state rather than registering it anew. + if (!custom) + { + throw new ResourceException( + "Cannot read an existing resource unless it has a custom provider", options.Parent); + } + + Deployment.InternalInstance.ReadResource(this, args, options); + } + else + { + // Kick off the resource registration. If we are actually performing a deployment, + // this resource's properties will be resolved asynchronously after the operation + // completes, so that dependent computations resolve normally. If we are just + // planning, on the other hand, values will never resolve. + Deployment.InternalInstance.RegisterResource(this, custom, args, options); + } + } + + /// + /// Fetches the provider for the given module member, if any. + /// + internal ProviderResource? GetProvider(string moduleMember) + { + var memComponents = moduleMember.Split(":"); + if (memComponents.Length != 3) + { + return null; + } + + this._providers.TryGetValue(memComponents[0], out var result); + return result; + } + + private static Output CollapseAliasToUrn( + Input alias, + string defaultName, + string defaultType, + Resource? defaultParent) + { + return alias.ToOutput().Apply(a => + { + if (a.Urn != null) + { + CheckNull(a.Name, nameof(a.Name)); + CheckNull(a.Type, nameof(a.Type)); + CheckNull(a.Project, nameof(a.Project)); + CheckNull(a.Stack, nameof(a.Stack)); + CheckNull(a.Parent, nameof(a.Parent)); + CheckNull(a.ParentUrn, nameof(a.ParentUrn)); + if (a.NoParent) + ThrowAliasPropertyConflict(nameof(a.NoParent)); + + return Output.Create(a.Urn); + } + + var name = a.Name ?? defaultName; + var type = a.Type ?? defaultType; + var project = a.Project ?? Deployment.Instance.ProjectName; + var stack = a.Stack ?? Deployment.Instance.StackName; + + var parentCount = + (a.Parent != null ? 1 : 0) + + (a.ParentUrn != null ? 1 : 0) + + (a.NoParent ? 1 : 0); + + if (parentCount >= 2) + { + throw new ArgumentException( +$"Only specify one of '{nameof(Alias.Parent)}', '{nameof(Alias.ParentUrn)}' or '{nameof(Alias.NoParent)}' in an {nameof(Alias)}"); + } + + var (parent, parentUrn) = GetParentInfo(defaultParent, a); + + if (name == null) + throw new Exception("No valid 'Name' passed in for alias."); + + if (type == null) + throw new Exception("No valid 'type' passed in for alias."); + + return Pulumi.Urn.Create(name, type, parent, parentUrn, project, stack); + }); + } + + private static void CheckNull(T? value, string name) where T : class + { + if (value != null) + { + ThrowAliasPropertyConflict(name); + return; + } + } + + private static void ThrowAliasPropertyConflict(string name) + => throw new ArgumentException($"{nameof(Alias)} should not specify both {nameof(Alias.Urn)} and {name}"); + + private static (Resource? parent, Input? urn) GetParentInfo(Resource? defaultParent, Alias alias) + { + if (alias.Parent != null) + return (alias.Parent, null); + + if (alias.ParentUrn != null) + return (null, alias.ParentUrn); + + if (alias.NoParent) + return (null, null); + + return (defaultParent, null); + } + + private static ImmutableDictionary ConvertToProvidersMap(List? providers) + { + var result = ImmutableDictionary.CreateBuilder(); + if (providers != null) + { + foreach (var provider in providers) + { + result[provider.Package] = provider; + } + } + + return result.ToImmutable(); + } + } +} diff --git a/sdk/dotnet/Pulumi/Resources/ResourceArgs.cs b/sdk/dotnet/Pulumi/Resources/ResourceArgs.cs new file mode 100644 index 000000000..32cee0b97 --- /dev/null +++ b/sdk/dotnet/Pulumi/Resources/ResourceArgs.cs @@ -0,0 +1,108 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Google.Protobuf; +using Pulumi.Serialization; + +namespace Pulumi +{ + /// + /// Base type for all resource argument classes. + /// + public abstract class ResourceArgs + { + public static readonly ResourceArgs Empty = new EmptyResourceArgs(); + + private readonly ImmutableArray _inputInfos; + + protected ResourceArgs() + { + var fieldQuery = + from field in this.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) + let attr = field.GetCustomAttribute() + where attr != null + select (attr, memberName: field.Name, memberType: field.FieldType, getValue: (Func)field.GetValue); + + var propQuery = + from prop in this.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) + let attr = prop.GetCustomAttribute() + where attr != null + select (attr, memberName: prop.Name, memberType: prop.PropertyType, getValue: (Func)prop.GetValue); + + var all = fieldQuery.Concat(propQuery).ToList(); + + foreach (var (attr, memberName, memberType, getValue) in all) + { + var fullName = $"[Input] {this.GetType().FullName}.{memberName}"; + + if (!typeof(IInput).IsAssignableFrom(memberType)) + { + throw new InvalidOperationException($"{fullName} was not an Input"); + } + } + + _inputInfos = all.Select(t => + new InputInfo(t.attr, t.memberName, t.memberType, t.getValue)).ToImmutableArray(); + } + + internal async Task> ToDictionaryAsync() + { + var builder = ImmutableDictionary.CreateBuilder(); + foreach (var info in _inputInfos) + { + var fullName = $"[Input] {this.GetType().FullName}.{info.MemberName}"; + + var value = (IInput?)info.GetValue(this); + if (info.Attribute.IsRequired && value == null) + { + throw new ArgumentNullException(info.MemberName, $"{fullName} is required but was not given a value"); + } + + if (info.Attribute.Json) + { + value = await ConvertToJsonAsync(fullName, value).ConfigureAwait(false); + } + + builder.Add(info.Attribute.Name, value); + } + + return builder.ToImmutable(); + } + + private async Task ConvertToJsonAsync(string context, IInput? input) + { + if (input == null) + return null; + + var serializer = new Serializer(excessiveDebugOutput: false); + var obj = await serializer.SerializeAsync(context, input).ConfigureAwait(false); + var value = Serializer.CreateValue(obj); + var valueString = JsonFormatter.Default.Format(value); + return (Input)valueString; + } + + private class EmptyResourceArgs : ResourceArgs + { + } + + private struct InputInfo + { + public readonly InputAttribute Attribute; + public readonly Type MemberType; + public readonly string MemberName; + public Func GetValue; + + public InputInfo(InputAttribute attribute, string memberName, Type memberType, Func getValue) : this() + { + Attribute = attribute; + MemberName = memberName; + MemberType = memberType; + GetValue = getValue; + } + } + } +} diff --git a/sdk/dotnet/Pulumi/Resources/ResourceOptions.cs b/sdk/dotnet/Pulumi/Resources/ResourceOptions.cs new file mode 100644 index 000000000..e26091f6c --- /dev/null +++ b/sdk/dotnet/Pulumi/Resources/ResourceOptions.cs @@ -0,0 +1,93 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System.Collections.Generic; + +namespace Pulumi +{ + /// + /// ResourceOptions is a bag of optional settings that control a resource's behavior. + /// + public partial class ResourceOptions + { + /// + /// An optional existing ID to load, rather than create. + /// + public Input? Id { get; set; } + + /// + /// An optional parent resource to which this resource belongs. + /// + public Resource? Parent { get; set; } + + private InputList? _dependsOn; + + /// + /// Optional additional explicit dependencies on other resources. + /// + public InputList DependsOn + { + get => _dependsOn ?? (_dependsOn = new InputList()); + set => _dependsOn = value; + } + + /// + /// When set to true, protect ensures this resource cannot be deleted. + /// + public bool? Protect { get; set; } + + private List? _ignoreChanges; + + /// + /// Ignore changes to any of the specified properties. + /// + public List IgnoreChanges + { + get => _ignoreChanges ?? (_ignoreChanges = new List()); + set => _ignoreChanges = value; + } + + /// + /// An optional version, corresponding to the version of the provider plugin that should be + /// used when operating on this resource. This version overrides the version information + /// inferred from the current package and should rarely be used. + /// + public string? Version { get; set; } + + /// + /// An optional provider to use for this resource's CRUD operations. If no provider is + /// supplied, the default provider for the resource's package will be used. The default + /// provider is pulled from the parent's provider bag (see also + /// ComponentResourceOptions.providers). + /// + /// If this is a do not provide both and . + /// + public ProviderResource? Provider { get; set; } + + /// + /// An optional CustomTimeouts configuration block. + /// + public CustomTimeouts? CustomTimeouts { get; set; } + + private List? _resourceTransformations; + + /// + /// Optional list of transformations to apply to this resource during construction.The + /// transformations are applied in order, and are applied prior to transformation applied to + /// parents walking from the resource up to the stack. + /// + public List ResourceTransformations + { + get => _resourceTransformations ?? (_resourceTransformations = new List()); + set => _resourceTransformations = value; + } + + /// + /// An optional list of aliases to treat this resource as matching. + /// + public List> Aliases { get; set; } = new List>(); + + internal virtual ResourceOptions Clone() + => CreateResourceOptionsCopy(this); + } +} diff --git a/sdk/dotnet/Pulumi/Resources/ResourceOptions_Copy.cs b/sdk/dotnet/Pulumi/Resources/ResourceOptions_Copy.cs new file mode 100644 index 000000000..67f4c4a2d --- /dev/null +++ b/sdk/dotnet/Pulumi/Resources/ResourceOptions_Copy.cs @@ -0,0 +1,75 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System.Linq; +using System.Collections.Generic; + +namespace Pulumi +{ + public partial class ResourceOptions + { + internal static ResourceOptions CreateResourceOptionsCopy(ResourceOptions options) + => new ResourceOptions + { + Aliases = options.Aliases.ToList(), + CustomTimeouts = CustomTimeouts.Clone(options.CustomTimeouts), + DependsOn = options.DependsOn.Clone(), + Id = options.Id, + Parent = options.Parent, + IgnoreChanges = options.IgnoreChanges.ToList(), + Protect = options.Protect, + Provider = options.Provider, + ResourceTransformations = options.ResourceTransformations.ToList(), + Version = options.Version, + }; + + internal static CustomResourceOptions CreateCustomResourceOptionsCopy(ResourceOptions options) + { + var customOptions = options as CustomResourceOptions; + var copied = CreateResourceOptionsCopy(options); + + return new CustomResourceOptions + { + // Base properties + Aliases = copied.Aliases, + CustomTimeouts = copied.CustomTimeouts, + DependsOn = copied.DependsOn, + Id = copied.Id, + Parent = copied.Parent, + IgnoreChanges = copied.IgnoreChanges, + Protect = copied.Protect, + Provider = copied.Provider, + ResourceTransformations = copied.ResourceTransformations, + Version = copied.Version, + + // Our properties + AdditionalSecretOutputs = customOptions?.AdditionalSecretOutputs.ToList() ?? new List(), + DeleteBeforeReplace = customOptions?.DeleteBeforeReplace, + ImportId = customOptions?.ImportId, + }; + } + + internal static ComponentResourceOptions CreateComponentResourceOptionsCopy(ResourceOptions options) + { + var componentOptions = options as ComponentResourceOptions; + var cloned = CreateResourceOptionsCopy(options); + + return new ComponentResourceOptions + { + // Base properties + Aliases = cloned.Aliases, + CustomTimeouts = cloned.CustomTimeouts, + DependsOn = cloned.DependsOn, + Id = cloned.Id, + Parent = cloned.Parent, + IgnoreChanges = cloned.IgnoreChanges, + Protect = cloned.Protect, + Provider = cloned.Provider, + ResourceTransformations = cloned.ResourceTransformations, + Version = cloned.Version, + + // Our properties + Providers = componentOptions?.Providers.ToList() ?? new List(), + }; + } + } +} diff --git a/sdk/dotnet/Pulumi/Resources/ResourceOptions_Merge.cs b/sdk/dotnet/Pulumi/Resources/ResourceOptions_Merge.cs new file mode 100644 index 000000000..8d778c3f3 --- /dev/null +++ b/sdk/dotnet/Pulumi/Resources/ResourceOptions_Merge.cs @@ -0,0 +1,125 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections.Generic; + +namespace Pulumi +{ + public partial class ResourceOptions + { + /// + /// Takes two ResourceOptions values and produces a new ResourceOptions with the respective + /// properties of merged over the same properties in . The original options objects will be unchanged. + /// + /// A new instance will always be returned. + /// + /// Conceptually property merging follows these basic rules: + /// + /// + /// If the property is a collection, the final value will be a collection containing the + /// values from each options object. + /// + /// + /// Simple scaler values from (i.e. s, + /// s, s) will replace the values of . + /// + /// + /// values in will be ignored. + /// + /// + /// + public static ResourceOptions Merge(ResourceOptions? options1, ResourceOptions? options2) + { + options1 ??= new ResourceOptions(); + options2 ??= new ResourceOptions(); + + if ((options1 is CustomResourceOptions && options2 is ComponentResourceOptions) || + (options1 is ComponentResourceOptions && options2 is CustomResourceOptions)) + { + throw new ArgumentException( + $"Cannot merge a {nameof(CustomResourceOptions)} and {nameof(ComponentResourceOptions)} together."); + } + + // make an appropriate copy of both options bag, then the copy of options2 into the copy + // of options1 and return the copy of options1. + if (options1 is CustomResourceOptions || options2 is CustomResourceOptions) + { + return MergeCustomOptions( + CreateCustomResourceOptionsCopy(options1), + CreateCustomResourceOptionsCopy(options2)); + } + else if (options1 is ComponentResourceOptions || options2 is ComponentResourceOptions) + { + return MergeComponentOptions( + CreateComponentResourceOptionsCopy(options1), + CreateComponentResourceOptionsCopy(options2)); + } + else + { + return MergeNormalOptions( + CreateResourceOptionsCopy(options1), + CreateResourceOptionsCopy(options2)); + } + + static ResourceOptions MergeNormalOptions(ResourceOptions options1, ResourceOptions options2) + { + options1.Id = options2.Id ?? options1.Id; + options1.Parent = options2.Parent ?? options1.Parent; + options1.Protect = options2.Protect ?? options1.Protect; + options1.Version = options2.Version ?? options1.Version; + options1.Provider = options2.Provider ?? options1.Provider; + options1.CustomTimeouts = options2.CustomTimeouts ?? options1.CustomTimeouts; + + options1.IgnoreChanges.AddRange(options2.IgnoreChanges); + options1.ResourceTransformations.AddRange(options2.ResourceTransformations); + options1.Aliases.AddRange(options2.Aliases); + + options1.DependsOn = options1.DependsOn.Concat(options2.DependsOn); + return options1; + } + + static CustomResourceOptions MergeCustomOptions(CustomResourceOptions options1, CustomResourceOptions options2) + { + // first, merge all the normal option values over + MergeNormalOptions(options1, options2); + + options1.DeleteBeforeReplace = options2.DeleteBeforeReplace ?? options1.DeleteBeforeReplace; + options1.ImportId = options2.ImportId ?? options1.ImportId; + + options1.AdditionalSecretOutputs.AddRange(options2.AdditionalSecretOutputs); + + return options1; + } + + static ComponentResourceOptions MergeComponentOptions(ComponentResourceOptions options1, ComponentResourceOptions options2) + { + ExpandProviders(options1); + ExpandProviders(options2); + + // first, merge all the normal option values over + MergeNormalOptions(options1, options2); + + options1.Providers.AddRange(options2.Providers); + + if (options1.Providers.Count == 1) + { + options1.Provider = options1.Providers[0]; + options1.Providers.Clear(); + } + + return options1; + } + + static void ExpandProviders(ComponentResourceOptions options) + { + if (options.Provider != null) + { + options.Providers = new List { options.Provider }; + options.Provider = null; + } + } + } + } +} diff --git a/sdk/dotnet/Pulumi/Resources/ResourceTransformation.cs b/sdk/dotnet/Pulumi/Resources/ResourceTransformation.cs new file mode 100644 index 000000000..c13a8aace --- /dev/null +++ b/sdk/dotnet/Pulumi/Resources/ResourceTransformation.cs @@ -0,0 +1,54 @@ +// Copyright 2016-2019, Pulumi Corporation + +namespace Pulumi +{ + /// + /// ResourceTransformation is the callback signature for . A transformation is passed the same set of + /// inputs provided to the constructor, and can optionally return back + /// alternate values for the properties and/or options prior to the resource + /// actually being created. The effect will be as though those properties and/or + /// options were passed in place of the original call to the + /// constructor. If the transformation returns , this indicates that the + /// resource will not be transformed. + /// + /// The new values to use for the args and options of the in place of the originally provided values. + public delegate ResourceTransformationResult? ResourceTransformation(ResourceTransformationArgs args); + + public readonly struct ResourceTransformationArgs + { + /// + /// The Resource instance that is being transformed. + /// + public Resource Resource { get; } + /// + /// The original properties passed to the Resource constructor. + /// + public ResourceArgs Args { get; } + /// + /// The original resource options passed to the Resource constructor. + /// + public ResourceOptions Options { get; } + + public ResourceTransformationArgs( + Resource resource, ResourceArgs args, ResourceOptions options) + { + Resource = resource; + Args = args; + Options = options; + } + } + + public readonly struct ResourceTransformationResult + { + public ResourceArgs Args { get; } + public ResourceOptions Options { get; } + + public ResourceTransformationResult(ResourceArgs args, ResourceOptions options) + { + Args = args; + Options = options; + } + } +} diff --git a/sdk/dotnet/Pulumi/Serialization/Attributes.cs b/sdk/dotnet/Pulumi/Serialization/Attributes.cs new file mode 100644 index 000000000..790f6cb71 --- /dev/null +++ b/sdk/dotnet/Pulumi/Serialization/Attributes.cs @@ -0,0 +1,77 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using Google.Protobuf.WellKnownTypes; + +namespace Pulumi.Serialization +{ + /// + /// Attribute used by a Pulumi Cloud Provider Package to mark Resource output properties. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class OutputAttribute : Attribute + { + public string Name { get; } + + public OutputAttribute(string name) + { + Name = name; + } + } + + /// + /// Attribute used by a Pulumi Cloud Provider Package to mark Resource input fields and + /// properties. + /// + /// Note: for simple inputs (i.e. this should just be placed on the + /// property itself. i.e. [Input] Input<string> Acl. + /// + /// For collection inputs (i.e. this shuld be placed on the + /// backing field for the property. i.e. + /// + /// + /// [Input] private InputList<string> _acls; + /// public InputList<string> Acls + /// { + /// get => _acls ?? (_acls = new InputList<string>()); + /// set => _acls = value; + /// } + /// + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class InputAttribute : Attribute + { + internal string Name { get; } + internal bool IsRequired { get; } + internal bool Json { get; } + + public InputAttribute(string name, bool required = false, bool json = false) + { + Name = name; + IsRequired = required; + Json = json; + } + } + + /// + /// Attribute used by a Pulumi Cloud Provider Package to mark complex types used for a Resource + /// output property. A complex type must have a single constructor in it marked with the + /// attribute. + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class OutputTypeAttribute : Attribute + { + } + + /// + /// Attribute used by a Pulumi Cloud Provider Package to marks the constructor for a complex + /// property type so that it can be instantiated by the Pulumi runtime. + /// + /// The constructor should contain parameters that map to the resultant returned by the engine. + /// + [AttributeUsage(AttributeTargets.Constructor)] + public sealed class OutputConstructorAttribute : Attribute + { + } +} diff --git a/sdk/dotnet/Pulumi/Serialization/Constants.cs b/sdk/dotnet/Pulumi/Serialization/Constants.cs new file mode 100644 index 000000000..a469ef45e --- /dev/null +++ b/sdk/dotnet/Pulumi/Serialization/Constants.cs @@ -0,0 +1,43 @@ +// Copyright 2016-2019, Pulumi Corporation + +namespace Pulumi.Serialization +{ + internal static class Constants + { + /// + /// Unknown values are encoded as a distinguished string value. + /// + public const string UnknownValue = "04da6b54-80e4-46f7-96ec-b56ff0331ba9"; + + /// + /// SpecialSigKey is sometimes used to encode type identity inside of a map. See pkg/resource/properties.go. + /// + public const string SpecialSigKey = "4dabf18193072939515e22adb298388d"; + + /// + /// SpecialAssetSig is a randomly assigned hash used to identify assets in maps. See pkg/resource/asset.go. + /// + public const string SpecialAssetSig = "c44067f5952c0a294b673a41bacd8c17"; + + /// + /// SpecialArchiveSig is a randomly assigned hash used to identify archives in maps. See pkg/resource/asset.go. + /// + public const string SpecialArchiveSig = "0def7320c3a5731c473e5ecbe6d01bc7"; + + /// + /// SpecialSecretSig is a randomly assigned hash used to identify secrets in maps. See pkg/resource/properties.go. + /// + public const string SpecialSecretSig = "1b47061264138c4ac30d75fd1eb44270"; + + public const string SecretValueName = "value"; + + public const string AssetTextName = "text"; + public const string ArchiveAssetsName = "assets"; + + public const string AssetOrArchivePathName = "path"; + public const string AssetOrArchiveUriName = "uri"; + + public const string IdPropertyName = "id"; + public const string UrnPropertyName = "urn"; + } +} diff --git a/sdk/dotnet/Pulumi/Serialization/Converter.cs b/sdk/dotnet/Pulumi/Serialization/Converter.cs new file mode 100644 index 000000000..c5cdfa401 --- /dev/null +++ b/sdk/dotnet/Pulumi/Serialization/Converter.cs @@ -0,0 +1,264 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using Google.Protobuf.WellKnownTypes; + +namespace Pulumi.Serialization +{ + internal static class Converter + { + public static OutputData ConvertValue(string context, Value value) + { + var (data, isKnown, isSecret) = ConvertValue(context, value, typeof(T)); + return new OutputData((T)data!, isKnown, isSecret); + } + + public static OutputData ConvertValue(string context, Value value, System.Type targetType) + { + CheckTargetType(context, targetType); + + var (deserialized, isKnown, isSecret) = Deserializer.Deserialize(value); + var converted = ConvertObject(context, deserialized, targetType); + + return new OutputData(converted, isKnown, isSecret); + } + + private static object? ConvertObject(string context, object? val, System.Type targetType) + { + var targetIsNullable = targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>); + + // Note: 'null's can enter the system as the representation of an 'unknown' value. + // Before calling 'Convert' we will have already lifted the 'IsKnown' bit out, but we + // will be passing null around as a value. + if (val == null) + { + if (targetIsNullable) + // A 'null' value coerces to a nullable null. + return null; + + if (targetType.IsValueType) + return Activator.CreateInstance(targetType); + + // for all other types, can just return the null value right back out as a legal + // reference type value. + return null; + } + + // We're not null and we're converting to Nullable, just convert our value to be a T. + if (targetIsNullable) + return ConvertObject(context, val, targetType.GenericTypeArguments.Single()); + + if (targetType == typeof(string)) + return EnsureType(context, val); + + if (targetType == typeof(bool)) + return EnsureType(context, val); + + if (targetType == typeof(double)) + return EnsureType(context, val); + + if (targetType == typeof(int)) + return (int)EnsureType(context, val); + + if (targetType == typeof(Asset)) + return EnsureType(context, val); + + if (targetType == typeof(Archive)) + return EnsureType(context, val); + + if (targetType == typeof(AssetOrArchive)) + return EnsureType(context, val); + + if (targetType.IsConstructedGenericType) + { + if (targetType.GetGenericTypeDefinition() == typeof(ImmutableArray<>)) + return ConvertArray(context, val, targetType); + + if (targetType.GetGenericTypeDefinition() == typeof(ImmutableDictionary<,>)) + return ConvertDictionary(context, val, targetType); + + throw new InvalidOperationException( + $"Unexpected generic target type {targetType.FullName} when deserializing {context}"); + } + + if (targetType.GetCustomAttribute() == null) + throw new InvalidOperationException( + $"Unexpected target type {targetType.FullName} when deserializing {context}"); + + var constructor = GetPropertyConstructor(targetType); + if (constructor == null) + throw new InvalidOperationException( + $"Expected target type {targetType.FullName} to have [{nameof(OutputConstructorAttribute)}] constructor when deserializing {context}"); + + var dictionary = EnsureType>(context, val); + + var constructorParameters = constructor.GetParameters(); + var arguments = new object?[constructorParameters.Length]; + + for (int i = 0, n = constructorParameters.Length; i < n; i++) + { + var parameter = constructorParameters[i]; + + // Note: TryGetValue may not find a value here. That can happen for things like + // unknown vals. That's ok. We'll pass that through to 'Convert' and will get the + // default value needed for the parameter type. + dictionary.TryGetValue(parameter.Name!, out var argValue); + arguments[i] = ConvertObject($"{targetType.FullName}({parameter.Name})", argValue, parameter.ParameterType); + } + + return constructor.Invoke(arguments); + } + + private static T EnsureType(string context, object val) + => val is T t ? t : throw new InvalidOperationException($"Expected {typeof(T).FullName} but got {val.GetType().FullName} deserializing {context}"); + + private static object ConvertArray(string fieldName, object val, System.Type targetType) + { + if (!(val is ImmutableArray array)) + { + throw new InvalidOperationException( + $"Expected {typeof(ImmutableArray).FullName} but got {val.GetType().FullName} deserializing {fieldName}"); + } + + var builder = + typeof(ImmutableArray).GetMethod(nameof(ImmutableArray.CreateBuilder), Array.Empty())! + .MakeGenericMethod(targetType.GenericTypeArguments) + .Invoke(obj: null, parameters: null)!; + + var builderAdd = builder.GetType().GetMethod(nameof(ImmutableArray.Builder.Add))!; + var builderToImmutable = builder.GetType().GetMethod(nameof(ImmutableArray.Builder.ToImmutable))!; + + var elementType = targetType.GenericTypeArguments.Single(); + foreach (var element in array) + { + builderAdd.Invoke(builder, new[] { ConvertObject(fieldName, element, elementType) }); + } + + return builderToImmutable.Invoke(builder, null)!; + } + + private static object ConvertDictionary(string fieldName, object val, System.Type targetType) + { + if (!(val is ImmutableDictionary dictionary)) + { + throw new InvalidOperationException( + $"Expected {typeof(ImmutableDictionary).FullName} but got {val.GetType().FullName} deserializing {fieldName}"); + } + + if (targetType == typeof(ImmutableDictionary)) + { + // already in the form we need. no need to convert anything. + return val; + } + + var keyType = targetType.GenericTypeArguments[0]; + if (keyType != typeof(string)) + { + throw new InvalidOperationException( + $"Unexpected type {targetType.FullName} when deserializing {fieldName}. ImmutableDictionary's TKey type was not {typeof(string).FullName}"); + } + + var builder = + typeof(ImmutableDictionary).GetMethod(nameof(ImmutableDictionary.CreateBuilder), Array.Empty())! + .MakeGenericMethod(targetType.GenericTypeArguments) + .Invoke(obj: null, parameters: null)!; + + // var b = ImmutableDictionary.CreateBuilder().Add() + + var builderAdd = builder.GetType().GetMethod(nameof(ImmutableDictionary.Builder.Add), targetType.GenericTypeArguments)!; + var builderToImmutable = builder.GetType().GetMethod(nameof(ImmutableDictionary.Builder.ToImmutable))!; + + var elementType = targetType.GenericTypeArguments[1]; + foreach (var (key, element) in dictionary) + { + builderAdd.Invoke(builder, new[] { key, ConvertObject(fieldName, element, elementType) }); + } + + return builderToImmutable.Invoke(builder, null)!; + } + + public static void CheckTargetType(string context, System.Type targetType) + { + if (targetType == typeof(bool) || + targetType == typeof(int) || + targetType == typeof(double) || + targetType == typeof(string) || + targetType == typeof(Asset) || + targetType == typeof(Archive) || + targetType == typeof(AssetOrArchive)) + { + return; + } + + if (targetType == typeof(ImmutableDictionary)) + { + // This type is what is generated for things like azure/aws tags. It's an untyped + // map in our original schema. This is the only place that `object` should appear + // as a legal value. + return; + } + + if (targetType.IsConstructedGenericType) + { + if (targetType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + CheckTargetType(context, targetType.GenericTypeArguments.Single()); + return; + } + else if (targetType.GetGenericTypeDefinition() == typeof(ImmutableArray<>)) + { + CheckTargetType(context, targetType.GenericTypeArguments.Single()); + return; + } + else if (targetType.GetGenericTypeDefinition() == typeof(ImmutableDictionary<,>)) + { + var dictTypeArgs = targetType.GenericTypeArguments; + if (dictTypeArgs[0] != typeof(string)) + { + throw new InvalidOperationException( +$@"{context} contains invalid type {targetType.FullName}: + The only allowed ImmutableDictionary 'TKey' type is 'String'."); + } + + CheckTargetType(context, dictTypeArgs[1]); + return; + } + else + { + throw new InvalidOperationException( +$@"{context} contains invalid type {targetType.FullName}: + The only generic types allowed are ImmutableArray<...> and ImmutableDictionary"); + } + } + + var propertyTypeAttribute = targetType.GetCustomAttribute(); + if (propertyTypeAttribute == null) + { + throw new InvalidOperationException( +$@"{context} contains invalid type {targetType.FullName}. Allowed types are: + String, Boolean, Int32, Double, + Nullable<...>, ImmutableArray<...> and ImmutableDictionary or + a class explicitly marked with the [{nameof(OutputTypeAttribute)}]."); + } + + var constructor = GetPropertyConstructor(targetType); + if (constructor == null) + { + throw new InvalidOperationException( +$@"{targetType.FullName} had [{nameof(OutputTypeAttribute)}], but did not contain constructor marked with [{nameof(OutputConstructorAttribute)}]."); + } + + foreach (var param in constructor.GetParameters()) + { + CheckTargetType($@"{targetType.FullName}({param.Name})", param.ParameterType); + } + } + + private static ConstructorInfo GetPropertyConstructor(System.Type outputTypeArg) + => outputTypeArg.GetConstructors(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).FirstOrDefault( + c => c.GetCustomAttributes() != null); + } +} diff --git a/sdk/dotnet/Pulumi/Serialization/Deserializer.cs b/sdk/dotnet/Pulumi/Serialization/Deserializer.cs new file mode 100644 index 000000000..551c8d0ea --- /dev/null +++ b/sdk/dotnet/Pulumi/Serialization/Deserializer.cs @@ -0,0 +1,209 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Google.Protobuf.Collections; +using Google.Protobuf.WellKnownTypes; + +namespace Pulumi.Serialization +{ + internal static class Deserializer + { + private static OutputData DeserializeCore(Value value, Func> func) + { + var (innerVal, isSecret) = UnwrapSecret(value); + value = innerVal; + + if (value.KindCase == Value.KindOneofCase.StringValue && + value.StringValue == Constants.UnknownValue) + { + // always deserialize unknown as the null value. + return new OutputData(default!, isKnown: false, isSecret); + } + + if (TryDeserializeAssetOrArchive(value, out var assetOrArchive)) + { + return new OutputData((T)(object)assetOrArchive, isKnown: true, isSecret); + } + + var innerData = func(value); + return OutputData.Create(innerData.Value, innerData.IsKnown, isSecret || innerData.IsSecret); + } + + private static OutputData DeserializeOneOf(Value value, Value.KindOneofCase kind, Func> func) + => DeserializeCore(value, v => + v.KindCase == kind ? func(v) : throw new InvalidOperationException($"Trying to deserialize {v.KindCase} as a {kind}")); + + private static OutputData DeserializePrimitive(Value value, Value.KindOneofCase kind, Func func) + => DeserializeOneOf(value, kind, v => OutputData.Create(func(v), isKnown: true, isSecret: false)); + + private static OutputData DeserializeBoolean(Value value) + => DeserializePrimitive(value, Value.KindOneofCase.BoolValue, v => v.BoolValue); + + private static OutputData DeserializerString(Value value) + => DeserializePrimitive(value, Value.KindOneofCase.StringValue, v => v.StringValue); + + private static OutputData DeserializerDouble(Value value) + => DeserializePrimitive(value, Value.KindOneofCase.NumberValue, v => v.NumberValue); + + private static OutputData> DeserializeList(Value value) + => DeserializeOneOf(value, Value.KindOneofCase.ListValue, + v => + { + var result = ImmutableArray.CreateBuilder(); + var isKnown = true; + var isSecret = false; + + foreach (var element in v.ListValue.Values) + { + var elementData = Deserialize(element); + (isKnown, isSecret) = OutputData.Combine(elementData, isKnown, isSecret); + result.Add(elementData.Value); + } + + return OutputData.Create(result.ToImmutable(), isKnown, isSecret); + }); + + private static OutputData> DeserializeStruct(Value value) + => DeserializeOneOf(value, Value.KindOneofCase.StructValue, + v => + { + var result = ImmutableDictionary.CreateBuilder(); + var isKnown = true; + var isSecret = false; + + foreach (var (key, element) in v.StructValue.Fields) + { + var elementData = Deserialize(element); + (isKnown, isSecret) = OutputData.Combine(elementData, isKnown, isSecret); + result.Add(key, elementData.Value); + } + + return OutputData.Create(result.ToImmutable(), isKnown, isSecret); + }); + + public static OutputData Deserialize(Value value) + => DeserializeCore(value, + v => v.KindCase switch + { + Value.KindOneofCase.NumberValue => DeserializerDouble(v), + Value.KindOneofCase.StringValue => DeserializerString(v), + Value.KindOneofCase.BoolValue => DeserializeBoolean(v), + Value.KindOneofCase.StructValue => DeserializeStruct(v), + Value.KindOneofCase.ListValue => DeserializeList(v), + Value.KindOneofCase.NullValue => new OutputData(null, isKnown: true, isSecret: false), + Value.KindOneofCase.None => throw new InvalidOperationException("Should never get 'None' type when deserializing protobuf"), + _ => throw new InvalidOperationException("Unknown type when deserializing protobuf: " + v.KindCase), + }); + + private static (Value unwrapped, bool isSecret) UnwrapSecret(Value value) + { + var isSecret = false; + + while (IsSpecialStruct(value, out var sig) && + sig == Constants.SpecialSecretSig) + { + if (!value.StructValue.Fields.TryGetValue(Constants.SecretValueName, out var secretValue)) + throw new InvalidOperationException("Secrets must have a field called 'value'"); + + isSecret = true; + value = secretValue; + } + + return (value, isSecret); + } + + private static bool IsSpecialStruct( + Value value, [NotNullWhen(true)] out string? sig) + { + if (value.KindCase == Value.KindOneofCase.StructValue && + value.StructValue.Fields.TryGetValue(Constants.SpecialSigKey, out var sigVal) && + sigVal.KindCase == Value.KindOneofCase.StringValue) + { + sig = sigVal.StringValue; + return true; + } + + sig = null; + return false; + } + + private static bool TryDeserializeAssetOrArchive( + Value value, [NotNullWhen(true)] out AssetOrArchive? assetOrArchive) + { + if (IsSpecialStruct(value, out var sig)) + { + if (sig == Constants.SpecialAssetSig) + { + assetOrArchive = DeserializeAsset(value); + return true; + } + else if (sig == Constants.SpecialArchiveSig) + { + assetOrArchive = DeserializeArchive(value); + return true; + } + } + + assetOrArchive = null; + return false; + } + + private static Archive DeserializeArchive(Value value) + { + if (TryGetStringValue(value.StructValue.Fields, Constants.AssetOrArchivePathName, out var path)) + return new FileArchive(path); + + if (TryGetStringValue(value.StructValue.Fields, Constants.AssetOrArchiveUriName, out var uri)) + return new RemoteArchive(uri); + + if (value.StructValue.Fields.TryGetValue(Constants.ArchiveAssetsName, out var assetsValue)) + { + if (assetsValue.KindCase == Value.KindOneofCase.StructValue) + { + var assets = ImmutableDictionary.CreateBuilder(); + foreach (var (name, val) in assetsValue.StructValue.Fields) + { + if (!TryDeserializeAssetOrArchive(val, out var innerAssetOrArchive)) + throw new InvalidOperationException("AssetArchive contained an element that wasn't itself an Asset or Archive."); + + assets[name] = innerAssetOrArchive; + } + + return new AssetArchive(assets.ToImmutable()); + } + } + + throw new InvalidOperationException("Value was marked as Archive, but did not conform to required shape."); + } + + private static Asset DeserializeAsset(Value value) + { + if (TryGetStringValue(value.StructValue.Fields, Constants.AssetOrArchivePathName, out var path)) + return new FileAsset(path); + + if (TryGetStringValue(value.StructValue.Fields, Constants.AssetOrArchiveUriName, out var uri)) + return new RemoteAsset(uri); + + if (TryGetStringValue(value.StructValue.Fields, Constants.AssetTextName, out var text)) + return new StringAsset(text); + + throw new InvalidOperationException("Value was marked as Asset, but did not conform to required shape."); + } + + private static bool TryGetStringValue( + MapField fields, string keyName, [NotNullWhen(true)] out string? result) + { + if (fields.TryGetValue(keyName, out var value) && + value.KindCase == Value.KindOneofCase.StringValue) + { + result = value.StringValue; + return true; + } + + result = null; + return false; + } + } +} diff --git a/sdk/dotnet/Pulumi/Serialization/OutputCompletionSource.cs b/sdk/dotnet/Pulumi/Serialization/OutputCompletionSource.cs new file mode 100644 index 000000000..113f5e5ba --- /dev/null +++ b/sdk/dotnet/Pulumi/Serialization/OutputCompletionSource.cs @@ -0,0 +1,98 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Pulumi.Serialization +{ + internal interface IOutputCompletionSource + { + Type TargetType { get; } + IOutput Output { get; } + + void TrySetException(Exception exception); + void TrySetDefaultResult(bool isKnown); + + void SetStringValue(string value, bool isKnown); + void SetValue(OutputData data); + } + + internal class OutputCompletionSource : IOutputCompletionSource + { + private readonly TaskCompletionSource> _taskCompletionSource; + public readonly Output Output; + + public OutputCompletionSource(Resource? resource) + { + _taskCompletionSource = new TaskCompletionSource>(); + Output = new Output( + resource == null ? ImmutableHashSet.Empty : ImmutableHashSet.Create(resource), + _taskCompletionSource.Task); + } + + public System.Type TargetType => typeof(T); + + IOutput IOutputCompletionSource.Output => Output; + + public void SetStringValue(string value, bool isKnown) + => _taskCompletionSource.SetResult(new OutputData((T)(object)value, isKnown, isSecret: false)); + + public void SetValue(OutputData data) + => _taskCompletionSource.SetResult(new OutputData((T)data.Value!, data.IsKnown, data.IsSecret)); + + public void TrySetDefaultResult(bool isKnown) + => _taskCompletionSource.TrySetResult(new OutputData(default!, isKnown, isSecret: false)); + + public void TrySetException(Exception exception) + => _taskCompletionSource.TrySetException(exception); + } + + internal static class OutputCompletionSource + { + public static ImmutableDictionary GetSources(Resource resource) + { + var name = resource.GetResourceName(); + var type = resource.GetResourceType(); + + var query = from property in resource.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) + let attr = property.GetCustomAttribute() + where attr != null + select (property, attr); + + var result = ImmutableDictionary.CreateBuilder(); + foreach (var (prop, attr) in query.ToList()) + { + var propType = prop.PropertyType; + var propFullName = $"[Output] {resource.GetType().FullName}.{prop.Name}"; + if (!propType.IsConstructedGenericType && + propType.GetGenericTypeDefinition() != typeof(Output<>)) + { + throw new InvalidOperationException($"{propFullName} was not an Output"); + } + + var setMethod = prop.DeclaringType!.GetMethod("set_" + prop.Name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); + if (setMethod == null) + { + throw new InvalidOperationException($"{propFullName} did not have a 'set' method"); + } + + var outputTypeArg = propType.GenericTypeArguments.Single(); + Converter.CheckTargetType(propFullName, outputTypeArg); + + var ocsType = typeof(OutputCompletionSource<>).MakeGenericType(outputTypeArg); + var ocsContructor = ocsType.GetConstructors().Single(); + var completionSource = (IOutputCompletionSource)ocsContructor.Invoke(new[] { resource }); + + setMethod.Invoke(resource, new[] { completionSource.Output }); + result.Add(attr.Name, completionSource); + } + + Log.Debug("Fields to assign: " + JsonSerializer.Serialize(result.Keys), resource); + return result.ToImmutable(); + } + } +} diff --git a/sdk/dotnet/Pulumi/Serialization/OutputData.cs b/sdk/dotnet/Pulumi/Serialization/OutputData.cs new file mode 100644 index 000000000..26c59469b --- /dev/null +++ b/sdk/dotnet/Pulumi/Serialization/OutputData.cs @@ -0,0 +1,37 @@ +// Copyright 2016-2019, Pulumi Corporation + +namespace Pulumi.Serialization +{ + internal static class OutputData + { + public static OutputData Create(X value, bool isKnown, bool isSecret) + => new OutputData(value, isKnown, isSecret); + + public static (bool isKnown, bool isSecret) Combine(OutputData data, bool isKnown, bool isSecret) + => (isKnown && data.IsKnown, isSecret || data.IsSecret); + } + + internal struct OutputData + { + public readonly X Value; + public readonly bool IsKnown; + public readonly bool IsSecret; + + public OutputData(X value, bool isKnown, bool isSecret) + { + Value = value; + IsKnown = isKnown; + IsSecret = isSecret; + } + + public static implicit operator OutputData(OutputData data) + => new OutputData(data.Value, data.IsKnown, data.IsSecret); + + public void Deconstruct(out X value, out bool isKnown, out bool isSecret) + { + value = Value; + isKnown = IsKnown; + isSecret = IsSecret; + } + } +} diff --git a/sdk/dotnet/Pulumi/Serialization/Serializer.cs b/sdk/dotnet/Pulumi/Serialization/Serializer.cs new file mode 100644 index 000000000..e1c665b63 --- /dev/null +++ b/sdk/dotnet/Pulumi/Serialization/Serializer.cs @@ -0,0 +1,297 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections; +using System.Collections.Immutable; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Google.Protobuf.WellKnownTypes; + +namespace Pulumi.Serialization +{ + internal struct Serializer + { + public readonly HashSet DependentResources; + + private readonly bool _excessiveDebugOutput; + + public Serializer(bool excessiveDebugOutput) + { + this.DependentResources = new HashSet(); + _excessiveDebugOutput = excessiveDebugOutput; + } + + /// + /// Takes in an arbitrary object and serializes it into a uniform form that can converted + /// trivially to a protobuf to be passed to the Pulumi engine. + /// + /// The allowed 'basis' forms that can be serialized are: + /// + /// s + /// s + /// s + /// s + /// s + /// s + /// s + /// s + /// s + /// + /// Additionally, other more complex objects can be serialized as long as they are built + /// out of serializable objects. These complex objects include: + /// + /// s. As long as they are an Input of a serializable type. + /// s. As long as they are an Output of a serializable type. + /// s. As long as all elements in the list are serializable. + /// . As long as the key of the dictionary are s and as long as the value are all serializable. + /// + /// No other forms are allowed. + /// + /// This function will only return values of a very specific shape. Specifically, the + /// result values returned will *only* be one of: + /// + /// + /// + /// + /// + /// + /// + /// An containing only these result value types. + /// An where the keys are strings and + /// the values are only these result value types. + /// + /// No other result type are allowed to be returned. + /// + public async Task SerializeAsync(string ctx, object? prop) + { + // IMPORTANT: + // IMPORTANT: Keep this in sync with serializesPropertiesSync in invoke.ts + // IMPORTANT: + if (prop == null || + prop is bool || + prop is int || + prop is double || + prop is string) + { + if (_excessiveDebugOutput) + { + Log.Debug($"Serialize property[{ctx}]: primitive={prop}"); + } + + return prop; + } + + if (prop is ResourceArgs args) + return await SerializeResourceArgsAsync(ctx, args).ConfigureAwait(false); + + if (prop is AssetOrArchive assetOrArchive) + return await SerializeAssetOrArchiveAsync(ctx, assetOrArchive).ConfigureAwait(false); + + if (prop is Task) + { + throw new InvalidOperationException( +$"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:\n\t{ctx}"); + } + + if (prop is IInput input) + { + if (_excessiveDebugOutput) + { + Log.Debug($"Serialize property[{ctx}]: Recursing into input"); + } + + return await SerializeAsync(ctx, input.ToOutput()).ConfigureAwait(false); + } + + if (prop is IOutput output) + { + if (_excessiveDebugOutput) + { + Log.Debug($"Serialize property[{ctx}]: Recursing into Output"); + } + + this.DependentResources.AddRange(output.Resources); + var data = await output.GetDataAsync().ConfigureAwait(false); + + // When serializing an Output, we will either serialize it as its resolved value or the "unknown value" + // sentinel. We will do the former for all outputs created directly by user code (such outputs always + // resolve isKnown to true) and for any resource outputs that were resolved with known values. + var isKnown = data.IsKnown; + var isSecret = data.IsSecret; + + if (!isKnown) + return Constants.UnknownValue; + + var value = await SerializeAsync($"{ctx}.id", data.Value).ConfigureAwait(false); + if (isSecret) + { + var builder = ImmutableDictionary.CreateBuilder(); + builder.Add(Constants.SpecialSigKey, Constants.SpecialSecretSig); + builder.Add(Constants.SecretValueName, value); + return builder.ToImmutable(); + } + + return value; + } + + if (prop is CustomResource customResource) + { + // Resources aren't serializable; instead, we serialize them as references to the ID property. + if (_excessiveDebugOutput) + { + Log.Debug($"Serialize property[{ctx}]: Encountered CustomResource"); + } + + this.DependentResources.Add(customResource); + return await SerializeAsync($"{ctx}.id", customResource.Id).ConfigureAwait(false); + } + + if (prop is ComponentResource componentResource) + { + // Component resources often can contain cycles in them. For example, an awsinfra + // SecurityGroupRule can point a the awsinfra SecurityGroup, which in turn can point + // back to its rules through its 'egressRules' and 'ingressRules' properties. If + // serializing out the 'SecurityGroup' resource ends up trying to serialize out + // those properties, a deadlock will happen, due to waiting on the child, which is + // waiting on the parent. + // + // Practically, there is no need to actually serialize out a component. It doesn't + // represent a real resource, nor does it have normal properties that need to be + // tracked for differences (since changes to its properties don't represent changes + // to resources in the real world). + // + // So, to avoid these problems, while allowing a flexible and simple programming + // model, we just serialize out the component as its urn. This allows the component + // to be identified and tracked in a reasonable manner, while not causing us to + // compute or embed information about it that is not needed, and which can lead to + // deadlocks. + if (_excessiveDebugOutput) + { + Log.Debug($"Serialize property[{ctx}]: Encountered ComponentResource"); + } + + return await SerializeAsync($"{ctx}.urn", componentResource.Urn).ConfigureAwait(false); + } + + if (prop is IDictionary dictionary) + return await SerializeDictionaryAsync(ctx, dictionary).ConfigureAwait(false); + + if (prop is IList list) + return await SerializeListAsync(ctx, list).ConfigureAwait(false); + + throw new InvalidOperationException($"{prop.GetType().FullName} is not a supported argument type.\n\t{ctx}"); + } + + private async Task> SerializeAssetOrArchiveAsync(string ctx, AssetOrArchive assetOrArchive) + { + if (_excessiveDebugOutput) + { + Log.Debug($"Serialize property[{ctx}]: asset/archive={assetOrArchive.GetType().Name}"); + } + + var propName = assetOrArchive.PropName; + var value = await SerializeAsync(ctx + "." + propName, assetOrArchive.Value).ConfigureAwait(false); + + var builder = ImmutableDictionary.CreateBuilder(); + builder.Add(Constants.SpecialSigKey, assetOrArchive.SigKey); + builder.Add(assetOrArchive.PropName, value!); + return builder.ToImmutable(); + } + + private async Task> SerializeResourceArgsAsync(string ctx, ResourceArgs args) + { + if (_excessiveDebugOutput) + { + Log.Debug($"Serialize property[{ctx}]: Recursing into ResourceArgs"); + } + + var dictionary = await args.ToDictionaryAsync().ConfigureAwait(false); + return await SerializeDictionaryAsync(ctx, dictionary).ConfigureAwait(false); + } + + private async Task> SerializeListAsync(string ctx, IList list) + { + if (_excessiveDebugOutput) + { + Log.Debug($"Serialize property[{ctx}]: Hit list"); + } + + var result = ImmutableArray.CreateBuilder(list.Count); + for (int i = 0, n = list.Count; i < n; i++) + { + if (_excessiveDebugOutput) + { + Log.Debug($"Serialize property[{ctx}]: array[{i}] element"); + } + + result.Add(await SerializeAsync($"{ctx}[{i}]", list[i]).ConfigureAwait(false)); + } + + return result.MoveToImmutable(); + } + + private async Task> SerializeDictionaryAsync(string ctx, IDictionary dictionary) + { + if (_excessiveDebugOutput) + { + Log.Debug($"Serialize property[{ctx}]: Hit dictionary"); + } + + var result = ImmutableDictionary.CreateBuilder(); + foreach (var key in dictionary.Keys) + { + if (!(key is string stringKey)) + { + throw new InvalidOperationException( + $"Dictionaries are only supported with string keys:\n\t{ctx}"); + } + + if (_excessiveDebugOutput) + { + Log.Debug($"Serialize property[{ctx}]: object.{stringKey}"); + } + + // When serializing an object, we omit any keys with null values. This matches + // JSON semantics. + var v = await SerializeAsync($"{ctx}.{stringKey}", dictionary[stringKey]).ConfigureAwait(false); + if (v != null) + { + result[stringKey] = v; + } + } + + return result.ToImmutable(); + } + + /// + /// Internal for testing purposes. + /// + internal static Value CreateValue(object? value) + => value switch + { + null => Value.ForNull(), + int i => Value.ForNumber(i), + double d => Value.ForNumber(d), + bool b => Value.ForBool(b), + string s => Value.ForString(s), + ImmutableArray list => Value.ForList(list.Select(v => CreateValue(v)).ToArray()), + ImmutableDictionary dict => Value.ForStruct(CreateStruct(dict)), + _ => throw new InvalidOperationException("Unsupported value when converting to protobuf: " + value.GetType().FullName), + }; + + /// + /// Given a produced by , + /// produces the equivalent that can be passed to the Pulumi engine. + /// + public static Struct CreateStruct(ImmutableDictionary serializedDictionary) + { + var result = new Struct(); + foreach (var key in serializedDictionary.Keys.OrderBy(k => k)) + { + result.Fields.Add(key, CreateValue(serializedDictionary[key])); + } + return result; + } + } +} diff --git a/sdk/dotnet/Pulumi/Stack.cs b/sdk/dotnet/Pulumi/Stack.cs new file mode 100644 index 000000000..2e40a44af --- /dev/null +++ b/sdk/dotnet/Pulumi/Stack.cs @@ -0,0 +1,77 @@ +// Copyright 2016-2019, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; + +namespace Pulumi +{ + /// + /// Stack is the root resource for a Pulumi stack. Before invoking the init callback, it + /// registers itself as the root resource with the Pulumi engine. + /// + /// An instance of this will be automatically created when any overload is called. + /// + internal sealed class Stack : ComponentResource + { + /// + /// Constant to represent the 'root stack' resource for a Pulumi application. The purpose + /// of this is solely to make it easy to write an like so: + /// + /// aliases = { new Alias { Parent = Pulumi.Stack.Root } } + /// + /// This indicates that the prior name for a resource was created based on it being parented + /// directly by the stack itself and no other resources. Note: this is equivalent to: + /// + /// aliases = { new Alias { Parent = null } } + /// + /// However, the former form is preferable as it is more self-descriptive, while the latter + /// may look a bit confusing and may incorrectly look like something that could be removed + /// without changing semantics. + /// + public static readonly Resource? Root = null; + + /// + /// is the type name that should be used to construct + /// the root component in the tree of Pulumi resources allocated by a deployment.This must + /// be kept up to date with + /// github.com/pulumi/pulumi/pkg/resource/stack.RootPulumiStackTypeName. + /// + internal const string _rootPulumiStackTypeName = "pulumi:pulumi:Stack"; + + /// + /// The outputs of this stack, if the init callback exited normally. + /// + public readonly Output> Outputs = + Output.Create>(ImmutableDictionary.Empty); + + internal Stack(Func>> init) + : base(_rootPulumiStackTypeName, $"{Deployment.Instance.ProjectName}-{Deployment.Instance.StackName}") + { + Deployment.InternalInstance.Stack = this; + + try + { + this.Outputs = Output.Create(RunInitAsync(init)); + } + finally + { + this.RegisterOutputs(this.Outputs); + } + } + + private async Task> RunInitAsync(Func>> init) + { + // Ensure we are known as the root resource. This is needed before we execute any user + // code as many codepaths will request the root resource. + await Deployment.InternalInstance.SetRootResourceAsync(this).ConfigureAwait(false); + + var dictionary = await init().ConfigureAwait(false); + return dictionary == null + ? ImmutableDictionary.Empty + : dictionary.ToImmutableDictionary(); + } + } +} diff --git a/sdk/dotnet/README.md b/sdk/dotnet/README.md new file mode 100644 index 000000000..663e502c3 --- /dev/null +++ b/sdk/dotnet/README.md @@ -0,0 +1,78 @@ +# Experimental .NET Language Provider + +An early prototype of a .NET language provider for Pulumi. + + +## Building and Running + +To build, you'll want to install the .NET Core 3.0 SDK or greater, and ensure +`dotnet` is on your path. Once that it does, running `make` in either the root +directory or the `sdk/dotnet` directory will build and install the language +plugin. + +Once this is done you can write a Pulumi app written on top of .NET. See the +`sdk/dotnet/examples` directory showing how this can be done with C#, F#, or VB. +Your application will need to reference the `Pulumi.dll` built above. + +Here's a simple example of a Pulumi app written in C# that creates some simple +AWS resources: + +```c# +// Copyright 2016-2019, Pulumi Corporation + +using System.Collections.Generic; +using System.Threading.Tasks; +using Pulumi; +using Pulumi.Aws.S3; + +class Program +{ + static Task Main() + => Deployment.RunAsync(() => + { + var config = new Config("hello-dotnet"); + var name = config.Require("name"); + + // Create the bucket, and make it public. + var bucket = new Bucket(name, new BucketArgs { Acl = "public-read" }); + + // Add some content. + var content = new BucketObject($"{name}-content", new BucketObjectArgs + { + Acl = "public-read", + Bucket = bucket.Id, + ContentType = "text/plain; charset=utf8", + Key = "hello.txt", + Source = new StringAsset("Made with ❤, Pulumi, and .NET"), + }); + + // Return some values that will become the Outputs of the stack. + return new Dictionary + { + { "hello", "world" }, + { "bucket-id", bucket.Id }, + { "content-id", content.Id }, + { "object-url", Output.Format($"http://{bucket.BucketDomainName}/{content.Key}") }, + }; + }); +} +``` + +Make a Pulumi.yaml file: + +``` +$ cat Pulumi.yaml + +name: hello-dotnet +runtime: dotnet +``` + +Then, configure it: + +``` +$ pulumi stack init hello-dotnet +$ pulumi config set name hello-dotnet +$ pulumi config set aws:region us-west-2 +``` + +And finally, preview and update as you would any other Pulumi project. diff --git a/sdk/dotnet/cmd/pulumi-language-dotnet/main.go b/sdk/dotnet/cmd/pulumi-language-dotnet/main.go new file mode 100644 index 000000000..06d7bfb61 --- /dev/null +++ b/sdk/dotnet/cmd/pulumi-language-dotnet/main.go @@ -0,0 +1,355 @@ +// Copyright 2016-2018, Pulumi Corporation. All rights reserved. + +// pulumi-language-dotnet serves as the "language host" for Pulumi programs written in .NET. It is ultimately +// responsible for spawning the language runtime that executes the program. +// +// The program being executed is executed by a shim exe called `pulumi-language-dotnet-exec`. This script is +// written in the hosted language (in this case, C#) and is responsible for initiating RPC links to the resource +// monitor and engine. +// +// It's therefore the responsibility of this program to implement the LanguageHostServer endpoint by spawning +// instances of `pulumi-language-dotnet-exec` and forwarding the RPC request arguments to the command-line. +package main + +import ( + "bytes" + "context" + "encoding/json" + "flag" + "fmt" + "math/rand" + "os" + "os/exec" + "strings" + "syscall" + + "github.com/golang/glog" + pbempty "github.com/golang/protobuf/ptypes/empty" + "github.com/pkg/errors" + "github.com/pulumi/pulumi/pkg/util/cmdutil" + "github.com/pulumi/pulumi/pkg/util/logging" + "github.com/pulumi/pulumi/pkg/util/rpcutil" + "github.com/pulumi/pulumi/pkg/version" + pulumirpc "github.com/pulumi/pulumi/sdk/proto/go" + "google.golang.org/grpc" +) + +var ( + // A exit-code we recognize when the nodejs process exits. If we see this error, there's no + // need for us to print any additional error messages since the user already got a a good + // one they can handle. + dotnetProcessExitedAfterShowingUserActionableMessage = 32 +) + +// Launches the language host RPC endpoint, which in turn fires up an RPC server implementing the +// LanguageRuntimeServer RPC endpoint. +func main() { + var tracing string + flag.StringVar(&tracing, "tracing", "", "Emit tracing to a Zipkin-compatible tracing endpoint") + + // You can use the below flag to request that the language host load a specific executor instead of probing the + // PATH. This can be used during testing to override the default location. + var givenExecutor string + flag.StringVar(&givenExecutor, "use-executor", "", + "Use the given program as the executor instead of looking for one on PATH") + + flag.Parse() + args := flag.Args() + logging.InitLogging(false, 0, false) + cmdutil.InitTracing("pulumi-language-dotnet", "pulumi-language-dotnet", tracing) + var dotnetExec string + if givenExecutor == "" { + pathExec, err := exec.LookPath("dotnet") + if err != nil { + err = errors.Wrap(err, "could not find `dotnet` on the $PATH") + cmdutil.Exit(err) + } + + glog.V(3).Infof("language host identified executor from path: `%s`", pathExec) + dotnetExec = pathExec + } else { + glog.V(3).Infof("language host asked to use specific executor: `%s`", givenExecutor) + dotnetExec = givenExecutor + } + + // Optionally pluck out the engine so we can do logging, etc. + var engineAddress string + if len(args) > 0 { + engineAddress = args[0] + } + + // Fire up a gRPC server, letting the kernel choose a free port. + port, done, err := rpcutil.Serve(0, nil, []func(*grpc.Server) error{ + func(srv *grpc.Server) error { + host := newLanguageHost(dotnetExec, engineAddress, tracing) + pulumirpc.RegisterLanguageRuntimeServer(srv, host) + return nil + }, + }, nil) + if err != nil { + cmdutil.Exit(errors.Wrapf(err, "could not start language host RPC server")) + } + + // Otherwise, print out the port so that the spawner knows how to reach us. + fmt.Printf("%d\n", port) + + // And finally wait for the server to stop serving. + if err := <-done; err != nil { + cmdutil.Exit(errors.Wrapf(err, "language host RPC stopped serving")) + } +} + +// dotnetLanguageHost implements the LanguageRuntimeServer interface +// for use as an API endpoint. +type dotnetLanguageHost struct { + exec string + engineAddress string + tracing string +} + +func newLanguageHost(exec, engineAddress, tracing string) pulumirpc.LanguageRuntimeServer { + + return &dotnetLanguageHost{ + exec: exec, + engineAddress: engineAddress, + tracing: tracing, + } +} + +// GetRequiredPlugins computes the complete set of anticipated plugins required by a program. +func (host *dotnetLanguageHost) GetRequiredPlugins(ctx context.Context, + req *pulumirpc.GetRequiredPluginsRequest) (*pulumirpc.GetRequiredPluginsResponse, error) { + // TODO: implement this. + return &pulumirpc.GetRequiredPluginsResponse{}, nil +} + +// RPC endpoint for LanguageRuntimeServer::Run +func (host *dotnetLanguageHost) Run(ctx context.Context, req *pulumirpc.RunRequest) (*pulumirpc.RunResponse, error) { + if err := host.DotnetBuild(ctx, req); err != nil { + return &pulumirpc.RunResponse{Error: err.Error()}, nil + } + + return host.DotnetRun(ctx, req) +} + +func (host *dotnetLanguageHost) DotnetBuild(ctx context.Context, req *pulumirpc.RunRequest) error { + args := []string{"build"} + + if req.GetProgram() != "" { + args = append(args, req.GetProgram()) + } + + if glog.V(5) { + commandStr := strings.Join(args, " ") + glog.V(5).Infoln("Language host launching process: ", host.exec, commandStr) + } + + // Make a connection to the real engine that we will log messages to. + conn, err := grpc.Dial(host.engineAddress, grpc.WithInsecure()) + if err != nil { + return errors.Wrapf(err, "language host could not make connection to engine") + } + + // Make a client around that connection. We can then make our own server that will act as a + // monitor for the sdk and forward to the real monitor. + engineClient := pulumirpc.NewEngineClient(conn) + + // Buffer the writes we see from dotnet from its stdout and stderr streams. We will display + // these ephemerally as `dotnet build` runs. If the build does fail though, we will dump + // messages back to our own stdout/stderr so they get picked up and displayed to the user. + streamID := rand.Int31() + + infoBuffer := &bytes.Buffer{} + errorBuffer := &bytes.Buffer{} + + infoWriter := &logWriter{ + ctx: ctx, + engineClient: engineClient, + streamID: streamID, + buffer: infoBuffer, + severity: pulumirpc.LogSeverity_INFO, + } + + errorWriter := &logWriter{ + ctx: ctx, + engineClient: engineClient, + streamID: streamID, + buffer: errorBuffer, + severity: pulumirpc.LogSeverity_ERROR, + } + + // Now simply spawn a process to execute the requested program, wiring up stdout/stderr directly. + cmd := exec.Command(host.exec, args...) // nolint: gas, intentionally running dynamic program name. + + cmd.Stdout = infoWriter + cmd.Stderr = errorWriter + + _, err = engineClient.Log(ctx, &pulumirpc.LogRequest{ + Message: "running 'dotnet build'", + Urn: "", + Ephemeral: true, + StreamId: streamID, + Severity: pulumirpc.LogSeverity_INFO, + }) + if err != nil { + return err + } + + if err := cmd.Run(); err != nil { + // The command failed. Dump any data we collected to the actual stdout/stderr streams so + // they get displayed to the user. + os.Stdout.Write(infoBuffer.Bytes()) + os.Stderr.Write(errorBuffer.Bytes()) + + if exiterr, ok := err.(*exec.ExitError); ok { + // If the program ran, but exited with a non-zero error code. This will happen often, since user + // errors will trigger this. So, the error message should look as nice as possible. + if status, stok := exiterr.Sys().(syscall.WaitStatus); stok { + return errors.Errorf("'dotnet build' exited with non-zero exit code: %d", status.ExitStatus()) + } + + return errors.Wrapf(exiterr, "'dotnet build' exited unexpectedly") + } + + // Otherwise, we didn't even get to run the program. This ought to never happen unless there's + // a bug or system condition that prevented us from running the language exec. Issue a scarier error. + return errors.Wrapf(err, "Problem executing 'dotnet build'") + } + + _, err = engineClient.Log(ctx, &pulumirpc.LogRequest{ + Message: "'dotnet build' completed successfully", + Urn: "", + Ephemeral: true, + StreamId: streamID, + Severity: pulumirpc.LogSeverity_INFO, + }) + + return err +} + +type logWriter struct { + ctx context.Context + engineClient pulumirpc.EngineClient + streamID int32 + severity pulumirpc.LogSeverity + buffer *bytes.Buffer +} + +func (w *logWriter) Write(p []byte) (n int, err error) { + n, err = w.buffer.Write(p) + if err != nil { + return + } + + _, err = w.engineClient.Log(w.ctx, &pulumirpc.LogRequest{ + Message: string(p), + Urn: "", + Ephemeral: true, + StreamId: w.streamID, + Severity: w.severity, + }) + + if err != nil { + return 0, err + } + + return len(p), nil +} + +func (host *dotnetLanguageHost) DotnetRun( + ctx context.Context, req *pulumirpc.RunRequest) (*pulumirpc.RunResponse, error) { + + config, err := host.constructConfig(req) + if err != nil { + err = errors.Wrap(err, "failed to serialize configuration") + return nil, err + } + + args := []string{"run"} + + if req.GetProgram() != "" { + args = append(args, req.GetProgram()) + } + + if glog.V(5) { + commandStr := strings.Join(args, " ") + glog.V(5).Infoln("Language host launching process: ", host.exec, commandStr) + } + + // Now simply spawn a process to execute the requested program, wiring up stdout/stderr directly. + var errResult string + cmd := exec.Command(host.exec, args...) // nolint: gas, intentionally running dynamic program name. + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = host.constructEnv(req, config) + if err := cmd.Run(); err != nil { + if exiterr, ok := err.(*exec.ExitError); ok { + // If the program ran, but exited with a non-zero error code. This will happen often, since user + // errors will trigger this. So, the error message should look as nice as possible. + if status, stok := exiterr.Sys().(syscall.WaitStatus); stok { + // Check if we got special exit code that means "we already gave the user an + // actionable message". In that case, we can simply bail out and terminate `pulumi` + // without showing any more messages. + if status.ExitStatus() == dotnetProcessExitedAfterShowingUserActionableMessage { + return &pulumirpc.RunResponse{Error: "", Bail: true}, nil + } + + err = errors.Errorf("Program exited with non-zero exit code: %d", status.ExitStatus()) + } else { + err = errors.Wrapf(exiterr, "Program exited unexpectedly") + } + } else { + // Otherwise, we didn't even get to run the program. This ought to never happen unless there's + // a bug or system condition that prevented us from running the language exec. Issue a scarier error. + err = errors.Wrapf(err, "Problem executing program (could not run language executor)") + } + + errResult = err.Error() + } + + return &pulumirpc.RunResponse{Error: errResult}, nil +} + +func (host *dotnetLanguageHost) constructEnv(req *pulumirpc.RunRequest, config string) []string { + env := os.Environ() + + maybeAppendEnv := func(k, v string) { + if v != "" { + env = append(env, strings.ToUpper("PULUMI_"+k)+"="+v) + } + } + + maybeAppendEnv("monitor", req.GetMonitorAddress()) + maybeAppendEnv("engine", host.engineAddress) + maybeAppendEnv("project", req.GetProject()) + maybeAppendEnv("stack", req.GetStack()) + maybeAppendEnv("pwd", req.GetPwd()) + maybeAppendEnv("dry_run", fmt.Sprintf("%v", req.GetDryRun())) + maybeAppendEnv("query_mode", fmt.Sprint(req.GetQueryMode())) + maybeAppendEnv("parallel", fmt.Sprint(req.GetParallel())) + maybeAppendEnv("tracing", host.tracing) + maybeAppendEnv("config", config) + + return env +} + +// constructConfig json-serializes the configuration data given as part of a RunRequest. +func (host *dotnetLanguageHost) constructConfig(req *pulumirpc.RunRequest) (string, error) { + configMap := req.GetConfig() + if configMap == nil { + return "", nil + } + + configJSON, err := json.Marshal(configMap) + if err != nil { + return "", err + } + + return string(configJSON), nil +} + +func (host *dotnetLanguageHost) GetPluginInfo(ctx context.Context, req *pbempty.Empty) (*pulumirpc.PluginInfo, error) { + return &pulumirpc.PluginInfo{ + Version: version.Version, + }, nil +} diff --git a/sdk/dotnet/dotnet.sln b/sdk/dotnet/dotnet.sln new file mode 100644 index 000000000..12d76ab91 --- /dev/null +++ b/sdk/dotnet/dotnet.sln @@ -0,0 +1,150 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29411.138 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pulumi", "Pulumi\Pulumi.csproj", "{0A2BFED8-13F3-43A4-A38B-B5D1651203EB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{F634CAAC-92CB-42A4-8229-37EABD9CAC5D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "bucket", "examples\bucket\bucket.csproj", "{02C0AC19-181C-4B6E-BD07-D98B5DC81370}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pulumi.Tests", "Pulumi.Tests\Pulumi.Tests.csproj", "{B732E03B-BA4D-4F4A-AE38-5833BC2DAC7C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PulumiAzure", "examples\PulumiAzure\PulumiAzure.csproj", "{67972C68-173F-40D5-B0E5-974FC620943C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpExamples", "examples\CSharpExamples\CSharpExamples.csproj", "{E93FA892-060D-47A3-951E-C061C6487482}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharpExamples", "examples\FSharpExamples\FSharpExamples.fsproj", "{193DBB9E-BE39-4E32-BEC3-B40762F18C67}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Pulumi.FSharp", "Pulumi.FSharp\Pulumi.FSharp.fsproj", "{F45E8B4A-DAF3-48E8-B9D6-01924AF2188D}" +EndProject +Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "VBExamples", "examples\VBExamples\VBExamples.vbproj", "{E2662E3A-4F40-42EB-B0DB-8F9166C2CE61}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D4626281-DB93-4BB3-B23C-02F47032E4FA}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitignore = .gitignore + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0A2BFED8-13F3-43A4-A38B-B5D1651203EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A2BFED8-13F3-43A4-A38B-B5D1651203EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A2BFED8-13F3-43A4-A38B-B5D1651203EB}.Debug|x64.ActiveCfg = Debug|Any CPU + {0A2BFED8-13F3-43A4-A38B-B5D1651203EB}.Debug|x64.Build.0 = Debug|Any CPU + {0A2BFED8-13F3-43A4-A38B-B5D1651203EB}.Debug|x86.ActiveCfg = Debug|Any CPU + {0A2BFED8-13F3-43A4-A38B-B5D1651203EB}.Debug|x86.Build.0 = Debug|Any CPU + {0A2BFED8-13F3-43A4-A38B-B5D1651203EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A2BFED8-13F3-43A4-A38B-B5D1651203EB}.Release|Any CPU.Build.0 = Release|Any CPU + {0A2BFED8-13F3-43A4-A38B-B5D1651203EB}.Release|x64.ActiveCfg = Release|Any CPU + {0A2BFED8-13F3-43A4-A38B-B5D1651203EB}.Release|x64.Build.0 = Release|Any CPU + {0A2BFED8-13F3-43A4-A38B-B5D1651203EB}.Release|x86.ActiveCfg = Release|Any CPU + {0A2BFED8-13F3-43A4-A38B-B5D1651203EB}.Release|x86.Build.0 = Release|Any CPU + {02C0AC19-181C-4B6E-BD07-D98B5DC81370}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02C0AC19-181C-4B6E-BD07-D98B5DC81370}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02C0AC19-181C-4B6E-BD07-D98B5DC81370}.Debug|x64.ActiveCfg = Debug|Any CPU + {02C0AC19-181C-4B6E-BD07-D98B5DC81370}.Debug|x64.Build.0 = Debug|Any CPU + {02C0AC19-181C-4B6E-BD07-D98B5DC81370}.Debug|x86.ActiveCfg = Debug|Any CPU + {02C0AC19-181C-4B6E-BD07-D98B5DC81370}.Debug|x86.Build.0 = Debug|Any CPU + {02C0AC19-181C-4B6E-BD07-D98B5DC81370}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02C0AC19-181C-4B6E-BD07-D98B5DC81370}.Release|Any CPU.Build.0 = Release|Any CPU + {02C0AC19-181C-4B6E-BD07-D98B5DC81370}.Release|x64.ActiveCfg = Release|Any CPU + {02C0AC19-181C-4B6E-BD07-D98B5DC81370}.Release|x64.Build.0 = Release|Any CPU + {02C0AC19-181C-4B6E-BD07-D98B5DC81370}.Release|x86.ActiveCfg = Release|Any CPU + {02C0AC19-181C-4B6E-BD07-D98B5DC81370}.Release|x86.Build.0 = Release|Any CPU + {B732E03B-BA4D-4F4A-AE38-5833BC2DAC7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B732E03B-BA4D-4F4A-AE38-5833BC2DAC7C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B732E03B-BA4D-4F4A-AE38-5833BC2DAC7C}.Debug|x64.ActiveCfg = Debug|Any CPU + {B732E03B-BA4D-4F4A-AE38-5833BC2DAC7C}.Debug|x64.Build.0 = Debug|Any CPU + {B732E03B-BA4D-4F4A-AE38-5833BC2DAC7C}.Debug|x86.ActiveCfg = Debug|Any CPU + {B732E03B-BA4D-4F4A-AE38-5833BC2DAC7C}.Debug|x86.Build.0 = Debug|Any CPU + {B732E03B-BA4D-4F4A-AE38-5833BC2DAC7C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B732E03B-BA4D-4F4A-AE38-5833BC2DAC7C}.Release|Any CPU.Build.0 = Release|Any CPU + {B732E03B-BA4D-4F4A-AE38-5833BC2DAC7C}.Release|x64.ActiveCfg = Release|Any CPU + {B732E03B-BA4D-4F4A-AE38-5833BC2DAC7C}.Release|x64.Build.0 = Release|Any CPU + {B732E03B-BA4D-4F4A-AE38-5833BC2DAC7C}.Release|x86.ActiveCfg = Release|Any CPU + {B732E03B-BA4D-4F4A-AE38-5833BC2DAC7C}.Release|x86.Build.0 = Release|Any CPU + {67972C68-173F-40D5-B0E5-974FC620943C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67972C68-173F-40D5-B0E5-974FC620943C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67972C68-173F-40D5-B0E5-974FC620943C}.Debug|x64.ActiveCfg = Debug|Any CPU + {67972C68-173F-40D5-B0E5-974FC620943C}.Debug|x64.Build.0 = Debug|Any CPU + {67972C68-173F-40D5-B0E5-974FC620943C}.Debug|x86.ActiveCfg = Debug|Any CPU + {67972C68-173F-40D5-B0E5-974FC620943C}.Debug|x86.Build.0 = Debug|Any CPU + {67972C68-173F-40D5-B0E5-974FC620943C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67972C68-173F-40D5-B0E5-974FC620943C}.Release|Any CPU.Build.0 = Release|Any CPU + {67972C68-173F-40D5-B0E5-974FC620943C}.Release|x64.ActiveCfg = Release|Any CPU + {67972C68-173F-40D5-B0E5-974FC620943C}.Release|x64.Build.0 = Release|Any CPU + {67972C68-173F-40D5-B0E5-974FC620943C}.Release|x86.ActiveCfg = Release|Any CPU + {67972C68-173F-40D5-B0E5-974FC620943C}.Release|x86.Build.0 = Release|Any CPU + {E93FA892-060D-47A3-951E-C061C6487482}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E93FA892-060D-47A3-951E-C061C6487482}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E93FA892-060D-47A3-951E-C061C6487482}.Debug|x64.ActiveCfg = Debug|Any CPU + {E93FA892-060D-47A3-951E-C061C6487482}.Debug|x64.Build.0 = Debug|Any CPU + {E93FA892-060D-47A3-951E-C061C6487482}.Debug|x86.ActiveCfg = Debug|Any CPU + {E93FA892-060D-47A3-951E-C061C6487482}.Debug|x86.Build.0 = Debug|Any CPU + {E93FA892-060D-47A3-951E-C061C6487482}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E93FA892-060D-47A3-951E-C061C6487482}.Release|Any CPU.Build.0 = Release|Any CPU + {E93FA892-060D-47A3-951E-C061C6487482}.Release|x64.ActiveCfg = Release|Any CPU + {E93FA892-060D-47A3-951E-C061C6487482}.Release|x64.Build.0 = Release|Any CPU + {E93FA892-060D-47A3-951E-C061C6487482}.Release|x86.ActiveCfg = Release|Any CPU + {E93FA892-060D-47A3-951E-C061C6487482}.Release|x86.Build.0 = Release|Any CPU + {193DBB9E-BE39-4E32-BEC3-B40762F18C67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {193DBB9E-BE39-4E32-BEC3-B40762F18C67}.Debug|Any CPU.Build.0 = Debug|Any CPU + {193DBB9E-BE39-4E32-BEC3-B40762F18C67}.Debug|x64.ActiveCfg = Debug|Any CPU + {193DBB9E-BE39-4E32-BEC3-B40762F18C67}.Debug|x64.Build.0 = Debug|Any CPU + {193DBB9E-BE39-4E32-BEC3-B40762F18C67}.Debug|x86.ActiveCfg = Debug|Any CPU + {193DBB9E-BE39-4E32-BEC3-B40762F18C67}.Debug|x86.Build.0 = Debug|Any CPU + {193DBB9E-BE39-4E32-BEC3-B40762F18C67}.Release|Any CPU.ActiveCfg = Release|Any CPU + {193DBB9E-BE39-4E32-BEC3-B40762F18C67}.Release|Any CPU.Build.0 = Release|Any CPU + {193DBB9E-BE39-4E32-BEC3-B40762F18C67}.Release|x64.ActiveCfg = Release|Any CPU + {193DBB9E-BE39-4E32-BEC3-B40762F18C67}.Release|x64.Build.0 = Release|Any CPU + {193DBB9E-BE39-4E32-BEC3-B40762F18C67}.Release|x86.ActiveCfg = Release|Any CPU + {193DBB9E-BE39-4E32-BEC3-B40762F18C67}.Release|x86.Build.0 = Release|Any CPU + {F45E8B4A-DAF3-48E8-B9D6-01924AF2188D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F45E8B4A-DAF3-48E8-B9D6-01924AF2188D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F45E8B4A-DAF3-48E8-B9D6-01924AF2188D}.Debug|x64.ActiveCfg = Debug|Any CPU + {F45E8B4A-DAF3-48E8-B9D6-01924AF2188D}.Debug|x64.Build.0 = Debug|Any CPU + {F45E8B4A-DAF3-48E8-B9D6-01924AF2188D}.Debug|x86.ActiveCfg = Debug|Any CPU + {F45E8B4A-DAF3-48E8-B9D6-01924AF2188D}.Debug|x86.Build.0 = Debug|Any CPU + {F45E8B4A-DAF3-48E8-B9D6-01924AF2188D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F45E8B4A-DAF3-48E8-B9D6-01924AF2188D}.Release|Any CPU.Build.0 = Release|Any CPU + {F45E8B4A-DAF3-48E8-B9D6-01924AF2188D}.Release|x64.ActiveCfg = Release|Any CPU + {F45E8B4A-DAF3-48E8-B9D6-01924AF2188D}.Release|x64.Build.0 = Release|Any CPU + {F45E8B4A-DAF3-48E8-B9D6-01924AF2188D}.Release|x86.ActiveCfg = Release|Any CPU + {F45E8B4A-DAF3-48E8-B9D6-01924AF2188D}.Release|x86.Build.0 = Release|Any CPU + {E2662E3A-4F40-42EB-B0DB-8F9166C2CE61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2662E3A-4F40-42EB-B0DB-8F9166C2CE61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2662E3A-4F40-42EB-B0DB-8F9166C2CE61}.Debug|x64.ActiveCfg = Debug|Any CPU + {E2662E3A-4F40-42EB-B0DB-8F9166C2CE61}.Debug|x64.Build.0 = Debug|Any CPU + {E2662E3A-4F40-42EB-B0DB-8F9166C2CE61}.Debug|x86.ActiveCfg = Debug|Any CPU + {E2662E3A-4F40-42EB-B0DB-8F9166C2CE61}.Debug|x86.Build.0 = Debug|Any CPU + {E2662E3A-4F40-42EB-B0DB-8F9166C2CE61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2662E3A-4F40-42EB-B0DB-8F9166C2CE61}.Release|Any CPU.Build.0 = Release|Any CPU + {E2662E3A-4F40-42EB-B0DB-8F9166C2CE61}.Release|x64.ActiveCfg = Release|Any CPU + {E2662E3A-4F40-42EB-B0DB-8F9166C2CE61}.Release|x64.Build.0 = Release|Any CPU + {E2662E3A-4F40-42EB-B0DB-8F9166C2CE61}.Release|x86.ActiveCfg = Release|Any CPU + {E2662E3A-4F40-42EB-B0DB-8F9166C2CE61}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {02C0AC19-181C-4B6E-BD07-D98B5DC81370} = {F634CAAC-92CB-42A4-8229-37EABD9CAC5D} + {E93FA892-060D-47A3-951E-C061C6487482} = {F634CAAC-92CB-42A4-8229-37EABD9CAC5D} + {193DBB9E-BE39-4E32-BEC3-B40762F18C67} = {F634CAAC-92CB-42A4-8229-37EABD9CAC5D} + {E2662E3A-4F40-42EB-B0DB-8F9166C2CE61} = {F634CAAC-92CB-42A4-8229-37EABD9CAC5D} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {273EE841-009F-495B-A58A-681A8CC9DBAC} + EndGlobalSection +EndGlobal diff --git a/sdk/dotnet/examples/CSharpExamples/CSharpExamples.csproj b/sdk/dotnet/examples/CSharpExamples/CSharpExamples.csproj new file mode 100644 index 000000000..014d71db5 --- /dev/null +++ b/sdk/dotnet/examples/CSharpExamples/CSharpExamples.csproj @@ -0,0 +1,13 @@ + + + + Exe + netcoreapp3.0 + + + + + + + + diff --git a/sdk/dotnet/examples/CSharpExamples/CosmosApp.cs b/sdk/dotnet/examples/CSharpExamples/CosmosApp.cs new file mode 100644 index 000000000..2b87817ce --- /dev/null +++ b/sdk/dotnet/examples/CSharpExamples/CosmosApp.cs @@ -0,0 +1,153 @@ +// Copyright 2016-2019, Pulumi Corporation + +#nullable enable + +using System.Collections.Generic; +using Pulumi.Azure.Core; +using Pulumi.Azure.AppService; +using Pulumi.Azure.Storage; +using CosmosDB = Pulumi.Azure.CosmosDB; +using System; +using System.Linq; + +namespace Pulumi.CSharpExamples +{ + public class CosmosAppArgs + { + public Input? ResourceGroupName { get; set; } + public InputList? Locations { get; set; } + public Input? DatabaseName { get; set; } + public Input? ContainerName { get; set; } + } + + public class CosmosApp : ComponentResource + { + public CosmosApp(string name, CosmosAppArgs args, ResourceOptions? options = null) + : base("examples:azure:CosmosApp", name, options) + { + if (args.Locations == null) + { + throw new ArgumentException(nameof(args.Locations)); + } + + var primaryLocation = args.Locations.ToOutput().Apply(ls => ls[0]); + var locations = args.Locations.ToOutput(); + + var cosmosAccount = new CosmosDB.Account($"cosmos-{name}", + new CosmosDB.AccountArgs + { + ResourceGroupName = args.ResourceGroupName, + Location = primaryLocation, + GeoLocations = locations.Apply(ls => + ls.Select((l, i) => + { + return new CosmosDB.AccountGeoLocation + { + Location = l, + FailoverPriority = i + }; + })), + OfferType = "Standard", + ConsistencyPolicy = new CosmosDB.AccountConsistencyPolicy + { + ConsistencyLevel = "Session", + }, + }); + } + } + + public class GlobalApp + { + public static Dictionary Run() + { + var resourceGroup = new ResourceGroup("dotnet-rg", new ResourceGroupArgs + { + Location = "West Europe" + }); + + var cosmosapp = new CosmosApp("capp", new CosmosAppArgs + { + ResourceGroupName = resourceGroup.Name, + Locations = new[] { resourceGroup.Location }, + }); + + var storageAccount = new Account("sa", new AccountArgs + { + ResourceGroupName = resourceGroup.Name, + AccountReplicationType = "LRS", + AccountTier = "Standard", + }); + + var appServicePlan = new Plan("asp", new PlanArgs + { + ResourceGroupName = resourceGroup.Name, + Kind = "App", + Sku = new PlanSkuArgs + { + Tier = "Basic", + Size = "B1", + }, + }); + + var container = new Container("c", new ContainerArgs + { + StorageAccountName = storageAccount.Name, + ContainerAccessType = "private", + }); + + var blob = new ZipBlob("zip", new ZipBlobArgs + { + StorageAccountName = storageAccount.Name, + StorageContainerName = container.Name, + Type = "block", + Content = new FileArchive("wwwroot"), + }); + + var codeBlobUrl = SharedAccessSignature.SignedBlobReadUrl(blob, storageAccount); + + //var username = "sa"; // TODO: Pulumi.Config + //var password = "pwd"; + //var sqlServer = new SqlServer("sql", new SqlServerArgs + //{ + // ResourceGroupName = resourceGroup.Name, + // AdministratorLogin = username, + // AdministratorLoginPassword = password, + // Version = "12.0", + //}); + + //var database = new Database("db", new DatabaseArgs + //{ + // ResourceGroupName = resourceGroup.Name, + // ServerName = sqlServer.Name, + // RequestedServiceObjectiveName = "S0", + //}); + + var app = new AppService("app", new AppServiceArgs + { + ResourceGroupName = resourceGroup.Name, + AppServicePlanId = appServicePlan.Id, + AppSettings = + { + { "WEBSITE_RUN_FROM_ZIP", codeBlobUrl }, + }, + //ConnectionStrings = new[] + //{ + // new AppService.ConnectionStringArgs + // { + // Name = "db", + // Type = "SQLAzure", + // Value = Output.All(sqlServer.Name, database.Name).Apply(values => + // { + // return $"Server= tcp:${values[0]}.database.windows.net;initial catalog=${values[1]};userID=${username};password=${password};Min Pool Size=0;Max Pool Size=30;Persist Security Info=true;"; + // }), + // }, + //}, + }); + + return new Dictionary + { + { "endpoint", app.DefaultSiteHostname }, + }; + } + } +} diff --git a/sdk/dotnet/examples/CSharpExamples/Minimal.cs b/sdk/dotnet/examples/CSharpExamples/Minimal.cs new file mode 100644 index 000000000..80309dcab --- /dev/null +++ b/sdk/dotnet/examples/CSharpExamples/Minimal.cs @@ -0,0 +1,33 @@ +// Copyright 2016-2019, Pulumi Corporation + +#nullable enable + +using System.Collections.Generic; +using Pulumi.Azure.Core; +using Storage = Pulumi.Azure.Storage; + +namespace Pulumi.CSharpExamples +{ + public class Minimal + { + public static IDictionary> Run() + { + var resourceGroup = new ResourceGroup("rg", new ResourceGroupArgs { Location = "West Europe" }); + + // "Account" without a namespace would be too vague, while "ResourceGroup" without namespace sounds good. + // We could suggest always using the namespace, but this makes new-ing of Args even longer and uglier? + var storageAccount = new Storage.Account("sa", new Storage.AccountArgs + { + ResourceGroupName = resourceGroup.Name, + AccountReplicationType = "LRS", + AccessTier = "Standard", + }); + + // How do we want to treat exports? + return new Dictionary> + { + { "accessKey", storageAccount.PrimaryAccessKey } + }; + } + } +} diff --git a/sdk/dotnet/examples/CSharpExamples/Program.cs b/sdk/dotnet/examples/CSharpExamples/Program.cs new file mode 100644 index 000000000..a3bbd83b3 --- /dev/null +++ b/sdk/dotnet/examples/CSharpExamples/Program.cs @@ -0,0 +1,14 @@ +// Copyright 2016-2019, Pulumi Corporation + +#nullable enable + +using System.Threading.Tasks; +using Pulumi; + +class Program +{ + static Task Main() + { + return Deployment.RunAsync(Pulumi.CSharpExamples.GlobalApp.Run); + } +} diff --git a/sdk/dotnet/examples/CSharpExamples/Pulumi.yaml b/sdk/dotnet/examples/CSharpExamples/Pulumi.yaml new file mode 100644 index 000000000..8472eb9d8 --- /dev/null +++ b/sdk/dotnet/examples/CSharpExamples/Pulumi.yaml @@ -0,0 +1,2 @@ +name: dotnet-azure +runtime: dotnet diff --git a/sdk/dotnet/examples/CSharpExamples/WebApp.cs b/sdk/dotnet/examples/CSharpExamples/WebApp.cs new file mode 100644 index 000000000..03e1164bd --- /dev/null +++ b/sdk/dotnet/examples/CSharpExamples/WebApp.cs @@ -0,0 +1,100 @@ +// Copyright 2016-2019, Pulumi Corporation + +#nullable enable + +using System.Collections.Generic; +using Pulumi.Azure.Core; +using Pulumi.Azure.AppService; +using Pulumi.Azure.Storage; + +namespace Pulumi.CSharpExamples +{ + public class WebApp + { + public static Dictionary Run() + { + var resourceGroup = new ResourceGroup("dotnet-rg", new ResourceGroupArgs + { + Location = "West Europe" + }); + + var storageAccount = new Account("sa", new AccountArgs + { + ResourceGroupName = resourceGroup.Name, + AccountReplicationType = "LRS", + AccountTier = "Standard", + }); + + var appServicePlan = new Plan("asp", new PlanArgs + { + ResourceGroupName = resourceGroup.Name, + Kind = "App", + Sku = new PlanSkuArgs + { + Tier = "Basic", + Size = "B1", + }, + }); + + var container = new Container("c", new ContainerArgs + { + StorageAccountName = storageAccount.Name, + ContainerAccessType = "private", + }); + + var blob = new ZipBlob("zip", new ZipBlobArgs + { + StorageAccountName = storageAccount.Name, + StorageContainerName = container.Name, + Type = "block", + Content = new FileArchive("wwwroot"), + }); + + var codeBlobUrl = SharedAccessSignature.SignedBlobReadUrl(blob, storageAccount); + + //var username = "sa"; // TODO: Pulumi.Config + //var password = "pwd"; + //var sqlServer = new SqlServer("sql", new SqlServerArgs + //{ + // ResourceGroupName = resourceGroup.Name, + // AdministratorLogin = username, + // AdministratorLoginPassword = password, + // Version = "12.0", + //}); + + //var database = new Database("db", new DatabaseArgs + //{ + // ResourceGroupName = resourceGroup.Name, + // ServerName = sqlServer.Name, + // RequestedServiceObjectiveName = "S0", + //}); + + var app = new AppService("app2", new AppServiceArgs + { + ResourceGroupName = resourceGroup.Name, + AppServicePlanId = appServicePlan.Id, + AppSettings = + { + { "WEBSITE_RUN_FROM_ZIP", codeBlobUrl }, + }, + //ConnectionStrings = new[] + //{ + // new AppService.ConnectionStringArgs + // { + // Name = "db", + // Type = "SQLAzure", + // Value = Output.All(sqlServer.Name, database.Name).Apply(values => + // { + // return $"Server= tcp:${values[0]}.database.windows.net;initial catalog=${values[1]};userID=${username};password=${password};Min Pool Size=0;Max Pool Size=30;Persist Security Info=true;"; + // }), + // }, + //}, + }); + + return new Dictionary + { + { "endpoint", app.DefaultSiteHostname }, + }; + } + } +} diff --git a/sdk/dotnet/examples/CSharpExamples/wwwroot/index.html b/sdk/dotnet/examples/CSharpExamples/wwwroot/index.html new file mode 100644 index 000000000..e4358a4e3 --- /dev/null +++ b/sdk/dotnet/examples/CSharpExamples/wwwroot/index.html @@ -0,0 +1 @@ +

OMG .NET works!!!

\ No newline at end of file diff --git a/sdk/dotnet/examples/CSharpScript/README.md b/sdk/dotnet/examples/CSharpScript/README.md new file mode 100644 index 000000000..2d90102a3 --- /dev/null +++ b/sdk/dotnet/examples/CSharpScript/README.md @@ -0,0 +1,12 @@ +# How To Run a C# script + +To run it from a console: + +- Install the `dotnet-script` tool: `dotnet tool install -g dotnet-script` +- Build the solution with `PulumiAzure` from the parent folder +- Execute `dotnet script main.csx` + +``` + └─ core.ResourceGroup rg created + └─ storage.Account sa created +``` \ No newline at end of file diff --git a/sdk/dotnet/examples/CSharpScript/main.csx b/sdk/dotnet/examples/CSharpScript/main.csx new file mode 100644 index 000000000..3acc5ea3a --- /dev/null +++ b/sdk/dotnet/examples/CSharpScript/main.csx @@ -0,0 +1,14 @@ +#r "../PulumiAzure/bin/Debug/netstandard2.1/Pulumi.dll" +#r "../PulumiAzure/bin/Debug/netstandard2.1/PulumiAzure.dll" + +using Pulumi.Azure.Core; +using Pulumi.Azure.Storage; + +var resourceGroup = new ResourceGroup("rg"); + +var storageAccount = new Account("sa", new AccountArgs +{ + ResourceGroupName = resourceGroup.Name, + AccountReplicationType = "LRS", + AccountTier = "Standard", +}); \ No newline at end of file diff --git a/sdk/dotnet/examples/FSharpExamples/FSharpExamples.fsproj b/sdk/dotnet/examples/FSharpExamples/FSharpExamples.fsproj new file mode 100644 index 000000000..d94196064 --- /dev/null +++ b/sdk/dotnet/examples/FSharpExamples/FSharpExamples.fsproj @@ -0,0 +1,20 @@ + + + + Exe + netcoreapp3.0 + + + + + + + + + + + + + + + diff --git a/sdk/dotnet/examples/FSharpExamples/Helpers.fs b/sdk/dotnet/examples/FSharpExamples/Helpers.fs new file mode 100644 index 000000000..567f33246 --- /dev/null +++ b/sdk/dotnet/examples/FSharpExamples/Helpers.fs @@ -0,0 +1,50 @@ +namespace Pulumi.FSharp + +open Pulumi +open Pulumi.Azure.Core +open Pulumi.Azure.Storage + +[] +module Builders = + + type ResourceGroupBuilder internal (name) = + member __.Yield(_) = ResourceGroupArgs() + + member __.Run(state: ResourceGroupArgs) : ResourceGroup = ResourceGroup(name, state) + + [] + member __.Location(state: ResourceGroupArgs, location: Input) = + state.Location <- location + state + member this.Location(state: ResourceGroupArgs, location: Output) = this.Location(state, io location) + member this.Location(state: ResourceGroupArgs, location: string) = this.Location(state, input location) + + let resourceGroup name = ResourceGroupBuilder name + + type StorageAccountBuilder internal (name) = + member __.Yield(_) = AccountArgs() + + member __.Run(state: AccountArgs) : Account = Account(name, state) + + [] + member __.ResourceGroupName(state: AccountArgs, value: Input) = + state.ResourceGroupName <- value + state + member this.ResourceGroupName(state: AccountArgs, value: Output) = this.ResourceGroupName(state, io value) + member this.ResourceGroupName(state: AccountArgs, value: string) = this.ResourceGroupName(state, input value) + + [] + member __.AccountReplicationType(state: AccountArgs, value: Input) = + state.AccountReplicationType <- value + state + member this.AccountReplicationType(state: AccountArgs, value: Output) = this.AccountReplicationType(state, io value) + member this.AccountReplicationType(state: AccountArgs, value: string) = this.AccountReplicationType(state, input value) + + [] + member __.AccountTier(state: AccountArgs, value: Input) = + state.AccountTier <- value + state + member this.AccountTier(state: AccountArgs, value: Output) = this.AccountTier(state, io value) + member this.AccountTier(state: AccountArgs, value: string) = this.AccountTier(state, input value) + + let storageAccount name = StorageAccountBuilder name \ No newline at end of file diff --git a/sdk/dotnet/examples/FSharpExamples/Minimal.fs b/sdk/dotnet/examples/FSharpExamples/Minimal.fs new file mode 100644 index 000000000..5467d63f5 --- /dev/null +++ b/sdk/dotnet/examples/FSharpExamples/Minimal.fs @@ -0,0 +1,34 @@ +module Minimal + +open System +open System.Collections.Generic +open Pulumi +open Pulumi.FSharp +open Pulumi.Azure.Core +open Pulumi.Azure.Storage + +let plain (): IDictionary = + let resourceGroup = ResourceGroup("rg", ResourceGroupArgs(Location = input "WestEurope")) + + let storageAccount = + Account("sa", + AccountArgs( + ResourceGroupName = io resourceGroup.Name, // No implicit operators in F# + AccountReplicationType = input "LRS", // Can't have two functions with same name but different signatures + AccountTier = input "Standard")) // There may be some neat operator trick for that? + + dict [ ("accessKey", storageAccount.PrimaryAccessKey :> Object) ] + + +let ce (): IDictionary> = + let resourceGroup = resourceGroup "rg" { + location "WestEurope" + } + + let storageAccount = storageAccount "sa" { + resourceGroupName resourceGroup.Name + accountReplicationType "LRS" + accountTier "Standard" + } + + dict [ ("accessKey", storageAccount.PrimaryAccessKey) ] diff --git a/sdk/dotnet/examples/FSharpExamples/Program.fs b/sdk/dotnet/examples/FSharpExamples/Program.fs new file mode 100644 index 000000000..6424a6b2a --- /dev/null +++ b/sdk/dotnet/examples/FSharpExamples/Program.fs @@ -0,0 +1,9 @@ +// Copyright 2016-2019, Pulumi Corporation + +module Program + +open Pulumi.FSharp + +[] +let main _ = + Deployment.run Minimal.plain diff --git a/sdk/dotnet/examples/FSharpExamples/Pulumi.yaml b/sdk/dotnet/examples/FSharpExamples/Pulumi.yaml new file mode 100644 index 000000000..0b55e052e --- /dev/null +++ b/sdk/dotnet/examples/FSharpExamples/Pulumi.yaml @@ -0,0 +1,2 @@ +name: dotnet-azure-fsharp +runtime: dotnet diff --git a/sdk/dotnet/examples/PulumiAzure/Account.cs b/sdk/dotnet/examples/PulumiAzure/Account.cs new file mode 100644 index 000000000..2b079c090 --- /dev/null +++ b/sdk/dotnet/examples/PulumiAzure/Account.cs @@ -0,0 +1,37 @@ +using Pulumi.Serialization; + +namespace Pulumi.Azure.Storage +{ + public class Account : CustomResource + { + [Output("name")] + public Output Name { get; private set; } + + [Output("primaryAccessKey")] + public Output PrimaryAccessKey { get; private set; } + + [Output("primaryConnectionString")] + public Output PrimaryConnectionString { get; private set; } + + public Account(string name, AccountArgs args = default, ResourceOptions opts = default) + : base("azure:storage/account:Account", name, args, opts) + { + } + } + + public class AccountArgs : ResourceArgs + { + [Input("accessTier")] + public Input AccessTier { get; set; } + [Input("accountKind")] + public Input AccountKind { get; set; } + [Input("accountTier")] + public Input AccountTier { get; set; } + [Input("accountReplicationType")] + public Input AccountReplicationType { get; set; } + [Input("location")] + public Input Location { get; set; } + [Input("resourceGroupName")] + public Input ResourceGroupName { get; set; } + } +} diff --git a/sdk/dotnet/examples/PulumiAzure/AppService.cs b/sdk/dotnet/examples/PulumiAzure/AppService.cs new file mode 100644 index 000000000..123628669 --- /dev/null +++ b/sdk/dotnet/examples/PulumiAzure/AppService.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Pulumi.Serialization; + +namespace Pulumi.Azure.AppService +{ + public class AppService : CustomResource + { + [Output("defaultSiteHostname")] + public Output DefaultSiteHostname { get; private set; } + + public AppService(string name, AppServiceArgs args, ResourceOptions opts = null) + : base("azure:appservice/appService:AppService", name, args, opts) + { + } + } + + public class AppServiceArgs : ResourceArgs + { + [Input("appServicePlanId")] + public Input AppServicePlanId { get; set; } + [Input("location")] + public Input Location { get; set; } + [Input("resourceGroupName")] + public Input ResourceGroupName { get; set; } + + [Input("appSettings")] + private InputMap _appSettings; + public InputMap AppSettings + { + get => _appSettings ?? (_appSettings = new InputMap()); + set => _appSettings = value; + } + + // TODO: why is this disabled? + // [Input("connectionStrings")] + private InputList _connectionStrings; + public InputList ConnectionStrings + { + get => _connectionStrings ?? (_connectionStrings = new InputList()); + set => _connectionStrings = value; + } + } + + public class ConnectionStringArgs : ResourceArgs + { + [Input("name")] + public Input Name { get; set; } + [Input("type")] + public Input Type { get; set; } + [Input("value")] + public Input Value { get; set; } + } +} diff --git a/sdk/dotnet/examples/PulumiAzure/Assets.cs b/sdk/dotnet/examples/PulumiAzure/Assets.cs new file mode 100644 index 000000000..2f0a0f865 --- /dev/null +++ b/sdk/dotnet/examples/PulumiAzure/Assets.cs @@ -0,0 +1,15 @@ +namespace Pulumi.Asset +{ + public interface IArchive + { + // TODO + } + + //public class FileArchive : IArchive + //{ + // public FileArchive(string name) + // { + // // TODO + // } + //} +} diff --git a/sdk/dotnet/examples/PulumiAzure/Container.cs b/sdk/dotnet/examples/PulumiAzure/Container.cs new file mode 100644 index 000000000..873af1364 --- /dev/null +++ b/sdk/dotnet/examples/PulumiAzure/Container.cs @@ -0,0 +1,23 @@ +using Pulumi.Serialization; + +namespace Pulumi.Azure.Storage +{ + public class Container : CustomResource + { + [Output("name")] + public Output Name { get; private set; } + + public Container(string name, ContainerArgs args = default, ResourceOptions opts = default) + : base("azure:storage/container:Container", name, args, opts) + { + } + } + + public class ContainerArgs : ResourceArgs + { + [Input("containerAccessType")] + public Input ContainerAccessType { get; set; } + [Input("storageAccountName")] + public Input StorageAccountName { get; set; } + } +} diff --git a/sdk/dotnet/examples/PulumiAzure/CosmosDB.cs b/sdk/dotnet/examples/PulumiAzure/CosmosDB.cs new file mode 100644 index 000000000..37db49c74 --- /dev/null +++ b/sdk/dotnet/examples/PulumiAzure/CosmosDB.cs @@ -0,0 +1,43 @@ +using Pulumi.Serialization; + +namespace Pulumi.Azure.CosmosDB +{ + public class Account : CustomResource + { + [Output("name")] + public Output Name { get; private set; } + + public Account(string name, AccountArgs args = default, ResourceOptions opts = default) + : base("azure:cosmosdb/account:Account", name, args, opts) + { + } + } + + public class AccountArgs : ResourceArgs + { + [Input("consistencyPolicy")] + public Input ConsistencyPolicy { get; set; } + [Input("geoLocations")] + public InputList GeoLocations { get; set; } + [Input("location")] + public Input Location { get; set; } + [Input("offerType")] + public Input OfferType { get; set; } + [Input("resourceGroupName")] + public Input ResourceGroupName { get; set; } + } + + public class AccountGeoLocation : ResourceArgs + { + [Input("location")] + public Input Location { get; set; } + [Input("failoverPriority")] + public Input FailoverPriority { get; set; } + } + + public class AccountConsistencyPolicy : ResourceArgs + { + [Input("consistencyLevel")] + public Input ConsistencyLevel { get; set; } + } +} diff --git a/sdk/dotnet/examples/PulumiAzure/Database.cs b/sdk/dotnet/examples/PulumiAzure/Database.cs new file mode 100644 index 000000000..73183b2fd --- /dev/null +++ b/sdk/dotnet/examples/PulumiAzure/Database.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Immutable; + +namespace Pulumi.Azure.Sql +{ + public class Database //: CustomResource + { + public Output Name { get; } + + public Database(string name, DatabaseArgs args = default, ResourceOptions opts = default)// : base("sql.Database", name, props(args), opts) + { + this.Name = Output.Create(name + "abc123de"); + Console.WriteLine($" └─ core.Database {name,-11} created"); + } + } + + public class DatabaseArgs + { + public Input ResourceGroupName { get; set; } + public Input ServerName { get; set; } + public Input RequestedServiceObjectiveName { get; set; } + } +} diff --git a/sdk/dotnet/examples/PulumiAzure/GetAccountBlobContainerSAS.cs b/sdk/dotnet/examples/PulumiAzure/GetAccountBlobContainerSAS.cs new file mode 100644 index 000000000..db966f956 --- /dev/null +++ b/sdk/dotnet/examples/PulumiAzure/GetAccountBlobContainerSAS.cs @@ -0,0 +1,57 @@ +using System.Threading.Tasks; +using Pulumi.Serialization; + +namespace Pulumi.Azure.Storage +{ + public class GetAccountBlobContainerSASArgs : ResourceArgs + { + [Input("connectionString")] + public Input ConnectionString { get; set; } + [Input("containerName")] + public Input ContainerName { get; set; } + [Input("start")] + public Input Start { get; set; } + [Input("expiry")] + public Input Expiry { get; set; } + [Input("permissions")] + public Input Permissions { get; set; } + } + + public class GetAccountBlobContainerSASPermissions : ResourceArgs + { + [Input("add")] + public Input Add { get; set; } + [Input("create")] + public Input Create { get; set; } + [Input("delete")] + public Input Delete { get; set; } + [Input("list")] + public Input List { get; set; } + [Input("read")] + public Input Read { get; set; } + [Input("write")] + public Input Write { get; set; } + } + + [OutputType] + public class GetAccountBlobContainerSASResult + { + public readonly string Sas; + + [OutputConstructor] + public GetAccountBlobContainerSASResult(string sas) + { + this.Sas = sas; + } + } + + public static class DataSource + { + public static async Task GetAccountBlobContainerSAS(GetAccountBlobContainerSASArgs args) + { + var result = await Deployment.Instance.InvokeAsync( + "azure:storage/getAccountBlobContainerSAS:getAccountBlobContainerSAS", args).ConfigureAwait(false); + return result.Sas.ToString(); + } + } +} diff --git a/sdk/dotnet/examples/PulumiAzure/Plan.cs b/sdk/dotnet/examples/PulumiAzure/Plan.cs new file mode 100644 index 000000000..4b1b9be7e --- /dev/null +++ b/sdk/dotnet/examples/PulumiAzure/Plan.cs @@ -0,0 +1,35 @@ +using Pulumi.Serialization; + +namespace Pulumi.Azure.AppService +{ + public class Plan : CustomResource + { + [Output("name")] + public Output Name { get; private set; } + + public Plan(string name, PlanArgs args = default, ResourceOptions opts = default) + : base("azure:appservice/plan:Plan", name, args, opts) + { + } + } + + public class PlanArgs : ResourceArgs + { + [Input("kind")] + public Input Kind { get; set; } + [Input("location")] + public Input Location { get; set; } + [Input("resourceGroupName")] + public Input ResourceGroupName { get; set; } + [Input("sku")] + public Input Sku { get; set; } + } + + public class PlanSkuArgs : ResourceArgs + { + [Input("size")] + public Input Size { get; set; } + [Input("tier")] + public Input Tier { get; set; } + } +} diff --git a/sdk/dotnet/examples/PulumiAzure/PulumiAzure.csproj b/sdk/dotnet/examples/PulumiAzure/PulumiAzure.csproj new file mode 100644 index 000000000..02cd72442 --- /dev/null +++ b/sdk/dotnet/examples/PulumiAzure/PulumiAzure.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.0 + + + + + + + diff --git a/sdk/dotnet/examples/PulumiAzure/ResourceGroup.cs b/sdk/dotnet/examples/PulumiAzure/ResourceGroup.cs new file mode 100644 index 000000000..3ef261d8d --- /dev/null +++ b/sdk/dotnet/examples/PulumiAzure/ResourceGroup.cs @@ -0,0 +1,26 @@ +using Pulumi.Serialization; + +namespace Pulumi.Azure.Core +{ + public class ResourceGroup : CustomResource + { + [Output("location")] + public Output Location { get; private set; } + + [Output("name")] + public Output Name { get; private set; } + + public ResourceGroup(string name, ResourceGroupArgs args = default, ResourceOptions opts = default) + : base("azure:core/resourceGroup:ResourceGroup", name, args, opts) + { + } + } + + public class ResourceGroupArgs : ResourceArgs + { + [Input("location")] + public Input Location; + [Input("name")] + public Input Name; + } +} diff --git a/sdk/dotnet/examples/PulumiAzure/SqlServer.cs b/sdk/dotnet/examples/PulumiAzure/SqlServer.cs new file mode 100644 index 000000000..36a3a4531 --- /dev/null +++ b/sdk/dotnet/examples/PulumiAzure/SqlServer.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Immutable; + +namespace Pulumi.Azure.Sql +{ + public class SqlServer //: CustomResource + { + public Output Name { get; } + + public SqlServer(string name, SqlServerArgs args = default, ResourceOptions opts = default)// : base("sql.SqlServer", name, props(args), opts) + { + this.Name = Output.Create(name + "abc123de"); + Console.WriteLine($" └─ core.SqlServer {name,-11} created"); + } + } + + public class SqlServerArgs + { + public Input ResourceGroupName { get; set; } + public Input Location { get; set; } + public Input AdministratorLogin { get; set; } + public Input AdministratorLoginPassword { get; set; } + public Input Version { get; set; } + } +} diff --git a/sdk/dotnet/examples/PulumiAzure/ZipBlob.cs b/sdk/dotnet/examples/PulumiAzure/ZipBlob.cs new file mode 100644 index 000000000..601f8f61e --- /dev/null +++ b/sdk/dotnet/examples/PulumiAzure/ZipBlob.cs @@ -0,0 +1,61 @@ +using Pulumi.Serialization; + +namespace Pulumi.Azure.Storage +{ + public class ZipBlob : CustomResource + { + [Output("name")] + public Output Name { get; private set; } + + [Output("storageContainerName")] + public Output StorageContainerName { get; private set; } + + public ZipBlob(string name, ZipBlobArgs args = default, ResourceOptions opts = default) + : base("azure:storage/zipBlob:ZipBlob", name, args, opts) + { + } + } + + public class ZipBlobArgs : ResourceArgs + { + [Input("content")] + public Input Content { get; set; } + [Input("storageAccountName")] + public Input StorageAccountName { get; set; } + [Input("storageContainerName")] + public Input StorageContainerName { get; set; } + [Input("type")] + public Input Type { get; set; } + } + + public static class SharedAccessSignature + { + public static Output SignedBlobReadUrl(ZipBlob blob, Account account) + { + return Output + .All(account.Name, account.PrimaryConnectionString, blob.StorageContainerName, blob.Name) + .Apply(async values => + { + var sas = await DataSource.GetAccountBlobContainerSAS( + new GetAccountBlobContainerSASArgs + { + ConnectionString = values[1], + ContainerName = values[2], + Start = "2019-01-01", + Expiry = "2100-01-01", + Permissions = new GetAccountBlobContainerSASPermissions + { + Read = true, + Write = false, + Delete = false, + List = false, + Add = false, + Create = false, + }, + } + ); + return $"https://{values[0]}.blob.core.windows.net/{values[2]}/{values[3]}{sas}"; + }); + } + } +} diff --git a/sdk/dotnet/examples/Runner/Program.cs b/sdk/dotnet/examples/Runner/Program.cs new file mode 100644 index 000000000..f6c095e60 --- /dev/null +++ b/sdk/dotnet/examples/Runner/Program.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; + +namespace Pulumi.AzureExamples +{ + class Program + { + class Example + { + public string Name; + public Func>> Run; + } + + static void Main(string[] args) + { + var examples = new Example[] + { + new Example { Name = "Minimal C#", Run = CSharpExamples.Minimal.Run }, + new Example { Name = "Web App C#", Run = CSharpExamples.WebApp.Run }, + new Example { Name = "Minimal F#", Run = FSharpExamples.Minimal.run }, + new Example { Name = "Minimal F# based on C/E", Run = FSharpExamples.MinimalCE.run }, + new Example { Name = "Minimal VB.NET", Run = VBExamples.Minimal.Run }, + }; + + foreach (var example in examples) + { + Console.WriteLine($"Updating ({example.Name}):\n"); + Console.WriteLine(" Type Name Status"); + var exports = example.Run(); + Console.WriteLine("\nOutputs:"); + foreach (var v in exports) + { + Console.WriteLine($" + {v.Key}: {v.Value}"); + } + Console.WriteLine("\n\n"); + } + } + } +} diff --git a/sdk/dotnet/examples/Runner/Runner.csproj b/sdk/dotnet/examples/Runner/Runner.csproj new file mode 100644 index 000000000..966c5d241 --- /dev/null +++ b/sdk/dotnet/examples/Runner/Runner.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp3.0 + + + + + + + + + + + diff --git a/sdk/dotnet/examples/VBExamples/Minimal.vb b/sdk/dotnet/examples/VBExamples/Minimal.vb new file mode 100644 index 000000000..e1303c03e --- /dev/null +++ b/sdk/dotnet/examples/VBExamples/Minimal.vb @@ -0,0 +1,19 @@ +Imports Pulumi +Imports Pulumi.Azure.Core +Imports Storage = Pulumi.Azure.Storage + +Public Class Minimal + Public Shared Function Run() As IDictionary(Of String, Object) + Dim resourceGroup = New ResourceGroup("rg", New ResourceGroupArgs With { + .Location = "West Europe" + }) + Dim storageAccount = New Storage.Account("sa", New Storage.AccountArgs With { + .ResourceGroupName = resourceGroup.Name, + .AccountReplicationType = "LRS", + .AccountTier = "Standard" + }) + Return New Dictionary(Of String, Object) From { + {"accessKey", storageAccount.PrimaryAccessKey} + } + End Function +End Class diff --git a/sdk/dotnet/examples/VBExamples/Program.vb b/sdk/dotnet/examples/VBExamples/Program.vb new file mode 100644 index 000000000..e9143c7cd --- /dev/null +++ b/sdk/dotnet/examples/VBExamples/Program.vb @@ -0,0 +1,9 @@ +Imports Pulumi + +Module Program + + Sub Main() + Deployment.RunAsync(AddressOf Minimal.Run).Wait() + End Sub + +End Module \ No newline at end of file diff --git a/sdk/dotnet/examples/VBExamples/Pulumi.yaml b/sdk/dotnet/examples/VBExamples/Pulumi.yaml new file mode 100644 index 000000000..589d0ec94 --- /dev/null +++ b/sdk/dotnet/examples/VBExamples/Pulumi.yaml @@ -0,0 +1,2 @@ +name: dotnet-azure-vb +runtime: dotnet diff --git a/sdk/dotnet/examples/VBExamples/VBExamples.vbproj b/sdk/dotnet/examples/VBExamples/VBExamples.vbproj new file mode 100644 index 000000000..6d9b23436 --- /dev/null +++ b/sdk/dotnet/examples/VBExamples/VBExamples.vbproj @@ -0,0 +1,14 @@ + + + + Exe + netcoreapp3.0 + VBExamples + + + + + + + + diff --git a/sdk/dotnet/examples/bucket/Bucket.cs b/sdk/dotnet/examples/bucket/Bucket.cs new file mode 100644 index 000000000..31eafeafb --- /dev/null +++ b/sdk/dotnet/examples/bucket/Bucket.cs @@ -0,0 +1,958 @@ +// *** WARNING: this file was generated by the Pulumi Terraform Bridge (tfgen) Tool. *** +// *** Do not edit by hand unless you're certain you know what you are doing! *** + +#nullable enable + +using System.Collections.Immutable; +using System.Threading.Tasks; +using Pulumi.Serialization; + +namespace Pulumi.Aws.S3 +{ + public class Bucket : CustomResource + { + [Output("accelerationStatus")] + public Output AccelerationStatus { get; private set; } = null!; + + [Output("acl")] + public Output Acl { get; private set; } = null!; + + [Output("arn")] + public Output Arn { get; private set; } = null!; + + [Output("bucket")] + public Output BucketName { get; private set; } = null!; + + [Output("bucketDomainName")] + public Output BucketDomainName { get; private set; } = null!; + + [Output("bucketPrefix")] + public Output BucketPrefix { get; private set; } = null!; + + [Output("bucketRegionalDomainName")] + public Output BucketRegionalDomainName { get; private set; } = null!; + + [Output("corsRules")] + public Output?> CorsRules { get; private set; } = null!; + + [Output("forceDestroy")] + public Output ForceDestroy { get; private set; } = null!; + + [Output("hostedZoneId")] + public Output HostedZoneId { get; private set; } = null!; + + [Output("lifecycleRules")] + public Output?> LifecycleRules { get; private set; } = null!; + + [Output("loggings")] + public Output?> Loggings { get; private set; } = null!; + + [Output("objectLockConfiguration")] + public Output ObjectLockConfiguration { get; private set; } = null!; + + [Output("policy")] + public Output Policy { get; private set; } = null!; + + [Output("region")] + public Output Region { get; private set; } = null!; + + [Output("replicationConfiguration")] + public Output ReplicationConfiguration { get; private set; } = null!; + + [Output("requestPayer")] + public Output RequestPayer { get; private set; } = null!; + + [Output("serverSideEncryptionConfiguration")] + public Output ServerSideEncryptionConfiguration { get; private set; } = null!; + + [Output("tags")] + public Output?> Tags { get; private set; } = null!; + + [Output("versioning")] + public Output Versioning { get; private set; } = null!; + + [Output("website")] + public Output Website { get; private set; } = null!; + + [Output("websiteDomain")] + public Output WebsiteDomain { get; private set; } = null!; + + [Output("websiteEndpoint")] + public Output WebsiteEndpoint { get; private set; } = null!; + + + public Bucket(string name, BucketArgs? args = null, CustomResourceOptions? options = null) + : base("aws:s3/bucket:Bucket", name, args, options) + { + } + } + + public class BucketArgs : ResourceArgs + { + [Input("accelerationStatus")] + public Input? AccelerationStatus { get; set; } + + [Input("acl")] + public Input? Acl { get; set; } + + [Input("arn")] + public Input? Arn { get; set; } + + [Input("bucket")] + public Input? BucketName { get; set; } + + [Input("bucketPrefix")] + public Input? BucketPrefix { get; set; } + + [Input("corsRules")] + private InputList? _corsRules; + public InputList? CorsRules + { + get => _corsRules ?? (_corsRules = new InputList()); + set => _corsRules = value; + } + + [Input("forceDestroy")] + public Input? ForceDestroy { get; set; } + + [Input("hostedZoneId")] + public Input? HostedZoneId { get; set; } + + [Input("lifecycleRules")] + private InputList? _lifecycleRules; + public InputList? LifecycleRules + { + get => _lifecycleRules ?? (_lifecycleRules = new InputList()); + set => _lifecycleRules = value; + } + + [Input("loggings")] + private InputList? _loggings; + public InputList? Loggings + { + get => _loggings ?? (_loggings = new InputList()); + set => _loggings = value; + } + + [Input("objectLockConfiguration")] + public Input? ObjectLockConfiguration { get; set; } + + [Input("policy")] + public Input? Policy { get; set; } + + [Input("region")] + public Input? Region { get; set; } + + [Input("replicationConfiguration")] + public Input? ReplicationConfiguration { get; set; } + + [Input("requestPayer")] + public Input? RequestPayer { get; set; } + + [Input("serverSideEncryptionConfiguration")] + public Input? ServerSideEncryptionConfiguration { get; set; } + + [Input("tags")] + private InputMap? _tags; + public InputMap? Tags + { + get => _tags ?? (_tags = new InputMap()); + set => _tags = value; + } + + [Input("versioning")] + public Input? Versioning { get; set; } + + [Input("website")] + public Input? Website { get; set; } + + [Input("websiteDomain")] + public Input? WebsiteDomain { get; set; } + + [Input("websiteEndpoint")] + public Input? WebsiteEndpoint { get; set; } + + public BucketArgs() + { + } + } + + public class BucketCorsRulesArgs + { + [Input("allowedHeaders")] + private InputList? _allowedHeaders; + public InputList? AllowedHeaders + { + get => _allowedHeaders ?? (_allowedHeaders = new InputList()); + set => _allowedHeaders = value; + } + + [Input("allowedMethods", required: true)] + private InputList? _allowedMethods; + public InputList AllowedMethods + { + get => _allowedMethods ?? (_allowedMethods = new InputList()); + set => _allowedMethods = value; + } + + [Input("allowedOrigins", required: true)] + private InputList? _allowedOrigins; + public InputList AllowedOrigins + { + get => _allowedOrigins ?? (_allowedOrigins = new InputList()); + set => _allowedOrigins = value; + } + + [Input("exposeHeaders")] + private InputList? _exposeHeaders; + public InputList? ExposeHeaders + { + get => _exposeHeaders ?? (_exposeHeaders = new InputList()); + set => _exposeHeaders = value; + } + + [Input("maxAgeSeconds")] + public Input? MaxAgeSeconds { get; set; } + + public BucketCorsRulesArgs() + { + } + } + + public class BucketLifecycleRulesArgs + { + [Input("abortIncompleteMultipartUploadDays")] + public Input? AbortIncompleteMultipartUploadDays { get; set; } + + [Input("enabled", required: true)] + public Input Enabled { get; set; } = null!; + + [Input("expiration")] + public Input? Expiration { get; set; } + + [Input("id")] + public Input? Id { get; set; } + + [Input("noncurrentVersionExpiration")] + public Input? NoncurrentVersionExpiration { get; set; } + + [Input("noncurrentVersionTransitions")] + private InputList? _noncurrentVersionTransitions; + public InputList? NoncurrentVersionTransitions + { + get => _noncurrentVersionTransitions ?? (_noncurrentVersionTransitions = new InputList()); + set => _noncurrentVersionTransitions = value; + } + + [Input("prefix")] + public Input? Prefix { get; set; } + + [Input("tags")] + private InputMap? _tags; + public InputMap? Tags + { + get => _tags ?? (_tags = new InputMap()); + set => _tags = value; + } + + [Input("transitions")] + private InputList? _transitions; + public InputList? Transitions + { + get => _transitions ?? (_transitions = new InputList()); + set => _transitions = value; + } + + public BucketLifecycleRulesArgs() + { + } + } + + public class BucketLifecycleRulesExpirationArgs + { + [Input("date")] + public Input? Date { get; set; } + + [Input("days")] + public Input? Days { get; set; } + + [Input("expiredObjectDeleteMarker")] + public Input? ExpiredObjectDeleteMarker { get; set; } + + public BucketLifecycleRulesExpirationArgs() + { + } + } + + public class BucketLifecycleRulesNoncurrentVersionExpirationArgs + { + [Input("days")] + public Input? Days { get; set; } + + public BucketLifecycleRulesNoncurrentVersionExpirationArgs() + { + } + } + + public class BucketLifecycleRulesNoncurrentVersionTransitionsArgs + { + [Input("days")] + public Input? Days { get; set; } + + [Input("storageClass", required: true)] + public Input StorageClass { get; set; } = null!; + + public BucketLifecycleRulesNoncurrentVersionTransitionsArgs() + { + } + } + + public class BucketLifecycleRulesTransitionsArgs + { + [Input("date")] + public Input? Date { get; set; } + + [Input("days")] + public Input? Days { get; set; } + + [Input("storageClass", required: true)] + public Input StorageClass { get; set; } = null!; + + public BucketLifecycleRulesTransitionsArgs() + { + } + } + + public class BucketLoggingsArgs + { + [Input("targetBucket", required: true)] + public Input TargetBucket { get; set; } = null!; + + [Input("targetPrefix")] + public Input? TargetPrefix { get; set; } + + public BucketLoggingsArgs() + { + } + } + + public class BucketObjectLockConfigurationArgs + { + [Input("objectLockEnabled", required: true)] + public Input ObjectLockEnabled { get; set; } = null!; + + [Input("rule")] + public Input? Rule { get; set; } + + public BucketObjectLockConfigurationArgs() + { + } + } + + public class BucketObjectLockConfigurationRuleArgs + { + [Input("defaultRetention", required: true)] + public Input DefaultRetention { get; set; } = null!; + + public BucketObjectLockConfigurationRuleArgs() + { + } + } + + public class BucketObjectLockConfigurationRuleDefaultRetentionArgs + { + [Input("days")] + public Input? Days { get; set; } + + [Input("mode", required: true)] + public Input Mode { get; set; } = null!; + + [Input("years")] + public Input? Years { get; set; } + + public BucketObjectLockConfigurationRuleDefaultRetentionArgs() + { + } + } + + public class BucketReplicationConfigurationArgs + { + [Input("role", required: true)] + public Input Role { get; set; } = null!; + + [Input("rules", required: true)] + private InputList? _rules; + public InputList Rules + { + get => _rules ?? (_rules = new InputList()); + set => _rules = value; + } + + public BucketReplicationConfigurationArgs() + { + } + } + + public class BucketReplicationConfigurationRulesArgs + { + [Input("destination", required: true)] + public Input Destination { get; set; } = null!; + + [Input("filter")] + public Input? Filter { get; set; } + + [Input("id")] + public Input? Id { get; set; } + + [Input("prefix")] + public Input? Prefix { get; set; } + + [Input("priority")] + public Input? Priority { get; set; } + + [Input("sourceSelectionCriteria")] + public Input? SourceSelectionCriteria { get; set; } + + [Input("status", required: true)] + public Input Status { get; set; } = null!; + + public BucketReplicationConfigurationRulesArgs() + { + } + } + + public class BucketReplicationConfigurationRulesDestinationAccessControlTranslationArgs + { + [Input("owner", required: true)] + public Input Owner { get; set; } = null!; + + public BucketReplicationConfigurationRulesDestinationAccessControlTranslationArgs() + { + } + } + + public class BucketReplicationConfigurationRulesDestinationArgs + { + [Input("accessControlTranslation")] + public Input? AccessControlTranslation { get; set; } + + [Input("accountId")] + public Input? AccountId { get; set; } + + [Input("bucket", required: true)] + public Input Bucket { get; set; } = null!; + + [Input("replicaKmsKeyId")] + public Input? ReplicaKmsKeyId { get; set; } + + [Input("storageClass")] + public Input? StorageClass { get; set; } + + public BucketReplicationConfigurationRulesDestinationArgs() + { + } + } + + public class BucketReplicationConfigurationRulesFilterArgs + { + [Input("prefix")] + public Input? Prefix { get; set; } + + [Input("tags")] + private InputMap? _tags; + public InputMap? Tags + { + get => _tags ?? (_tags = new InputMap()); + set => _tags = value; + } + + public BucketReplicationConfigurationRulesFilterArgs() + { + } + } + + public class BucketReplicationConfigurationRulesSourceSelectionCriteriaArgs + { + [Input("sseKmsEncryptedObjects")] + public Input? SseKmsEncryptedObjects { get; set; } + + public BucketReplicationConfigurationRulesSourceSelectionCriteriaArgs() + { + } + } + + public class BucketReplicationConfigurationRulesSourceSelectionCriteriaSseKmsEncryptedObjectsArgs + { + [Input("enabled", required: true)] + public Input Enabled { get; set; } = null!; + + public BucketReplicationConfigurationRulesSourceSelectionCriteriaSseKmsEncryptedObjectsArgs() + { + } + } + + public class BucketServerSideEncryptionConfigurationArgs + { + [Input("rule", required: true)] + public Input Rule { get; set; } = null!; + + public BucketServerSideEncryptionConfigurationArgs() + { + } + } + + public class BucketServerSideEncryptionConfigurationRuleApplyServerSideEncryptionByDefaultArgs + { + [Input("kmsMasterKeyId")] + public Input? KmsMasterKeyId { get; set; } + + [Input("sseAlgorithm", required: true)] + public Input SseAlgorithm { get; set; } = null!; + + public BucketServerSideEncryptionConfigurationRuleApplyServerSideEncryptionByDefaultArgs() + { + } + } + + public class BucketServerSideEncryptionConfigurationRuleArgs + { + [Input("applyServerSideEncryptionByDefault", required: true)] + public Input ApplyServerSideEncryptionByDefault { get; set; } = null!; + + public BucketServerSideEncryptionConfigurationRuleArgs() + { + } + } + + public class BucketVersioningArgs + { + [Input("enabled")] + public Input? Enabled { get; set; } + + [Input("mfaDelete")] + public Input? MfaDelete { get; set; } + + public BucketVersioningArgs() + { + } + } + + public class BucketWebsiteArgs + { + [Input("errorDocument")] + public Input? ErrorDocument { get; set; } + + [Input("indexDocument")] + public Input? IndexDocument { get; set; } + + [Input("redirectAllRequestsTo")] + public Input? RedirectAllRequestsTo { get; set; } + + [Input("routingRules")] + public Input? RoutingRules { get; set; } + + public BucketWebsiteArgs() + { + } + } + + [OutputType] + public class BucketCorsRules + { + public readonly ImmutableArray? AllowedHeaders; + public readonly ImmutableArray AllowedMethods; + public readonly ImmutableArray AllowedOrigins; + public readonly ImmutableArray? ExposeHeaders; + public readonly int? MaxAgeSeconds; + + [OutputConstructor] + private BucketCorsRules( + ImmutableArray? allowedHeaders, + ImmutableArray allowedMethods, + ImmutableArray allowedOrigins, + ImmutableArray? exposeHeaders, + int? maxAgeSeconds) + { + AllowedHeaders = allowedHeaders; + AllowedMethods = allowedMethods; + AllowedOrigins = allowedOrigins; + ExposeHeaders = exposeHeaders; + MaxAgeSeconds = maxAgeSeconds; + } + } + + [OutputType] + public class BucketLifecycleRules + { + public readonly int? AbortIncompleteMultipartUploadDays; + public readonly bool Enabled; + public readonly BucketLifecycleRulesExpiration? Expiration; + public readonly string Id; + public readonly BucketLifecycleRulesNoncurrentVersionExpiration? NoncurrentVersionExpiration; + public readonly ImmutableArray? NoncurrentVersionTransitions; + public readonly string? Prefix; + public readonly ImmutableDictionary? Tags; + public readonly ImmutableArray? Transitions; + + [OutputConstructor] + private BucketLifecycleRules( + int? abortIncompleteMultipartUploadDays, + bool enabled, + BucketLifecycleRulesExpiration? expiration, + string id, + BucketLifecycleRulesNoncurrentVersionExpiration? noncurrentVersionExpiration, + ImmutableArray? noncurrentVersionTransitions, + string? prefix, + ImmutableDictionary? tags, + ImmutableArray? transitions) + { + AbortIncompleteMultipartUploadDays = abortIncompleteMultipartUploadDays; + Enabled = enabled; + Expiration = expiration; + Id = id; + NoncurrentVersionExpiration = noncurrentVersionExpiration; + NoncurrentVersionTransitions = noncurrentVersionTransitions; + Prefix = prefix; + Tags = tags; + Transitions = transitions; + } + } + + [OutputType] + public class BucketLifecycleRulesExpiration + { + public readonly string? Date; + public readonly int? Days; + public readonly bool? ExpiredObjectDeleteMarker; + + [OutputConstructor] + private BucketLifecycleRulesExpiration( + string? date, + int? days, + bool? expiredObjectDeleteMarker) + { + Date = date; + Days = days; + ExpiredObjectDeleteMarker = expiredObjectDeleteMarker; + } + } + + [OutputType] + public class BucketLifecycleRulesNoncurrentVersionExpiration + { + public readonly int? Days; + + [OutputConstructor] + private BucketLifecycleRulesNoncurrentVersionExpiration( + int? days) + { + Days = days; + } + } + + [OutputType] + public class BucketLifecycleRulesNoncurrentVersionTransitions + { + public readonly int? Days; + public readonly string StorageClass; + + [OutputConstructor] + private BucketLifecycleRulesNoncurrentVersionTransitions( + int? days, + string storageClass) + { + Days = days; + StorageClass = storageClass; + } + } + + [OutputType] + public class BucketLifecycleRulesTransitions + { + public readonly string? Date; + public readonly int? Days; + public readonly string StorageClass; + + [OutputConstructor] + private BucketLifecycleRulesTransitions( + string? date, + int? days, + string storageClass) + { + Date = date; + Days = days; + StorageClass = storageClass; + } + } + + [OutputType] + public class BucketLoggings + { + public readonly string TargetBucket; + public readonly string? TargetPrefix; + + [OutputConstructor] + private BucketLoggings( + string targetBucket, + string? targetPrefix) + { + TargetBucket = targetBucket; + TargetPrefix = targetPrefix; + } + } + + [OutputType] + public class BucketObjectLockConfiguration + { + public readonly string ObjectLockEnabled; + public readonly BucketObjectLockConfigurationRule? Rule; + + [OutputConstructor] + private BucketObjectLockConfiguration( + string objectLockEnabled, + BucketObjectLockConfigurationRule? rule) + { + ObjectLockEnabled = objectLockEnabled; + Rule = rule; + } + } + + [OutputType] + public class BucketObjectLockConfigurationRule + { + public readonly BucketObjectLockConfigurationRuleDefaultRetention DefaultRetention; + + [OutputConstructor] + private BucketObjectLockConfigurationRule( + BucketObjectLockConfigurationRuleDefaultRetention defaultRetention) + { + DefaultRetention = defaultRetention; + } + } + + [OutputType] + public class BucketObjectLockConfigurationRuleDefaultRetention + { + public readonly int? Days; + public readonly string Mode; + public readonly int? Years; + + [OutputConstructor] + private BucketObjectLockConfigurationRuleDefaultRetention( + int? days, + string mode, + int? years) + { + Days = days; + Mode = mode; + Years = years; + } + } + + [OutputType] + public class BucketReplicationConfiguration + { + public readonly string Role; + public readonly ImmutableArray Rules; + + [OutputConstructor] + private BucketReplicationConfiguration( + string role, + ImmutableArray rules) + { + Role = role; + Rules = rules; + } + } + + [OutputType] + public class BucketReplicationConfigurationRules + { + public readonly BucketReplicationConfigurationRulesDestination Destination; + public readonly BucketReplicationConfigurationRulesFilter? Filter; + public readonly string? Id; + public readonly string? Prefix; + public readonly int? Priority; + public readonly BucketReplicationConfigurationRulesSourceSelectionCriteria? SourceSelectionCriteria; + public readonly string Status; + + [OutputConstructor] + private BucketReplicationConfigurationRules( + BucketReplicationConfigurationRulesDestination destination, + BucketReplicationConfigurationRulesFilter? filter, + string? id, + string? prefix, + int? priority, + BucketReplicationConfigurationRulesSourceSelectionCriteria? sourceSelectionCriteria, + string status) + { + Destination = destination; + Filter = filter; + Id = id; + Prefix = prefix; + Priority = priority; + SourceSelectionCriteria = sourceSelectionCriteria; + Status = status; + } + } + + [OutputType] + public class BucketReplicationConfigurationRulesDestination + { + public readonly BucketReplicationConfigurationRulesDestinationAccessControlTranslation? AccessControlTranslation; + public readonly string? AccountId; + public readonly string Bucket; + public readonly string? ReplicaKmsKeyId; + public readonly string? StorageClass; + + [OutputConstructor] + private BucketReplicationConfigurationRulesDestination( + BucketReplicationConfigurationRulesDestinationAccessControlTranslation? accessControlTranslation, + string? accountId, + string bucket, + string? replicaKmsKeyId, + string? storageClass) + { + AccessControlTranslation = accessControlTranslation; + AccountId = accountId; + Bucket = bucket; + ReplicaKmsKeyId = replicaKmsKeyId; + StorageClass = storageClass; + } + } + + [OutputType] + public class BucketReplicationConfigurationRulesDestinationAccessControlTranslation + { + public readonly string Owner; + + [OutputConstructor] + private BucketReplicationConfigurationRulesDestinationAccessControlTranslation( + string owner) + { + Owner = owner; + } + } + + [OutputType] + public class BucketReplicationConfigurationRulesFilter + { + public readonly string? Prefix; + public readonly ImmutableDictionary? Tags; + + [OutputConstructor] + private BucketReplicationConfigurationRulesFilter( + string? prefix, + ImmutableDictionary? tags) + { + Prefix = prefix; + Tags = tags; + } + } + + [OutputType] + public class BucketReplicationConfigurationRulesSourceSelectionCriteria + { + public readonly BucketReplicationConfigurationRulesSourceSelectionCriteriaSseKmsEncryptedObjects? SseKmsEncryptedObjects; + + [OutputConstructor] + private BucketReplicationConfigurationRulesSourceSelectionCriteria( + BucketReplicationConfigurationRulesSourceSelectionCriteriaSseKmsEncryptedObjects? sseKmsEncryptedObjects) + { + SseKmsEncryptedObjects = sseKmsEncryptedObjects; + } + } + + [OutputType] + public class BucketReplicationConfigurationRulesSourceSelectionCriteriaSseKmsEncryptedObjects + { + public readonly bool Enabled; + + [OutputConstructor] + private BucketReplicationConfigurationRulesSourceSelectionCriteriaSseKmsEncryptedObjects( + bool enabled) + { + Enabled = enabled; + } + } + + [OutputType] + public class BucketServerSideEncryptionConfiguration + { + public readonly BucketServerSideEncryptionConfigurationRule Rule; + + [OutputConstructor] + private BucketServerSideEncryptionConfiguration( + BucketServerSideEncryptionConfigurationRule rule) + { + Rule = rule; + } + } + + [OutputType] + public class BucketServerSideEncryptionConfigurationRule + { + public readonly BucketServerSideEncryptionConfigurationRuleApplyServerSideEncryptionByDefault ApplyServerSideEncryptionByDefault; + + [OutputConstructor] + private BucketServerSideEncryptionConfigurationRule( + BucketServerSideEncryptionConfigurationRuleApplyServerSideEncryptionByDefault applyServerSideEncryptionByDefault) + { + ApplyServerSideEncryptionByDefault = applyServerSideEncryptionByDefault; + } + } + + [OutputType] + public class BucketServerSideEncryptionConfigurationRuleApplyServerSideEncryptionByDefault + { + public readonly string? KmsMasterKeyId; + public readonly string SseAlgorithm; + + [OutputConstructor] + private BucketServerSideEncryptionConfigurationRuleApplyServerSideEncryptionByDefault( + string? kmsMasterKeyId, + string sseAlgorithm) + { + KmsMasterKeyId = kmsMasterKeyId; + SseAlgorithm = sseAlgorithm; + } + } + + [OutputType] + public class BucketVersioning + { + public readonly bool? Enabled; + public readonly bool? MfaDelete; + + [OutputConstructor] + private BucketVersioning( + bool? enabled, + bool? mfaDelete) + { + Enabled = enabled; + MfaDelete = mfaDelete; + } + } + + [OutputType] + public class BucketWebsite + { + public readonly string? ErrorDocument; + public readonly string? IndexDocument; + public readonly string? RedirectAllRequestsTo; + public readonly string? RoutingRules; + + [OutputConstructor] + private BucketWebsite( + string? errorDocument, + string? indexDocument, + string? redirectAllRequestsTo, + string? routingRules) + { + ErrorDocument = errorDocument; + IndexDocument = indexDocument; + RedirectAllRequestsTo = redirectAllRequestsTo; + RoutingRules = routingRules; + } + } +} diff --git a/sdk/dotnet/examples/bucket/BucketObject.cs b/sdk/dotnet/examples/bucket/BucketObject.cs new file mode 100644 index 000000000..edbfa4c28 --- /dev/null +++ b/sdk/dotnet/examples/bucket/BucketObject.cs @@ -0,0 +1,172 @@ +// *** WARNING: this file was generated by the Pulumi Terraform Bridge (tfgen) Tool. *** +// *** Do not edit by hand unless you're certain you know what you are doing! *** + +#nullable enable + +using System.Collections.Immutable; +using System.Threading.Tasks; +using Pulumi.Serialization; + +namespace Pulumi.Aws.S3 +{ + public class BucketObject : CustomResource + { + [Output("acl")] + public Output Acl { get; private set; } = null!; + + [Output("bucket")] + public Output Bucket { get; private set; } = null!; + + [Output("cacheControl")] + public Output CacheControl { get; private set; } = null!; + + [Output("content")] + public Output Content { get; private set; } = null!; + + [Output("contentBase64")] + public Output ContentBase64 { get; private set; } = null!; + + [Output("contentDisposition")] + public Output ContentDisposition { get; private set; } = null!; + + [Output("contentEncoding")] + public Output ContentEncoding { get; private set; } = null!; + + [Output("contentLanguage")] + public Output ContentLanguage { get; private set; } = null!; + + [Output("contentType")] + public Output ContentType { get; private set; } = null!; + + [Output("etag")] + public Output Etag { get; private set; } = null!; + + [Output("forceDestroy")] + public Output ForceDestroy { get; private set; } = null!; + + [Output("key")] + public Output Key { get; private set; } = null!; + + [Output("kmsKeyId")] + public Output KmsKeyId { get; private set; } = null!; + + [Output("metadata")] + public Output?> Metadata { get; private set; } = null!; + + [Output("objectLockLegalHoldStatus")] + public Output ObjectLockLegalHoldStatus { get; private set; } = null!; + + [Output("objectLockMode")] + public Output ObjectLockMode { get; private set; } = null!; + + [Output("objectLockRetainUntilDate")] + public Output ObjectLockRetainUntilDate { get; private set; } = null!; + + [Output("serverSideEncryption")] + public Output ServerSideEncryption { get; private set; } = null!; + + [Output("source")] + public Output Source { get; private set; } = null!; + + [Output("storageClass")] + public Output StorageClass { get; private set; } = null!; + + [Output("tags")] + public Output?> Tags { get; private set; } = null!; + + [Output("versionId")] + public Output VersionId { get; private set; } = null!; + + [Output("websiteRedirect")] + public Output WebsiteRedirect { get; private set; } = null!; + + + public BucketObject(string name, BucketObjectArgs args, CustomResourceOptions? options = null) + : base("aws:s3/bucketObject:BucketObject", name, args, options) + { + } + } + + public class BucketObjectArgs : ResourceArgs + { + [Input("acl")] + public Input? Acl { get; set; } + + [Input("bucket", required: true)] + public Input Bucket { get; set; } = null!; + + [Input("cacheControl")] + public Input? CacheControl { get; set; } + + [Input("content")] + public Input? Content { get; set; } + + [Input("contentBase64")] + public Input? ContentBase64 { get; set; } + + [Input("contentDisposition")] + public Input? ContentDisposition { get; set; } + + [Input("contentEncoding")] + public Input? ContentEncoding { get; set; } + + [Input("contentLanguage")] + public Input? ContentLanguage { get; set; } + + [Input("contentType")] + public Input? ContentType { get; set; } + + [Input("etag")] + public Input? Etag { get; set; } + + [Input("forceDestroy")] + public Input? ForceDestroy { get; set; } + + [Input("key")] + public Input? Key { get; set; } + + [Input("kmsKeyId")] + public Input? KmsKeyId { get; set; } + + [Input("metadata")] + private InputMap? _metadata; + public InputMap? Metadata + { + get => _metadata ?? (_metadata = new InputMap()); + set => _metadata = value; + } + + [Input("objectLockLegalHoldStatus")] + public Input? ObjectLockLegalHoldStatus { get; set; } + + [Input("objectLockMode")] + public Input? ObjectLockMode { get; set; } + + [Input("objectLockRetainUntilDate")] + public Input? ObjectLockRetainUntilDate { get; set; } + + [Input("serverSideEncryption")] + public Input? ServerSideEncryption { get; set; } + + [Input("source")] + public Input? Source { get; set; } + + [Input("storageClass")] + public Input? StorageClass { get; set; } + + [Input("tags")] + private InputMap? _tags; + public InputMap? Tags + { + get => _tags ?? (_tags = new InputMap()); + set => _tags = value; + } + + [Input("websiteRedirect")] + public Input? WebsiteRedirect { get; set; } + + public BucketObjectArgs() + { + } + } +} diff --git a/sdk/dotnet/examples/bucket/GetBucket.cs b/sdk/dotnet/examples/bucket/GetBucket.cs new file mode 100644 index 000000000..b0aca2283 --- /dev/null +++ b/sdk/dotnet/examples/bucket/GetBucket.cs @@ -0,0 +1,67 @@ +// *** WARNING: this file was generated by the Pulumi Terraform Bridge (tfgen) Tool. *** +// *** Do not edit by hand unless you're certain you know what you are doing! *** +// + +#nullable enable + +using System.Collections.Immutable; +using System.Threading.Tasks; +using Pulumi.Serialization; + +namespace Pulumi.Aws.S3 +{ + public static partial class Invokes + { + public static Task GetBucket(GetBucketArgs args, InvokeOptions? options = null) + { + return Deployment.Instance.InvokeAsync("aws:s3/getBucket:getBucket", args, options); + } + } + + public class GetBucketArgs : ResourceArgs + { + [Input("bucket", required: true)] + public Input Bucket { get; set; } = null!; + + public GetBucketArgs() + { + } + } + + [OutputType] + public class GetBucketResult + { + public readonly string Arn; + public readonly string Bucket; + public readonly string BucketDomainName; + public readonly string BucketRegionalDomainName; + public readonly string HostedZoneId; + public readonly string Region; + public readonly string WebsiteDomain; + public readonly string WebsiteEndpoint; + public readonly string Id; + + [OutputConstructor] + private GetBucketResult( + string arn, + string bucket, + string bucketDomainName, + string bucketRegionalDomainName, + string hostedZoneId, + string region, + string websiteDomain, + string websiteEndpoint, + string id) + { + Arn = arn; + Bucket = bucket; + BucketDomainName = bucketDomainName; + BucketRegionalDomainName = bucketRegionalDomainName; + HostedZoneId = hostedZoneId; + Region = region; + WebsiteDomain = websiteDomain; + WebsiteEndpoint = websiteEndpoint; + Id = id; + } + } +} diff --git a/sdk/dotnet/examples/bucket/Program.cs b/sdk/dotnet/examples/bucket/Program.cs new file mode 100644 index 000000000..0708a3a59 --- /dev/null +++ b/sdk/dotnet/examples/bucket/Program.cs @@ -0,0 +1,41 @@ +// Copyright 2016-2019, Pulumi Corporation + +#nullable enable + +using System.Collections.Generic; +using System.Threading.Tasks; +using Pulumi; +using Pulumi.Aws.S3; + +class Program +{ + static Task Main() + => Deployment.RunAsync(() => + { + var config = new Config("hello-dotnet"); + var name = config.Require("name"); + + // Create the bucket, and make it public. + var bucket = new Bucket(name, new BucketArgs { Acl = "public-read" }); + + // Add some content. + var content = new BucketObject($"{name}-content", new BucketObjectArgs + { + Acl = "public-read", + Bucket = bucket.Id, + ContentType = "text/plain; charset=utf8", + Key = "hello.txt", + Source = new StringAsset("Made with ❤, Pulumi, and .NET"), + + }); + + // Return some values that will become the Outputs of the stack. + return new Dictionary + { + { "hello", "world" }, + { "bucket-id", bucket.Id }, + { "content-id", content.Id }, + { "object-url", Output.Format($"http://{bucket.BucketDomainName}/{content.Key}") }, + }; + }); +} diff --git a/sdk/dotnet/examples/bucket/Pulumi.cydotnet.yaml b/sdk/dotnet/examples/bucket/Pulumi.cydotnet.yaml new file mode 100644 index 000000000..009a9d02d --- /dev/null +++ b/sdk/dotnet/examples/bucket/Pulumi.cydotnet.yaml @@ -0,0 +1,3 @@ +config: + aws:region: us-east-2 + hello-dotnet:name: cyrus diff --git a/sdk/dotnet/examples/bucket/Pulumi.dev.yaml b/sdk/dotnet/examples/bucket/Pulumi.dev.yaml new file mode 100644 index 000000000..45e8eb013 --- /dev/null +++ b/sdk/dotnet/examples/bucket/Pulumi.dev.yaml @@ -0,0 +1,3 @@ +config: + aws:region: eu-central-1 + hello-dotnet:name: helloworld diff --git a/sdk/dotnet/examples/bucket/Pulumi.hello-dotnet.yaml b/sdk/dotnet/examples/bucket/Pulumi.hello-dotnet.yaml new file mode 100644 index 000000000..d4cc28fe5 --- /dev/null +++ b/sdk/dotnet/examples/bucket/Pulumi.hello-dotnet.yaml @@ -0,0 +1,3 @@ +config: + aws:region: us-west-2 + hello-dotnet:name: hello-dotnet diff --git a/sdk/dotnet/examples/bucket/Pulumi.yaml b/sdk/dotnet/examples/bucket/Pulumi.yaml new file mode 100644 index 000000000..662a9ce5f --- /dev/null +++ b/sdk/dotnet/examples/bucket/Pulumi.yaml @@ -0,0 +1,2 @@ +name: hello-dotnet +runtime: dotnet diff --git a/sdk/dotnet/examples/bucket/bucket.csproj b/sdk/dotnet/examples/bucket/bucket.csproj new file mode 100644 index 000000000..47552889b --- /dev/null +++ b/sdk/dotnet/examples/bucket/bucket.csproj @@ -0,0 +1,20 @@ + + + + Exe + netcoreapp3.0 + + + + 1701;1702 + + + + + + + + + + + diff --git a/sdk/dotnet/pulumi_logo_64x64.png b/sdk/dotnet/pulumi_logo_64x64.png new file mode 100644 index 000000000..708b71724 Binary files /dev/null and b/sdk/dotnet/pulumi_logo_64x64.png differ diff --git a/sdk/nodejs/runtime/rpc.ts b/sdk/nodejs/runtime/rpc.ts index 161ca42fa..abbb0e6c8 100644 --- a/sdk/nodejs/runtime/rpc.ts +++ b/sdk/nodejs/runtime/rpc.ts @@ -404,7 +404,7 @@ export function deserializeProperty(prop: any): any { return prop; } else if (prop instanceof Array) { - // We can just deserialize all the elements of the underyling array and return it. + // We can just deserialize all the elements of the underlying array and return it. // However, we want to push secretness up to the top level (since we can't set sub-properties to secret) // values since they are not typed as Output. let hadSecret = false; diff --git a/tests/integration/config_basic/dotnet/.gitignore b/tests/integration/config_basic/dotnet/.gitignore new file mode 100644 index 000000000..fd544f76a --- /dev/null +++ b/tests/integration/config_basic/dotnet/.gitignore @@ -0,0 +1,5 @@ +/.pulumi/ +[Bb]in/ +[Oo]bj/ + + diff --git a/tests/integration/config_basic/dotnet/ConfigBasic.csproj b/tests/integration/config_basic/dotnet/ConfigBasic.csproj new file mode 100644 index 000000000..5635c9dce --- /dev/null +++ b/tests/integration/config_basic/dotnet/ConfigBasic.csproj @@ -0,0 +1,8 @@ + + + + Exe + netcoreapp3.0 + + + diff --git a/tests/integration/config_basic/dotnet/Program.cs b/tests/integration/config_basic/dotnet/Program.cs new file mode 100644 index 000000000..cf15c4da3 --- /dev/null +++ b/tests/integration/config_basic/dotnet/Program.cs @@ -0,0 +1,30 @@ +// Copyright 2016-2019, Pulumi Corporation. All rights reserved. + +using System; +using System.Threading.Tasks; +using Pulumi; + +class Program +{ + static Task Main(string[] args) + { + return Deployment.RunAsync(() => + { + var config = new Config("config_basic_dotnet"); + + // This value is plaintext and doesn't require encryption. + var value = config.Require("aConfigValue"); + if (value != "this value is a value") + { + throw new Exception($"aConfigValue not the expected value; got {value}"); + } + + // This value is a secret + var secret = config.Require("bEncryptedSecret"); + if (secret != "this super secret is encrypted") + { + throw new Exception($"bEncryptedSecret not the expected value; got {secret}"); + } + }); + } +} \ No newline at end of file diff --git a/tests/integration/config_basic/dotnet/Pulumi.yaml b/tests/integration/config_basic/dotnet/Pulumi.yaml new file mode 100644 index 000000000..0d73eb999 --- /dev/null +++ b/tests/integration/config_basic/dotnet/Pulumi.yaml @@ -0,0 +1,3 @@ +name: config_basic_dotnet +description: A simple .NET program that uses configuration. +runtime: dotnet diff --git a/tests/integration/empty/dotnet/.gitignore b/tests/integration/empty/dotnet/.gitignore new file mode 100644 index 000000000..fd544f76a --- /dev/null +++ b/tests/integration/empty/dotnet/.gitignore @@ -0,0 +1,5 @@ +/.pulumi/ +[Bb]in/ +[Oo]bj/ + + diff --git a/tests/integration/empty/dotnet/Empty.csproj b/tests/integration/empty/dotnet/Empty.csproj new file mode 100644 index 000000000..5635c9dce --- /dev/null +++ b/tests/integration/empty/dotnet/Empty.csproj @@ -0,0 +1,8 @@ + + + + Exe + netcoreapp3.0 + + + diff --git a/tests/integration/empty/dotnet/Program.cs b/tests/integration/empty/dotnet/Program.cs new file mode 100644 index 000000000..6da9bc891 --- /dev/null +++ b/tests/integration/empty/dotnet/Program.cs @@ -0,0 +1,12 @@ +// Copyright 2016-2019, Pulumi Corporation. All rights reserved. + +using System.Threading.Tasks; +using Pulumi; + +class Program +{ + static Task Main(string[] args) + { + return Deployment.RunAsync(() => {}); + } +} \ No newline at end of file diff --git a/tests/integration/empty/dotnet/Pulumi.yaml b/tests/integration/empty/dotnet/Pulumi.yaml new file mode 100644 index 000000000..ff6f3e52a --- /dev/null +++ b/tests/integration/empty/dotnet/Pulumi.yaml @@ -0,0 +1,3 @@ +name: emptydotnet +description: An empty .NET Pulumi program. +runtime: dotnet diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 09e87387b..d03e811c4 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -84,6 +84,14 @@ func TestEmptyGo(t *testing.T) { }) } +// TestEmptyDotNet simply tests that we can run an empty .NET project. +func TestEmptyDotNet(t *testing.T) { + integration.ProgramTest(t, &integration.ProgramTestOptions{ + Dir: filepath.Join("empty", "dotnet"), + Quick: true, + }) +} + // Tests emitting many engine events doesn't result in a performance problem. func TestEngineEventPerf(t *testing.T) { // Prior to pulumi/pulumi#2303, a preview or update would take ~40s. @@ -306,7 +314,27 @@ func TestStackOutputsPython(t *testing.T) { } }, }) +} +func TestStackOutputsDotNet(t *testing.T) { + integration.ProgramTest(t, &integration.ProgramTestOptions{ + Dir: filepath.Join("stack_outputs", "dotnet"), + Quick: true, + ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) { + // Ensure the checkpoint contains a single resource, the Stack, with two outputs. + fmt.Printf("Deployment: %v", stackInfo.Deployment) + assert.NotNil(t, stackInfo.Deployment) + if assert.Equal(t, 1, len(stackInfo.Deployment.Resources)) { + stackRes := stackInfo.Deployment.Resources[0] + assert.NotNil(t, stackRes) + assert.Equal(t, resource.RootStackType, stackRes.URN.Type()) + assert.Equal(t, 0, len(stackRes.Inputs)) + assert.Equal(t, 2, len(stackRes.Outputs)) + assert.Equal(t, "ABC", stackRes.Outputs["xyz"]) + assert.Equal(t, float64(42), stackRes.Outputs["foo"]) + } + }, + }) } // TestStackOutputsJSON ensures the CLI properly formats stack outputs as JSON when requested. @@ -598,6 +626,20 @@ func TestConfigBasicGo(t *testing.T) { }) } +// Tests basic configuration from the perspective of a Pulumi .NET program. +func TestConfigBasicDotNet(t *testing.T) { + integration.ProgramTest(t, &integration.ProgramTestOptions{ + Dir: filepath.Join("config_basic", "dotnet"), + Quick: true, + Config: map[string]string{ + "aConfigValue": "this value is a value", + }, + Secrets: map[string]string{ + "bEncryptedSecret": "this super secret is encrypted", + }, + }) +} + // Tests an explicit provider instance. func TestExplicitProvider(t *testing.T) { integration.ProgramTest(t, &integration.ProgramTestOptions{ @@ -706,6 +748,21 @@ func TestStackReferencePython(t *testing.T) { integration.ProgramTest(t, opts) } +func TestStackReferenceDotNet(t *testing.T) { + if owner := os.Getenv("PULUMI_TEST_OWNER"); owner == "" { + t.Skipf("Skipping: PULUMI_TEST_OWNER is not set") + } + + opts := &integration.ProgramTestOptions{ + Dir: filepath.Join("stack_reference", "dotnet"), + Quick: true, + Config: map[string]string{ + "org": os.Getenv("PULUMI_TEST_OWNER"), + }, + } + integration.ProgramTest(t, opts) +} + // Tests that we issue an error if we fail to locate the Python command when running // a Python example. func TestPython3NotInstalled(t *testing.T) { diff --git a/tests/integration/stack_outputs/dotnet/.gitignore b/tests/integration/stack_outputs/dotnet/.gitignore new file mode 100644 index 000000000..fd544f76a --- /dev/null +++ b/tests/integration/stack_outputs/dotnet/.gitignore @@ -0,0 +1,5 @@ +/.pulumi/ +[Bb]in/ +[Oo]bj/ + + diff --git a/tests/integration/stack_outputs/dotnet/Program.cs b/tests/integration/stack_outputs/dotnet/Program.cs new file mode 100644 index 000000000..c280387db --- /dev/null +++ b/tests/integration/stack_outputs/dotnet/Program.cs @@ -0,0 +1,20 @@ +// Copyright 2016-2019, Pulumi Corporation. All rights reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Pulumi; + +class Program +{ + static Task Main(string[] args) + { + return Deployment.RunAsync(() => + { + return new Dictionary + { + { "xyz", "ABC" }, + { "foo", 42 }, + }; + }); + } +} \ No newline at end of file diff --git a/tests/integration/stack_outputs/dotnet/Pulumi.yaml b/tests/integration/stack_outputs/dotnet/Pulumi.yaml new file mode 100644 index 000000000..ffa8bb0db --- /dev/null +++ b/tests/integration/stack_outputs/dotnet/Pulumi.yaml @@ -0,0 +1,3 @@ +name: stack_outputs +description: A program that exports some outputs from .NET. +runtime: dotnet diff --git a/tests/integration/stack_outputs/dotnet/StackOutputs.csproj b/tests/integration/stack_outputs/dotnet/StackOutputs.csproj new file mode 100644 index 000000000..5635c9dce --- /dev/null +++ b/tests/integration/stack_outputs/dotnet/StackOutputs.csproj @@ -0,0 +1,8 @@ + + + + Exe + netcoreapp3.0 + + + diff --git a/tests/integration/stack_reference/dotnet/.gitignore b/tests/integration/stack_reference/dotnet/.gitignore new file mode 100644 index 000000000..fd544f76a --- /dev/null +++ b/tests/integration/stack_reference/dotnet/.gitignore @@ -0,0 +1,5 @@ +/.pulumi/ +[Bb]in/ +[Oo]bj/ + + diff --git a/tests/integration/stack_reference/dotnet/Program.cs b/tests/integration/stack_reference/dotnet/Program.cs new file mode 100644 index 000000000..ea7f239fd --- /dev/null +++ b/tests/integration/stack_reference/dotnet/Program.cs @@ -0,0 +1,18 @@ +// Copyright 2016-2019, Pulumi Corporation. All rights reserved. + +using System.Threading.Tasks; +using Pulumi; + +class Program +{ + static Task Main(string[] args) + { + return Deployment.RunAsync(() => + { + var config = new Config(); + var org = config.Require("org"); + var slug = $"{org}/{Deployment.Instance.ProjectName}/{Deployment.Instance.StackName}"; + //TODO var a = new StackReference(slug); + }); + } +} \ No newline at end of file diff --git a/tests/integration/stack_reference/dotnet/Pulumi.yaml b/tests/integration/stack_reference/dotnet/Pulumi.yaml new file mode 100644 index 000000000..f3ec82348 --- /dev/null +++ b/tests/integration/stack_reference/dotnet/Pulumi.yaml @@ -0,0 +1,3 @@ +name: stack_reference_dotnet +description: A simple .NET program that has a stack reference. +runtime: dotnet diff --git a/tests/integration/stack_reference/dotnet/StackReference.csproj b/tests/integration/stack_reference/dotnet/StackReference.csproj new file mode 100644 index 000000000..5635c9dce --- /dev/null +++ b/tests/integration/stack_reference/dotnet/StackReference.csproj @@ -0,0 +1,8 @@ + + + + Exe + netcoreapp3.0 + + +