diff --git a/CHANGELOG.md b/CHANGELOG.md index 123e7aa99..b5637d9dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ CHANGELOG ========= ## HEAD (Unreleased) -_(none)_ + + [sdk/dotnet] C# Automation API. + [#5761](https://github.com/pulumi/pulumi/pull/5761) ## 2.21.0 (2021-02-17) diff --git a/sdk/dotnet/.gitignore b/sdk/dotnet/.gitignore index b7c7f5d1d..1b4ddf07f 100644 --- a/sdk/dotnet/.gitignore +++ b/sdk/dotnet/.gitignore @@ -1,4 +1,5 @@ [Bb]in/ [Oo]bj/ .leu -Pulumi/Pulumi.xml \ No newline at end of file +Pulumi/Pulumi.xml +*.user \ No newline at end of file diff --git a/sdk/dotnet/Pulumi.Automation.Tests/AssemblyAttributes.cs b/sdk/dotnet/Pulumi.Automation.Tests/AssemblyAttributes.cs new file mode 100644 index 000000000..8e21ee46e --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/AssemblyAttributes.cs @@ -0,0 +1,6 @@ +// Copyright 2016-2021, Pulumi Corporation + +using Xunit; + +// Unfortunately, we depend on static state. So for now disable parallelization. +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/sdk/dotnet/Pulumi.Automation.Tests/CommandExceptionTests.cs b/sdk/dotnet/Pulumi.Automation.Tests/CommandExceptionTests.cs new file mode 100644 index 000000000..ee0a9a7b9 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/CommandExceptionTests.cs @@ -0,0 +1,124 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Pulumi.Automation.Commands.Exceptions; +using Xunit; + +namespace Pulumi.Automation.Tests +{ + public class CommandExceptionTests + { + private static string GetTestSuffix() + { + var random = new Random(); + var result = 100000 + random.Next(0, 900000); + return result.ToString(); + } + + [Fact] + public async Task StackNotFoundExceptionIsThrown() + { + var projectSettings = new ProjectSettings("command_exception_test", ProjectRuntimeName.NodeJS); + using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions + { + ProjectSettings = projectSettings, + }); + + var stackName = $"non_existent_stack{GetTestSuffix()}"; + var selectTask = workspace.SelectStackAsync(stackName); + + await Assert.ThrowsAsync( + () => selectTask); + } + + [Fact] + public async Task StackAlreadyExistsExceptionIsThrown() + { + var projectSettings = new ProjectSettings("command_exception_test", ProjectRuntimeName.NodeJS); + using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions + { + ProjectSettings = projectSettings, + }); + + var stackName = $"already_existing_stack{GetTestSuffix()}"; + await workspace.CreateStackAsync(stackName); + + try + { + var createTask = workspace.CreateStackAsync(stackName); + await Assert.ThrowsAsync( + () => createTask); + } + finally + { + await workspace.RemoveStackAsync(stackName); + } + } + + [Fact] + public async Task ConcurrentUpdateExceptionIsThrown() + { + + var projectSettings = new ProjectSettings("command_exception_test", ProjectRuntimeName.NodeJS); + using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions + { + ProjectSettings = projectSettings, + }); + + var stackName = $"concurrent_update_stack{GetTestSuffix()}"; + await workspace.CreateStackAsync(stackName); + + try + { + var stack = await WorkspaceStack.SelectAsync(stackName, workspace); + + var hitSemaphore = false; + using var semaphore = new SemaphoreSlim(0, 1); + var program = PulumiFn.Create(() => + { + hitSemaphore = true; + semaphore.Wait(); + return new Dictionary() + { + ["test"] = "doesnt matter", + }; + }); + + var upTask = stack.UpAsync(new UpOptions + { + Program = program, + }); + + // wait until we hit semaphore + while (!hitSemaphore) + { + await Task.Delay(TimeSpan.FromSeconds(2)); + if (upTask.IsFaulted) + throw upTask.Exception!; + else if (upTask.IsCompleted) + throw new Exception("never hit semaphore in first UP task"); + } + + // before releasing the semaphore, ensure another up throws + var concurrentTask = stack.UpAsync(new UpOptions + { + Program = program, // should never make it into this + }); + + await Assert.ThrowsAsync( + () => concurrentTask); + + // finish first up call + semaphore.Release(); + await upTask; + } + finally + { + await workspace.RemoveStackAsync(stackName); + } + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Data/json/Pulumi.dev.json b/sdk/dotnet/Pulumi.Automation.Tests/Data/json/Pulumi.dev.json new file mode 100644 index 000000000..0dffa4c33 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Data/json/Pulumi.dev.json @@ -0,0 +1,9 @@ +{ + "secretsProvider": "abc", + "config": { + "plain": "plain", + "secure": { + "secure": "secret" + } + } +} \ No newline at end of file diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Data/json/Pulumi.json b/sdk/dotnet/Pulumi.Automation.Tests/Data/json/Pulumi.json new file mode 100644 index 000000000..fc6f51048 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Data/json/Pulumi.json @@ -0,0 +1,5 @@ +{ + "name": "testproj", + "runtime": "go", + "description": "A minimal Go Pulumi program" +} \ No newline at end of file diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Data/testproj/Pulumi.yaml b/sdk/dotnet/Pulumi.Automation.Tests/Data/testproj/Pulumi.yaml new file mode 100644 index 000000000..009e9c7cf --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Data/testproj/Pulumi.yaml @@ -0,0 +1,3 @@ +name: testproj +runtime: go +description: A minimal Go Pulumi program \ No newline at end of file diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Data/testproj/go.mod b/sdk/dotnet/Pulumi.Automation.Tests/Data/testproj/go.mod new file mode 100644 index 000000000..045a46d4d --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Data/testproj/go.mod @@ -0,0 +1,5 @@ +module testproj + +go 1.14 + +require github.com/pulumi/pulumi/sdk/v2 v2.0.0 \ No newline at end of file diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Data/testproj/go.sum b/sdk/dotnet/Pulumi.Automation.Tests/Data/testproj/go.sum new file mode 100644 index 000000000..0529c1ad5 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Data/testproj/go.sum @@ -0,0 +1,261 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cheggaaa/pb v1.0.18 h1:G/DgkKaBP0V5lnBg/vx61nVxxAU+VqU5yMzSc0f2PPE= +github.com/cheggaaa/pb v1.0.18/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/djherbis/times v1.2.0 h1:xANXjsC/iBqbO00vkWlYwPWgBgEVU6m6AFYg0Pic+Mc= +github.com/djherbis/times v1.2.0/go.mod h1:CGMZlo255K5r4Yw0b9RRfFQpM2y7uOmxg4jm9HsaVf8= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/flock v0.7.1 h1:DP+LD/t0njgoPBvT5MJLeliUIVQR03hiKR6vezdwHlc= +github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0= +github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/pulumi/pulumi/sdk/v2 v2.0.0/go.mod h1:W7k1UDYerc5o97mHnlHHp5iQZKEby+oQrQefWt+2RF4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= +github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/texttheater/golang-levenshtein v0.0.0-20191208221605-eb6844b05fc6 h1:9VTskZOIRf2vKF3UL8TuWElry5pgUpV1tFSe/e/0m/E= +github.com/texttheater/golang-levenshtein v0.0.0-20191208221605-eb6844b05fc6/go.mod h1:XDKHRm5ThF8YJjx001LtgelzsoaEcvnA7lVWz9EeX3g= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/uber/jaeger-client-go v2.22.1+incompatible h1:NHcubEkVbahf9t3p75TOCR83gdUHXjRJvjoBh1yACsM= +github.com/uber/jaeger-client-go v2.22.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.2.0+incompatible h1:MxZXOiR2JuoANZ3J6DE/U0kSFv/eJ/GfSYVCjK7dyaw= +github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6 h1:TjszyFsQsyZNHwdVdZ5m7bjmreu0znc2kRYsEml9/Ww= +golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d h1:62ap6LNOjDU6uGmKXHJbSfciMoV+FeI1sRXx/pLDL44= +golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= +gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 h1:ucqkfpjg9WzSUubAO62csmucvxl4/JeW3F4I4909XkM= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= \ No newline at end of file diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Data/testproj/main.go b/sdk/dotnet/Pulumi.Automation.Tests/Data/testproj/main.go new file mode 100644 index 000000000..cf22aaa79 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Data/testproj/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "github.com/pulumi/pulumi/sdk/v2/go/pulumi" + "github.com/pulumi/pulumi/sdk/v2/go/pulumi/config" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + c := config.New(ctx, "") + ctx.Export("exp_static", pulumi.String("foo")) + ctx.Export("exp_cfg", pulumi.String(c.Get("bar"))) + ctx.Export("exp_secret", c.GetSecret("buzz")) + return nil + }) +} diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Data/yaml/Pulumi.dev.yaml b/sdk/dotnet/Pulumi.Automation.Tests/Data/yaml/Pulumi.dev.yaml new file mode 100644 index 000000000..355e2ce47 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Data/yaml/Pulumi.dev.yaml @@ -0,0 +1,5 @@ +secretsProvider: abc +config: + plain: plain + secure: + secure: secret \ No newline at end of file diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Data/yaml/Pulumi.yaml b/sdk/dotnet/Pulumi.Automation.Tests/Data/yaml/Pulumi.yaml new file mode 100644 index 000000000..009e9c7cf --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Data/yaml/Pulumi.yaml @@ -0,0 +1,3 @@ +name: testproj +runtime: go +description: A minimal Go Pulumi program \ No newline at end of file diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Data/yml/Pulumi.dev.yml b/sdk/dotnet/Pulumi.Automation.Tests/Data/yml/Pulumi.dev.yml new file mode 100644 index 000000000..355e2ce47 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Data/yml/Pulumi.dev.yml @@ -0,0 +1,5 @@ +secretsProvider: abc +config: + plain: plain + secure: + secure: secret \ No newline at end of file diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Data/yml/Pulumi.yml b/sdk/dotnet/Pulumi.Automation.Tests/Data/yml/Pulumi.yml new file mode 100644 index 000000000..009e9c7cf --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Data/yml/Pulumi.yml @@ -0,0 +1,3 @@ +name: testproj +runtime: go +description: A minimal Go Pulumi program \ No newline at end of file diff --git a/sdk/dotnet/Pulumi.Automation.Tests/LocalWorkspaceTests.cs b/sdk/dotnet/Pulumi.Automation.Tests/LocalWorkspaceTests.cs new file mode 100644 index 000000000..8cdf7ddad --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/LocalWorkspaceTests.cs @@ -0,0 +1,646 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Pulumi.Automation.Commands.Exceptions; +using Xunit; + +namespace Pulumi.Automation.Tests +{ + public class LocalWorkspaceTests + { + private static readonly string _dataDirectory = + Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName, "Data"); + + private static string GetTestSuffix() + { + var random = new Random(); + var result = 100000 + random.Next(0, 900000); + return result.ToString(); + } + + private static string NormalizeConfigKey(string key, string projectName) + { + var parts = key.Split(":"); + if (parts.Length < 2) + return $"{projectName}:{key}"; + + return string.Empty; + } + + [Theory] + [InlineData("yaml")] + [InlineData("yml")] + [InlineData("json")] + public async Task GetProjectSettings(string extension) + { + var workingDir = Path.Combine(_dataDirectory, extension); + using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions + { + WorkDir = workingDir, + }); + + var settings = await workspace.GetProjectSettingsAsync(); + Assert.NotNull(settings); + Assert.Equal("testproj", settings!.Name); + Assert.Equal(ProjectRuntimeName.Go, settings.Runtime.Name); + Assert.Equal("A minimal Go Pulumi program", settings.Description); + } + + [Theory] + [InlineData("yaml")] + [InlineData("yml")] + [InlineData("json")] + public async Task GetStackSettings(string extension) + { + var workingDir = Path.Combine(_dataDirectory, extension); + using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions + { + WorkDir = workingDir, + }); + + var settings = await workspace.GetStackSettingsAsync("dev"); + Assert.NotNull(settings); + Assert.Equal("abc", settings!.SecretsProvider); + Assert.NotNull(settings.Config); + + Assert.True(settings.Config!.TryGetValue("plain", out var plainValue)); + Assert.Equal("plain", plainValue!.Value); + Assert.False(plainValue.IsSecure); + + Assert.True(settings.Config.TryGetValue("secure", out var secureValue)); + Assert.Equal("secret", secureValue!.Value); + Assert.True(secureValue.IsSecure); + } + + [Fact] + public async Task AddRemoveListPlugins() + { + using var workspace = await LocalWorkspace.CreateAsync(); + + var plugins = await workspace.ListPluginsAsync(); + if (plugins.Any(p => p.Name == "aws" && p.Version == "3.0.0")) + { + await workspace.RemovePluginAsync("aws", "3.0.0"); + plugins = await workspace.ListPluginsAsync(); + Assert.DoesNotContain(plugins, p => p.Name == "aws" && p.Version == "3.0.0"); + } + + await workspace.InstallPluginAsync("aws", "v3.0.0"); + plugins = await workspace.ListPluginsAsync(); + var aws = plugins.FirstOrDefault(p => p.Name == "aws" && p.Version == "3.0.0"); + Assert.NotNull(aws); + + await workspace.RemovePluginAsync("aws", "3.0.0"); + plugins = await workspace.ListPluginsAsync(); + Assert.DoesNotContain(plugins, p => p.Name == "aws" && p.Version == "3.0.0"); + } + + [Fact] + public async Task CreateSelectRemoveStack() + { + var projectSettings = new ProjectSettings("node_test", ProjectRuntimeName.NodeJS); + using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions + { + ProjectSettings = projectSettings, + EnvironmentVariables = new Dictionary() + { + ["PULUMI_CONFIG_PASSPHRASE"] = "test", + } + }); + + var stackName = $"int_test{GetTestSuffix()}"; + + var stacks = await workspace.ListStacksAsync(); + Assert.Empty(stacks); + + await workspace.CreateStackAsync(stackName); + stacks = await workspace.ListStacksAsync(); + var newStack = stacks.FirstOrDefault(s => s.Name == stackName); + Assert.NotNull(newStack); + Assert.True(newStack.IsCurrent); + + await workspace.SelectStackAsync(stackName); + await workspace.RemoveStackAsync(stackName); + stacks = await workspace.ListStacksAsync(); + Assert.Empty(stacks); + } + + [Fact] + public async Task ManipulateConfig() + { + var projectName = "node_test"; + var projectSettings = new ProjectSettings(projectName, ProjectRuntimeName.NodeJS); + + using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions + { + ProjectSettings = projectSettings, + EnvironmentVariables = new Dictionary() + { + ["PULUMI_CONFIG_PASSPHRASE"] = "test", + } + }); + + var stackName = $"int_test{GetTestSuffix()}"; + var stack = await WorkspaceStack.CreateAsync(stackName, workspace); + + var config = new Dictionary() + { + ["plain"] = new ConfigValue("abc"), + ["secret"] = new ConfigValue("def", isSecret: true), + }; + + var plainKey = NormalizeConfigKey("plain", projectName); + var secretKey = NormalizeConfigKey("secret", projectName); + + await Assert.ThrowsAsync( + () => stack.GetConfigValueAsync(plainKey)); + + var values = await stack.GetConfigAsync(); + Assert.Empty(values); + + await stack.SetConfigAsync(config); + values = await stack.GetConfigAsync(); + Assert.True(values.TryGetValue(plainKey, out var plainValue)); + Assert.Equal("abc", plainValue!.Value); + Assert.False(plainValue.IsSecret); + Assert.True(values.TryGetValue(secretKey, out var secretValue)); + Assert.Equal("def", secretValue!.Value); + Assert.True(secretValue.IsSecret); + + await stack.RemoveConfigValueAsync("plain"); + values = await stack.GetConfigAsync(); + Assert.Single(values); + + await stack.SetConfigValueAsync("foo", new ConfigValue("bar")); + values = await stack.GetConfigAsync(); + Assert.Equal(2, values.Count); + + await workspace.RemoveStackAsync(stackName); + } + + [Fact] + public async Task ListStackAndCurrentlySelected() + { + var projectSettings = new ProjectSettings( + $"node_list_test{GetTestSuffix()}", + ProjectRuntimeName.NodeJS); + + using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions + { + ProjectSettings = projectSettings, + EnvironmentVariables = new Dictionary() + { + ["PULUMI_CONFIG_PASSPHRASE"] = "test", + } + }); + + var stackNames = new List(); + try + { + for (var i = 0; i < 2; i++) + { + var stackName = GetStackName(); + await WorkspaceStack.CreateAsync(stackName, workspace); + stackNames.Add(stackName); + var summary = await workspace.GetStackAsync(); + Assert.NotNull(summary); + Assert.True(summary!.IsCurrent); + var stacks = await workspace.ListStacksAsync(); + Assert.Equal(i + 1, stacks.Count); + } + } + finally + { + foreach (var name in stackNames) + await workspace.RemoveStackAsync(name); + } + + static string GetStackName() + => $"int_test{GetTestSuffix()}"; + } + + [Fact] + public async Task CheckStackStatus() + { + var projectSettings = new ProjectSettings("node_test", ProjectRuntimeName.NodeJS); + using var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions + { + ProjectSettings = projectSettings, + EnvironmentVariables = new Dictionary() + { + ["PULUMI_CONFIG_PASSPHRASE"] = "test", + } + }); + + var stackName = $"int_test{GetTestSuffix()}"; + var stack = await WorkspaceStack.CreateAsync(stackName, workspace); + try + { + var history = await stack.GetHistoryAsync(); + Assert.Empty(history); + var info = await stack.GetInfoAsync(); + Assert.Null(info); + } + finally + { + await workspace.RemoveStackAsync(stackName); + } + } + + [Fact] + public async Task StackLifecycleLocalProgram() + { + var stackName = $"int_test{GetTestSuffix()}"; + var workingDir = Path.Combine(_dataDirectory, "testproj"); + using var stack = await LocalWorkspace.CreateStackAsync(new LocalProgramArgs(stackName, workingDir) + { + EnvironmentVariables = new Dictionary() + { + ["PULUMI_CONFIG_PASSPHRASE"] = "test", + } + }); + + var config = new Dictionary() + { + ["bar"] = new ConfigValue("abc"), + ["buzz"] = new ConfigValue("secret", isSecret: true), + }; + await stack.SetConfigAsync(config); + + // pulumi up + var upResult = await stack.UpAsync(); + Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); + Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); + Assert.Equal(3, upResult.Outputs.Count); + + // exp_static + Assert.True(upResult.Outputs.TryGetValue("exp_static", out var expStaticValue)); + Assert.Equal("foo", expStaticValue!.Value); + Assert.False(expStaticValue.IsSecret); + + // exp_cfg + Assert.True(upResult.Outputs.TryGetValue("exp_cfg", out var expConfigValue)); + Assert.Equal("abc", expConfigValue!.Value); + Assert.False(expConfigValue.IsSecret); + + // exp_secret + Assert.True(upResult.Outputs.TryGetValue("exp_secret", out var expSecretValue)); + Assert.Equal("secret", expSecretValue!.Value); + Assert.True(expSecretValue.IsSecret); + + // pulumi preview + await stack.PreviewAsync(); + // TODO: update assertions when we have structured output + + // pulumi refresh + var refreshResult = await stack.RefreshAsync(); + Assert.Equal(UpdateKind.Refresh, refreshResult.Summary.Kind); + Assert.Equal(UpdateState.Succeeded, refreshResult.Summary.Result); + + // pulumi destroy + var destroyResult = await stack.DestroyAsync(); + Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind); + Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result); + + await stack.Workspace.RemoveStackAsync(stackName); + } + + [Fact] + public async Task StackLifecycleInlineProgram() + { + var program = PulumiFn.Create(() => + { + var config = new Pulumi.Config(); + return new Dictionary + { + ["exp_static"] = "foo", + ["exp_cfg"] = config.Get("bar"), + ["exp_secret"] = config.GetSecret("buzz"), + }; + }); + + var stackName = $"int_test{GetTestSuffix()}"; + var projectName = "inline_node"; + using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program) + { + EnvironmentVariables = new Dictionary() + { + ["PULUMI_CONFIG_PASSPHRASE"] = "test", + } + }); + + var config = new Dictionary() + { + ["bar"] = new ConfigValue("abc"), + ["buzz"] = new ConfigValue("secret", isSecret: true), + }; + await stack.SetConfigAsync(config); + + // pulumi up + var upResult = await stack.UpAsync(); + Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); + Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); + Assert.Equal(3, upResult.Outputs.Count); + + // exp_static + Assert.True(upResult.Outputs.TryGetValue("exp_static", out var expStaticValue)); + Assert.Equal("foo", expStaticValue!.Value); + Assert.False(expStaticValue.IsSecret); + + // exp_cfg + Assert.True(upResult.Outputs.TryGetValue("exp_cfg", out var expConfigValue)); + Assert.Equal("abc", expConfigValue!.Value); + Assert.False(expConfigValue.IsSecret); + + // exp_secret + Assert.True(upResult.Outputs.TryGetValue("exp_secret", out var expSecretValue)); + Assert.Equal("secret", expSecretValue!.Value); + Assert.True(expSecretValue.IsSecret); + + // pulumi preview + await stack.PreviewAsync(); + // TODO: update assertions when we have structured output + + // pulumi refresh + var refreshResult = await stack.RefreshAsync(); + Assert.Equal(UpdateKind.Refresh, refreshResult.Summary.Kind); + Assert.Equal(UpdateState.Succeeded, refreshResult.Summary.Result); + + // pulumi destroy + var destroyResult = await stack.DestroyAsync(); + Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind); + Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result); + + await stack.Workspace.RemoveStackAsync(stackName); + } + + private class ValidStack : Stack + { + [Output("exp_static")] + public Output ExpStatic { get; set; } + + [Output("exp_cfg")] + public Output ExpConfig { get; set; } + + [Output("exp_secret")] + public Output ExpSecret { get; set; } + + public ValidStack() + { + var config = new Pulumi.Config(); + this.ExpStatic = Output.Create("foo"); + this.ExpConfig = Output.Create(config.Get("bar")!); + this.ExpSecret = config.GetSecret("buzz")!; + } + } + + [Fact] + public async Task StackLifecycleInlineProgramWithTStack() + { + var program = PulumiFn.Create(); + + var stackName = $"int_test{GetTestSuffix()}"; + var projectName = "inline_tstack_node"; + using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program) + { + EnvironmentVariables = new Dictionary() + { + ["PULUMI_CONFIG_PASSPHRASE"] = "test", + } + }); + + var config = new Dictionary() + { + ["bar"] = new ConfigValue("abc"), + ["buzz"] = new ConfigValue("secret", isSecret: true), + }; + await stack.SetConfigAsync(config); + + // pulumi up + var upResult = await stack.UpAsync(); + Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); + Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); + Assert.Equal(3, upResult.Outputs.Count); + + // exp_static + Assert.True(upResult.Outputs.TryGetValue("exp_static", out var expStaticValue)); + Assert.Equal("foo", expStaticValue!.Value); + Assert.False(expStaticValue.IsSecret); + + // exp_cfg + Assert.True(upResult.Outputs.TryGetValue("exp_cfg", out var expConfigValue)); + Assert.Equal("abc", expConfigValue!.Value); + Assert.False(expConfigValue.IsSecret); + + // exp_secret + Assert.True(upResult.Outputs.TryGetValue("exp_secret", out var expSecretValue)); + Assert.Equal("secret", expSecretValue!.Value); + Assert.True(expSecretValue.IsSecret); + + // pulumi preview + await stack.PreviewAsync(); + // TODO: update assertions when we have structured output + + // pulumi refresh + var refreshResult = await stack.RefreshAsync(); + Assert.Equal(UpdateKind.Refresh, refreshResult.Summary.Kind); + Assert.Equal(UpdateState.Succeeded, refreshResult.Summary.Result); + + // pulumi destroy + var destroyResult = await stack.DestroyAsync(); + Assert.Equal(UpdateKind.Destroy, destroyResult.Summary.Kind); + Assert.Equal(UpdateState.Succeeded, destroyResult.Summary.Result); + + await stack.Workspace.RemoveStackAsync(stackName); + } + + [Fact] + public async Task InlineProgramExceptionPropagatesToCaller() + { + const string projectName = "exception_inline_node"; + var stackName = $"int_test_{GetTestSuffix()}"; + var program = PulumiFn.Create((Action)(() => throw new FileNotFoundException())); + + using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program) + { + EnvironmentVariables = new Dictionary() + { + ["PULUMI_CONFIG_PASSPHRASE"] = "test", + } + }); + + var upTask = stack.UpAsync(); + await Assert.ThrowsAsync( + () => upTask); + } + + private class FileNotFoundStack : Pulumi.Stack + { + public FileNotFoundStack() + { + throw new FileNotFoundException(); + } + } + + [Fact] + public async Task InlineProgramExceptionPropagatesToCallerWithTStack() + { + const string projectName = "exception_inline_tstack_node"; + var stackName = $"int_test_{GetTestSuffix()}"; + var program = PulumiFn.Create(); + + using var stack = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectName, stackName, program) + { + EnvironmentVariables = new Dictionary() + { + ["PULUMI_CONFIG_PASSPHRASE"] = "test", + } + }); + + var upTask = stack.UpAsync(); + await Assert.ThrowsAsync( + () => upTask); + } + + [Fact(Skip = "Parallel execution is not supported in this first version.")] + public async Task InlineProgramAllowsParallelExecution() + { + const string projectNameOne = "parallel_inline_node1"; + const string projectNameTwo = "parallel_inline_node2"; + var stackNameOne = $"int_test1_{GetTestSuffix()}"; + var stackNameTwo = $"int_test2_{GetTestSuffix()}"; + + var hasReachedSemaphoreOne = false; + using var semaphoreOne = new SemaphoreSlim(0, 1); + + var programOne = PulumiFn.Create(() => + { + // we want to assert before and after each interaction with + // the semaphore because we want to alternately stutter + // programOne and programTwo so we can assert they aren't + // touching eachothers instances + var config = new Pulumi.Config(); + Assert.Equal(projectNameOne, Deployment.Instance.ProjectName); + Assert.Equal(stackNameOne, Deployment.Instance.StackName); + hasReachedSemaphoreOne = true; + semaphoreOne.Wait(); + Assert.Equal(projectNameOne, Deployment.Instance.ProjectName); + Assert.Equal(stackNameOne, Deployment.Instance.StackName); + return new Dictionary + { + ["exp_static"] = "1", + ["exp_cfg"] = config.Get("bar"), + ["exp_secret"] = config.GetSecret("buzz"), + }; + }); + + var hasReachedSemaphoreTwo = false; + using var semaphoreTwo = new SemaphoreSlim(0, 1); + + var programTwo = PulumiFn.Create(() => + { + var config = new Pulumi.Config(); + Assert.Equal(projectNameTwo, Deployment.Instance.ProjectName); + Assert.Equal(stackNameTwo, Deployment.Instance.StackName); + hasReachedSemaphoreTwo = true; + semaphoreTwo.Wait(); + Assert.Equal(projectNameTwo, Deployment.Instance.ProjectName); + Assert.Equal(stackNameTwo, Deployment.Instance.StackName); + return new Dictionary + { + ["exp_static"] = "2", + ["exp_cfg"] = config.Get("bar"), + ["exp_secret"] = config.GetSecret("buzz"), + }; + }); + + using var stackOne = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectNameOne, stackNameOne, programOne) + { + EnvironmentVariables = new Dictionary() + { + ["PULUMI_CONFIG_PASSPHRASE"] = "test", + } + }); + + using var stackTwo = await LocalWorkspace.CreateStackAsync(new InlineProgramArgs(projectNameTwo, stackNameTwo, programTwo) + { + EnvironmentVariables = new Dictionary() + { + ["PULUMI_CONFIG_PASSPHRASE"] = "test", + } + }); + + await stackOne.SetConfigAsync(new Dictionary() + { + ["bar"] = new ConfigValue("1"), + ["buzz"] = new ConfigValue("1", isSecret: true), + }); + + await stackTwo.SetConfigAsync(new Dictionary() + { + ["bar"] = new ConfigValue("2"), + ["buzz"] = new ConfigValue("2", isSecret: true), + }); + + var upTaskOne = stackOne.UpAsync(); + // wait until we hit semaphore one + while (!hasReachedSemaphoreOne) + { + await Task.Delay(TimeSpan.FromSeconds(2)); + if (upTaskOne.IsFaulted) + throw upTaskOne.Exception!; + else if (upTaskOne.IsCompleted) + throw new Exception("Never hit semaphore in first UP task."); + } + + var upTaskTwo = stackTwo.UpAsync(); + // wait until we hit semaphore two + while (!hasReachedSemaphoreTwo) + { + await Task.Delay(TimeSpan.FromSeconds(2)); + if (upTaskTwo.IsFaulted) + throw upTaskTwo.Exception!; + else if (upTaskTwo.IsCompleted) + throw new Exception("Never hit semaphore in second UP task."); + } + + // alternately allow them to progress + semaphoreOne.Release(); + var upResultOne = await upTaskOne; + + semaphoreTwo.Release(); + var upResultTwo = await upTaskTwo; + + AssertUpResult(upResultOne, "1"); + AssertUpResult(upResultTwo, "2"); + + static void AssertUpResult(UpResult upResult, string value) + { + Assert.Equal(UpdateKind.Update, upResult.Summary.Kind); + Assert.Equal(UpdateState.Succeeded, upResult.Summary.Result); + Assert.Equal(3, upResult.Outputs.Count); + + // exp_static + Assert.True(upResult.Outputs.TryGetValue("exp_static", out var expStaticValue)); + Assert.Equal(value, expStaticValue!.Value); + Assert.False(expStaticValue.IsSecret); + + // exp_cfg + Assert.True(upResult.Outputs.TryGetValue("exp_cfg", out var expConfigValue)); + Assert.Equal(value, expConfigValue!.Value); + Assert.False(expConfigValue.IsSecret); + + // exp_secret + Assert.True(upResult.Outputs.TryGetValue("exp_secret", out var expSecretValue)); + Assert.Equal(value, expSecretValue!.Value); + Assert.True(expSecretValue.IsSecret); + } + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Pulumi.Automation.Tests.csproj b/sdk/dotnet/Pulumi.Automation.Tests/Pulumi.Automation.Tests.csproj new file mode 100644 index 000000000..c9cc28a67 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Pulumi.Automation.Tests.csproj @@ -0,0 +1,60 @@ + + + + netcoreapp3.1 + enable + true + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Serialization/DynamicObjectTests.cs b/sdk/dotnet/Pulumi.Automation.Tests/Serialization/DynamicObjectTests.cs new file mode 100644 index 000000000..9a64dfc6f --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Serialization/DynamicObjectTests.cs @@ -0,0 +1,52 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System.Collections.Generic; +using Pulumi.Automation.Serialization; +using Xunit; + +namespace Pulumi.Automation.Tests.Serialization +{ + public class DynamicObjectTests + { + private static LocalSerializer _serializer = new LocalSerializer(); + + [Fact] + public void Dynamic_With_YamlDotNet() + { + const string yaml = @" +one: 123 +two: two +three: true +nested: + test: test + testtwo: 123 +"; + + var dict = _serializer.DeserializeYaml>(yaml); + Assert.NotNull(dict); + Assert.NotEmpty(dict); + Assert.Equal(4, dict.Count); + } + + [Fact] + public void Dynamic_With_SystemTextJson() + { + const string json = @" +{ + ""one"": 123, + ""two"": ""two"", + ""three"": true, + ""nested"": { + ""test"": ""test"", + ""testtwo"": 123, + } +} +"; + + var dict = _serializer.DeserializeJson>(json); + Assert.NotNull(dict); + Assert.NotEmpty(dict); + Assert.Equal(4, dict.Count); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Serialization/GeneralJsonConverterTests.cs b/sdk/dotnet/Pulumi.Automation.Tests/Serialization/GeneralJsonConverterTests.cs new file mode 100644 index 000000000..136c7f147 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Serialization/GeneralJsonConverterTests.cs @@ -0,0 +1,140 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using Pulumi.Automation.Serialization; +using Xunit; + +namespace Pulumi.Automation.Tests.Serialization +{ + public class GeneralJsonConverterTests + { + private static LocalSerializer _serializer = new LocalSerializer(); + + [Fact] + public void CanDeserializeConfigValue() + { + var json = @" +{ + ""aws:region"": { + ""value"": ""us-east-1"", + ""secret"": false, + }, + ""project:name"": { + ""value"": ""test"", + ""secret"": true, + } +} +"; + + var config = _serializer.DeserializeJson>(json); + Assert.NotNull(config); + Assert.True(config.TryGetValue("aws:region", out var regionValue)); + Assert.Equal("us-east-1", regionValue!.Value); + Assert.False(regionValue.IsSecret); + Assert.True(config.TryGetValue("project:name", out var secretValue)); + Assert.Equal("test", secretValue!.Value); + Assert.True(secretValue.IsSecret); + } + + [Fact] + public void CanDeserializePluginInfo() + { + var json = @" +{ + ""name"": ""aws"", + ""kind"": ""resource"", + ""version"": ""3.19.2"", + ""size"": 258460028, + ""installTime"": ""2020-12-09T19:24:23.214Z"", + ""lastUsedTime"": ""2020-12-09T19:24:26.059Z"" +} +"; + var installTime = new DateTime(2020, 12, 9, 19, 24, 23, 214); + var lastUsedTime = new DateTime(2020, 12, 9, 19, 24, 26, 059); + + var info = _serializer.DeserializeJson(json); + Assert.NotNull(info); + Assert.Equal("aws", info.Name); + Assert.Equal(PluginKind.Resource, info.Kind); + Assert.Equal(258460028, info.Size); + Assert.Equal(new DateTimeOffset(installTime, TimeSpan.Zero), info.InstallTime); + Assert.Equal(new DateTimeOffset(lastUsedTime, TimeSpan.Zero), info.LastUsedTime); + } + + [Fact] + public void CanDeserializeUpdateSummary() + { + var json = @" +[ + { + ""kind"": ""destroy"", + ""startTime"": ""2021-01-07T17:08:49.000Z"", + ""message"": """", + ""environment"": { + ""exec.kind"": ""cli"" + }, + ""config"": { + ""aws:region"": { + ""value"": ""us-east-1"", + ""secret"": false + }, + ""quickstart:test"": { + ""value"": ""okok"", + ""secret"": true + } + }, + ""result"": ""in-progress"", + ""endTime"": ""2021-01-07T17:09:14.000Z"", + ""resourceChanges"": { + ""delete"": 3 + } + }, + { + ""kind"": ""update"", + ""startTime"": ""2021-01-07T17:02:10.000Z"", + ""message"": """", + ""environment"": { + ""exec.kind"": ""cli"" + }, + ""config"": { + ""aws:region"": { + ""value"": ""us-east-1"", + ""secret"": false + }, + ""quickstart:test"": { + ""value"": ""okok"", + ""secret"": true + } + }, + ""result"": ""succeeded"", + ""endTime"": ""2021-01-07T17:02:24.000Z"", + ""resourceChanges"": { + ""create"": 3 + } + } +] +"; + + var history = _serializer.DeserializeJson>(json); + Assert.NotNull(history); + Assert.Equal(2, history.Count); + + var destroy = history[0]; + Assert.Equal(UpdateKind.Destroy, destroy.Kind); + Assert.Equal(UpdateState.InProgress, destroy.Result); + Assert.NotNull(destroy.ResourceChanges); + Assert.Equal(1, destroy.ResourceChanges!.Count); + Assert.True(destroy.ResourceChanges.TryGetValue(OperationType.Delete, out var deletedCount)); + Assert.Equal(3, deletedCount); + + var update = history[1]; + Assert.Equal(UpdateKind.Update, update.Kind); + Assert.Equal(UpdateState.Succeeded, update.Result); + Assert.NotNull(update.ResourceChanges); + Assert.Equal(1, update.ResourceChanges!.Count); + Assert.True(update.ResourceChanges.TryGetValue(OperationType.Create, out var createdCount)); + Assert.Equal(3, createdCount); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Serialization/ProjectRuntimeJsonConverterTests.cs b/sdk/dotnet/Pulumi.Automation.Tests/Serialization/ProjectRuntimeJsonConverterTests.cs new file mode 100644 index 000000000..d6dd17d7f --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Serialization/ProjectRuntimeJsonConverterTests.cs @@ -0,0 +1,101 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Text.Json; +using Pulumi.Automation.Serialization; +using Xunit; + +namespace Pulumi.Automation.Tests.Serialization +{ + public class ProjectRuntimeJsonConverterTests + { + private static LocalSerializer _serializer = new LocalSerializer(); + + [Theory] + [InlineData(ProjectRuntimeName.NodeJS)] + [InlineData(ProjectRuntimeName.Go)] + [InlineData(ProjectRuntimeName.Python)] + [InlineData(ProjectRuntimeName.Dotnet)] + public void CanDeserializeWithStringRuntime(ProjectRuntimeName runtimeName) + { + var json = $@" +{{ + ""name"": ""test-project"", + ""runtime"": ""{runtimeName.ToString().ToLower()}"" +}} +"; + + var settings = _serializer.DeserializeJson(json); + Assert.NotNull(settings); + Assert.IsType(settings); + Assert.Equal("test-project", settings.Name); + Assert.Equal(runtimeName, settings.Runtime.Name); + Assert.Null(settings.Runtime.Options); + } + + [Theory] + [InlineData(ProjectRuntimeName.NodeJS)] + [InlineData(ProjectRuntimeName.Go)] + [InlineData(ProjectRuntimeName.Python)] + [InlineData(ProjectRuntimeName.Dotnet)] + public void CanDeserializeWithObjectRuntime(ProjectRuntimeName runtimeName) + { + var json = $@" +{{ + ""name"": ""test-project"", + ""runtime"": {{ + ""name"": ""{runtimeName.ToString().ToLower()}"", + ""options"": {{ + ""typeScript"": true, + ""binary"": ""test-binary"", + ""virtualEnv"": ""test-env"" + }} + }} +}} +"; + + var settings = _serializer.DeserializeJson(json); + Assert.NotNull(settings); + Assert.IsType(settings); + Assert.Equal("test-project", settings.Name); + Assert.Equal(runtimeName, settings.Runtime.Name); + Assert.NotNull(settings.Runtime.Options); + Assert.Equal(true, settings.Runtime.Options!.TypeScript); + Assert.Equal("test-binary", settings.Runtime.Options.Binary); + Assert.Equal("test-env", settings.Runtime.Options.VirtualEnv); + } + + [Fact] + public void SerializesAsStringIfOptionsNull() + { + var runtime = new ProjectRuntime(ProjectRuntimeName.Dotnet); + + var json = _serializer.SerializeJson(runtime); + Console.WriteLine(json); + + using var document = JsonDocument.Parse(json); + Assert.NotNull(document); + Assert.Equal(JsonValueKind.String, document.RootElement.ValueKind); + Assert.Equal("dotnet", document.RootElement.GetString()); + } + + [Fact] + public void SerializesAsObjectIfOptionsNotNull() + { + var runtime = new ProjectRuntime(ProjectRuntimeName.Dotnet) + { + Options = new ProjectRuntimeOptions + { + TypeScript = true, + }, + }; + + var json = _serializer.SerializeJson(runtime); + Console.WriteLine(json); + + using var document = JsonDocument.Parse(json); + Assert.NotNull(document); + Assert.Equal(JsonValueKind.Object, document.RootElement.ValueKind); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Serialization/ProjectRuntimeYamlConverterTests.cs b/sdk/dotnet/Pulumi.Automation.Tests/Serialization/ProjectRuntimeYamlConverterTests.cs new file mode 100644 index 000000000..667e3eb60 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Serialization/ProjectRuntimeYamlConverterTests.cs @@ -0,0 +1,97 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Text; +using Pulumi.Automation.Serialization; +using Xunit; + +namespace Pulumi.Automation.Tests.Serialization +{ + public class ProjectRuntimeYamlConverterTests + { + private static LocalSerializer _serializer = new LocalSerializer(); + + [Theory] + [InlineData(ProjectRuntimeName.NodeJS)] + [InlineData(ProjectRuntimeName.Go)] + [InlineData(ProjectRuntimeName.Python)] + [InlineData(ProjectRuntimeName.Dotnet)] + public void CanDeserializeWithStringRuntime(ProjectRuntimeName runtimeName) + { + var yaml = $@" +name: test-project +runtime: {runtimeName.ToString().ToLower()} +"; + + var model = _serializer.DeserializeYaml(yaml); + var settings = model.Convert(); + Assert.NotNull(settings); + Assert.IsType(settings); + Assert.Equal("test-project", settings.Name); + Assert.Equal(runtimeName, settings.Runtime.Name); + Assert.Null(settings.Runtime.Options); + } + + [Theory] + [InlineData(ProjectRuntimeName.NodeJS)] + [InlineData(ProjectRuntimeName.Go)] + [InlineData(ProjectRuntimeName.Python)] + [InlineData(ProjectRuntimeName.Dotnet)] + public void CanDeserializeWithObjectRuntime(ProjectRuntimeName runtimeName) + { + var yaml = $@" +name: test-project +runtime: + name: {runtimeName.ToString().ToLower()} + options: + typescript: true + binary: test-binary + virtualenv: test-env +"; + + var model = _serializer.DeserializeYaml(yaml); + var settings = model.Convert(); + Assert.NotNull(settings); + Assert.IsType(settings); + Assert.Equal("test-project", settings.Name); + Assert.Equal(runtimeName, settings.Runtime.Name); + Assert.NotNull(settings.Runtime.Options); + Assert.Equal(true, settings.Runtime.Options!.TypeScript); + Assert.Equal("test-binary", settings.Runtime.Options.Binary); + Assert.Equal("test-env", settings.Runtime.Options.VirtualEnv); + } + + [Fact] + public void SerializesAsStringIfOptionsNull() + { + var runtime = new ProjectRuntime(ProjectRuntimeName.Dotnet); + + var yaml = _serializer.SerializeYaml(runtime); + Console.WriteLine(yaml); + + Assert.Equal("dotnet\r\n", yaml); + } + + [Fact] + public void SerializesAsObjectIfOptionsNotNull() + { + var runtime = new ProjectRuntime(ProjectRuntimeName.Dotnet) + { + Options = new ProjectRuntimeOptions + { + TypeScript = true, + }, + }; + + var yaml = _serializer.SerializeYaml(runtime); + Console.WriteLine(yaml); + + var expected = new StringBuilder(); + expected.Append("name: dotnet\r\n"); + expected.Append("options:\r\n"); + expected.Append(" typescript: true\r\n"); + + Assert.Equal(expected.ToString(), yaml); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Serialization/StackSettingsConfigValueJsonConverterTests.cs b/sdk/dotnet/Pulumi.Automation.Tests/Serialization/StackSettingsConfigValueJsonConverterTests.cs new file mode 100644 index 000000000..30367ef0e --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Serialization/StackSettingsConfigValueJsonConverterTests.cs @@ -0,0 +1,104 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Text.Json; +using Pulumi.Automation.Serialization; +using Xunit; + +namespace Pulumi.Automation.Tests.Serialization +{ + public class StackSettingsConfigValueJsonConverterTests + { + private static LocalSerializer _serializer = new LocalSerializer(); + + [Fact] + public void CanDeserializePlainString() + { + const string json = @" +{ + ""config"": { + ""test"": ""plain"" + } +} +"; + + var settings = _serializer.DeserializeJson(json); + Assert.NotNull(settings?.Config); + Assert.True(settings!.Config!.ContainsKey("test")); + + var value = settings.Config["test"]; + Assert.NotNull(value); + Assert.Equal("plain", value.Value); + Assert.False(value.IsSecure); + } + + [Fact] + public void CanDeserializeSecureString() + { + const string json = @" +{ + ""config"": { + ""test"": { + ""secure"": ""secret"" + } + } +} +"; + + var settings = _serializer.DeserializeJson(json); + Assert.NotNull(settings?.Config); + Assert.True(settings!.Config!.ContainsKey("test")); + + var value = settings.Config["test"]; + Assert.NotNull(value); + Assert.Equal("secret", value.Value); + Assert.True(value.IsSecure); + } + + [Fact] + public void CannotDeserializeObject() + { + const string json = @" +{ + ""config"": { + ""value"": { + ""test"": ""test"", + ""nested"": { + ""one"": 1, + ""two"": true, + ""three"": ""three"" + } + } + } +} +"; + + Assert.Throws( + () => _serializer.DeserializeJson(json)); + } + + [Fact] + public void SerializesPlainStringAsString() + { + var value = new StackSettingsConfigValue("test", false); + var json = _serializer.SerializeJson(value); + + var element = JsonSerializer.Deserialize(json); + Assert.Equal(JsonValueKind.String, element.ValueKind); + Assert.Equal("test", element.GetString()); + } + + [Fact] + public void SerializesSecureStringAsObject() + { + var value = new StackSettingsConfigValue("secret", true); + var json = _serializer.SerializeJson(value); + + var element = JsonSerializer.Deserialize(json); + Assert.Equal(JsonValueKind.Object, element.ValueKind); + Assert.True(element.TryGetProperty("secure", out var secureProperty)); + Assert.Equal(JsonValueKind.String, secureProperty.ValueKind); + Assert.Equal("secret", secureProperty.GetString()); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Serialization/StackSettingsConfigValueYamlConverterTests.cs b/sdk/dotnet/Pulumi.Automation.Tests/Serialization/StackSettingsConfigValueYamlConverterTests.cs new file mode 100644 index 000000000..61e414d81 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/Serialization/StackSettingsConfigValueYamlConverterTests.cs @@ -0,0 +1,83 @@ +// Copyright 2016-2021, Pulumi Corporation + +using Pulumi.Automation.Serialization; +using Xunit; +using YamlDotNet.Core; + +namespace Pulumi.Automation.Tests.Serialization +{ + public class StackSettingsConfigValueYamlConverterTests + { + private static LocalSerializer _serializer = new LocalSerializer(); + + [Fact] + public void CanDeserializePlainString() + { + const string yaml = @" +config: + test: plain +"; + + var settings = _serializer.DeserializeYaml(yaml); + Assert.NotNull(settings?.Config); + Assert.True(settings!.Config!.ContainsKey("test")); + + var value = settings.Config["test"]; + Assert.NotNull(value); + Assert.Equal("plain", value.Value); + Assert.False(value.IsSecure); + } + + [Fact] + public void CanDeserializeSecureString() + { + const string yaml = @" +config: + test: + secure: secret +"; + + var settings = _serializer.DeserializeYaml(yaml); + Assert.NotNull(settings?.Config); + Assert.True(settings!.Config!.ContainsKey("test")); + + var value = settings.Config["test"]; + Assert.NotNull(value); + Assert.Equal("secret", value.Value); + Assert.True(value.IsSecure); + } + + [Fact] + public void CannotDeserializeObject() + { + const string yaml = @" +config: + value: + test: test + nested: + one: 1 + two: true + three: three +"; + + Assert.Throws( + () => _serializer.DeserializeYaml(yaml)); + } + + [Fact] + public void SerializesPlainStringAsString() + { + var value = new StackSettingsConfigValue("test", false); + var yaml = _serializer.SerializeYaml(value); + Assert.Equal("test\r\n", yaml); + } + + [Fact] + public void SerializesSecureStringAsObject() + { + var value = new StackSettingsConfigValue("secret", true); + var yaml = _serializer.SerializeYaml(value); + Assert.Equal("secure: secret\r\n", yaml); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation.Tests/xunit.runner.json b/sdk/dotnet/Pulumi.Automation.Tests/xunit.runner.json new file mode 100644 index 000000000..517db67fc --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "parallelizeTestCollections": false +} diff --git a/sdk/dotnet/Pulumi.Automation/AssemblyAttribute.cs b/sdk/dotnet/Pulumi.Automation/AssemblyAttribute.cs new file mode 100644 index 000000000..78d8e7d75 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/AssemblyAttribute.cs @@ -0,0 +1,5 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Pulumi.Automation.Tests")] diff --git a/sdk/dotnet/Pulumi.Automation/Commands/CommandResult.cs b/sdk/dotnet/Pulumi.Automation/Commands/CommandResult.cs new file mode 100644 index 000000000..eeeb1741c --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Commands/CommandResult.cs @@ -0,0 +1,35 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System.Text; + +namespace Pulumi.Automation.Commands +{ + internal class CommandResult + { + public int Code { get; } + + public string StandardOutput { get; } + + public string StandardError { get; } + + public CommandResult( + int code, + string standardOutput, + string standardError) + { + this.Code = code; + this.StandardOutput = standardOutput; + this.StandardError = standardError; + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendLine($"code: {this.Code}"); + sb.AppendLine($"stdout: {this.StandardOutput}"); + sb.AppendLine($"stderr: {this.StandardError}"); + + return sb.ToString(); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Commands/Exceptions/CommandException.cs b/sdk/dotnet/Pulumi.Automation/Commands/Exceptions/CommandException.cs new file mode 100644 index 000000000..3c00ecc90 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Commands/Exceptions/CommandException.cs @@ -0,0 +1,33 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Text.RegularExpressions; + +namespace Pulumi.Automation.Commands.Exceptions +{ + public class CommandException : Exception + { + public string Name { get; } + + internal CommandException(CommandResult result) + : this(nameof(CommandException), result) + { + } + + internal CommandException(string name, CommandResult result) + : base(result.ToString()) + { + this.Name = name; + } + + private static readonly Regex NotFoundRegexPattern = new Regex("no stack named.*found"); + private static readonly Regex AlreadyExistsRegexPattern = new Regex("stack.*already exists"); + private static readonly string ConflictText = "[409] Conflict: Another update is currently in progress."; + + internal static CommandException CreateFromResult(CommandResult result) + => NotFoundRegexPattern.IsMatch(result.StandardError) ? new StackNotFoundException(result) + : AlreadyExistsRegexPattern.IsMatch(result.StandardError) ? new StackAlreadyExistsException(result) + : result.StandardError?.IndexOf(ConflictText) >= 0 ? new ConcurrentUpdateException(result) + : new CommandException(result); + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Commands/Exceptions/ConcurrentUpdateException.cs b/sdk/dotnet/Pulumi.Automation/Commands/Exceptions/ConcurrentUpdateException.cs new file mode 100644 index 000000000..553ed4fac --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Commands/Exceptions/ConcurrentUpdateException.cs @@ -0,0 +1,12 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation.Commands.Exceptions +{ + public sealed class ConcurrentUpdateException : CommandException + { + internal ConcurrentUpdateException(CommandResult result) + : base(nameof(ConcurrentUpdateException), result) + { + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Commands/Exceptions/StackAlreadyExistsException.cs b/sdk/dotnet/Pulumi.Automation/Commands/Exceptions/StackAlreadyExistsException.cs new file mode 100644 index 000000000..b2e3c7c80 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Commands/Exceptions/StackAlreadyExistsException.cs @@ -0,0 +1,12 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation.Commands.Exceptions +{ + public class StackAlreadyExistsException : CommandException + { + internal StackAlreadyExistsException(CommandResult result) + : base(nameof(StackAlreadyExistsException), result) + { + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Commands/Exceptions/StackNotFoundException.cs b/sdk/dotnet/Pulumi.Automation/Commands/Exceptions/StackNotFoundException.cs new file mode 100644 index 000000000..8c1b4273a --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Commands/Exceptions/StackNotFoundException.cs @@ -0,0 +1,12 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation.Commands.Exceptions +{ + public class StackNotFoundException : CommandException + { + internal StackNotFoundException(CommandResult result) + : base(nameof(StackNotFoundException), result) + { + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Commands/IPulumiCmd.cs b/sdk/dotnet/Pulumi.Automation/Commands/IPulumiCmd.cs new file mode 100644 index 000000000..546c8b3eb --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Commands/IPulumiCmd.cs @@ -0,0 +1,19 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Pulumi.Automation.Commands +{ + internal interface IPulumiCmd + { + Task RunAsync( + IEnumerable args, + string workingDir, + IDictionary additionalEnv, + Action? onOutput = null, + CancellationToken cancellationToken = default); + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Commands/LocalPulumiCmd.cs b/sdk/dotnet/Pulumi.Automation/Commands/LocalPulumiCmd.cs new file mode 100644 index 000000000..f896b9055 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Commands/LocalPulumiCmd.cs @@ -0,0 +1,108 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Pulumi.Automation.Commands.Exceptions; + +namespace Pulumi.Automation.Commands +{ + internal class LocalPulumiCmd : IPulumiCmd + { + public async Task RunAsync( + IEnumerable args, + string workingDir, + IDictionary additionalEnv, + Action? onOutput = null, + CancellationToken cancellationToken = default) + { + // all commands should be run in non-interactive mode. + // this causes commands to fail rather than prompting for input (and thus hanging indefinitely) + var completeArgs = args.Concat(new[] { "--non-interactive" }); + + var env = new Dictionary(); + foreach (var element in Environment.GetEnvironmentVariables()) + { + if (element is KeyValuePair pair + && pair.Value is string valueStr) + env[pair.Key] = valueStr; + } + + foreach (var pair in additionalEnv) + env[pair.Key] = pair.Value; + + using var proc = new Process + { + EnableRaisingEvents = true, + StartInfo = new ProcessStartInfo + { + FileName = "pulumi", + WorkingDirectory = workingDir, + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true, + }, + }; + + foreach (var arg in completeArgs) + proc.StartInfo.ArgumentList.Add(arg); + + foreach (var pair in env) + proc.StartInfo.Environment[pair.Key] = pair.Value; + + proc.OutputDataReceived += (_, @event) => + { + if (@event.Data != null) + onOutput?.Invoke(@event.Data); + }; + + var tcs = new TaskCompletionSource(); + using var cancelRegistration = cancellationToken.Register(() => + { + // if the process has already exited than let's + // just let it set the result on the task + if (proc.HasExited || tcs.Task.IsCompleted) + return; + + // setting it cancelled before killing so there + // isn't a race condition to the proc.Exited event + tcs.TrySetCanceled(cancellationToken); + + try + { + proc.Kill(); + } + catch + { + // in case the process hasn't started yet + // or has already terminated + } + }); + + proc.Exited += async (_, @event) => + { + var code = proc.ExitCode; + var stdOut = await proc.StandardOutput.ReadToEndAsync().ConfigureAwait(false); + var stdErr = await proc.StandardError.ReadToEndAsync().ConfigureAwait(false); + + var result = new CommandResult(code, stdOut, stdErr); + if (code != 0) + { + var ex = CommandException.CreateFromResult(result); + tcs.TrySetException(ex); + } + else + { + tcs.TrySetResult(result); + } + }; + + proc.Start(); + return await tcs.Task.ConfigureAwait(false); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/ConfigValue.cs b/sdk/dotnet/Pulumi.Automation/ConfigValue.cs new file mode 100644 index 000000000..9443b29a2 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/ConfigValue.cs @@ -0,0 +1,19 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + public class ConfigValue + { + public string Value { get; set; } + + public bool IsSecret { get; set; } + + public ConfigValue( + string value, + bool isSecret = false) + { + this.Value = value; + this.IsSecret = isSecret; + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/DestroyOptions.cs b/sdk/dotnet/Pulumi.Automation/DestroyOptions.cs new file mode 100644 index 000000000..fcf818bf3 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/DestroyOptions.cs @@ -0,0 +1,16 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; + +namespace Pulumi.Automation +{ + /// + /// Options controlling the behavior of an operation. + /// + public sealed class DestroyOptions : UpdateOptions + { + public bool? TargetDependents { get; set; } + + public Action? OnOutput { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/HistoryOptions.cs b/sdk/dotnet/Pulumi.Automation/HistoryOptions.cs new file mode 100644 index 000000000..d1d1fcdf0 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/HistoryOptions.cs @@ -0,0 +1,14 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + /// + /// Options controlling the behavior of a operation. + /// + public sealed class HistoryOptions + { + public int? Page { get; set; } + + public int? PageSize { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/InlineProgramArgs.cs b/sdk/dotnet/Pulumi.Automation/InlineProgramArgs.cs new file mode 100644 index 000000000..782ba3560 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/InlineProgramArgs.cs @@ -0,0 +1,19 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + public class InlineProgramArgs : LocalWorkspaceOptions + { + public string StackName { get; } + + public InlineProgramArgs( + string projectName, + string stackName, + PulumiFn program) + { + this.ProjectSettings = ProjectSettings.Default(projectName); + this.StackName = stackName; + this.Program = program; + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/LocalProgramArgs.cs b/sdk/dotnet/Pulumi.Automation/LocalProgramArgs.cs new file mode 100644 index 000000000..f2621eba2 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/LocalProgramArgs.cs @@ -0,0 +1,20 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + /// + /// Description of a stack backed by pre-existing local Pulumi CLI program. + /// + public class LocalProgramArgs : LocalWorkspaceOptions + { + public string StackName { get; } + + public LocalProgramArgs( + string stackName, + string workDir) + { + this.StackName = stackName; + this.WorkDir = workDir; + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/LocalWorkspace.cs b/sdk/dotnet/Pulumi.Automation/LocalWorkspace.cs new file mode 100644 index 000000000..851323577 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/LocalWorkspace.cs @@ -0,0 +1,598 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Pulumi.Automation.Commands; +using Pulumi.Automation.Serialization; + +namespace Pulumi.Automation +{ + /// + /// LocalWorkspace is a default implementation of the Workspace interface. + /// + /// A Workspace is the execution context containing a single Pulumi project, a program, + /// and multiple stacks.Workspaces are used to manage the execution environment, + /// providing various utilities such as plugin installation, environment configuration + /// ($PULUMI_HOME), and creation, deletion, and listing of Stacks. + /// + /// LocalWorkspace relies on Pulumi.yaml and Pulumi.{stack}.yaml as the intermediate format + /// for Project and Stack settings.Modifying ProjectSettings will + /// alter the Workspace Pulumi.yaml file, and setting config on a Stack will modify the Pulumi.{stack}.yaml file. + /// This is identical to the behavior of Pulumi CLI driven workspaces. + /// + /// If not provided a working directory - causing LocalWorkspace to create a temp directory, + /// than the temp directory will be cleaned up on . + /// + public sealed class LocalWorkspace : Workspace + { + private readonly LocalSerializer _serializer = new LocalSerializer(); + private readonly bool _ownsWorkingDir; + private readonly Task _readyTask; + + /// + public override string WorkDir { get; } + + /// + public override string? PulumiHome { get; } + + /// + public override string? SecretsProvider { get; } + + /// + public override PulumiFn? Program { get; set; } + + /// + public override IDictionary? EnvironmentVariables { get; set; } + + /// + /// Creates a workspace using the specified options. Used for maximal control and + /// customization of the underlying environment before any stacks are created or selected. + /// + /// Options used to configure the workspace. + /// A cancellation token. + public static async Task CreateAsync( + LocalWorkspaceOptions? options = null, + CancellationToken cancellationToken = default) + { + var ws = new LocalWorkspace( + new LocalPulumiCmd(), + options, + cancellationToken); + await ws._readyTask.ConfigureAwait(false); + return ws; + } + + /// + /// Creates a Stack with a utilizing the specified + /// inline (in process) . This program + /// is fully debuggable and runs in process. If no + /// option is specified, default project settings will be created on behalf of the user. Similarly, unless a + /// option is specified, the working directory will default + /// to a new temporary directory provided by the OS. + /// + /// + /// A set of arguments to initialize a Stack with an inline program + /// that runs in process, as well as any additional customizations to be applied to the + /// workspace. + /// + public static Task CreateStackAsync(InlineProgramArgs args) + => CreateStackAsync(args, default); + + /// + /// Creates a Stack with a utilizing the specified + /// inline (in process) . This program + /// is fully debuggable and runs in process. If no + /// option is specified, default project settings will be created on behalf of the user. Similarly, unless a + /// option is specified, the working directory will default + /// to a new temporary directory provided by the OS. + /// + /// + /// A set of arguments to initialize a Stack with an inline program + /// that runs in process, as well as any additional customizations to be applied to the + /// workspace. + /// + /// A cancellation token. + public static Task CreateStackAsync(InlineProgramArgs args, CancellationToken cancellationToken) + => CreateStackHelperAsync(args, WorkspaceStack.CreateAsync, cancellationToken); + + /// + /// Creates a Stack with a utilizing the local Pulumi CLI program + /// from the specified . This is a way to create drivers + /// on top of pre-existing Pulumi programs. This Workspace will pick up any available Settings + /// files(Pulumi.yaml, Pulumi.{stack}.yaml). + /// + /// + /// A set of arguments to initialize a Stack with a pre-configured Pulumi CLI program that + /// already exists on disk, as well as any additional customizations to be applied to the + /// workspace. + /// + public static Task CreateStackAsync(LocalProgramArgs args) + => CreateStackAsync(args, default); + + /// + /// Creates a Stack with a utilizing the local Pulumi CLI program + /// from the specified . This is a way to create drivers + /// on top of pre-existing Pulumi programs. This Workspace will pick up any available Settings + /// files(Pulumi.yaml, Pulumi.{stack}.yaml). + /// + /// + /// A set of arguments to initialize a Stack with a pre-configured Pulumi CLI program that + /// already exists on disk, as well as any additional customizations to be applied to the + /// workspace. + /// + /// A cancellation token. + public static Task CreateStackAsync(LocalProgramArgs args, CancellationToken cancellationToken) + => CreateStackHelperAsync(args, WorkspaceStack.CreateAsync, cancellationToken); + + /// + /// Selects an existing Stack with a utilizing the specified + /// inline (in process) . This program + /// is fully debuggable and runs in process. If no + /// option is specified, default project settings will be created on behalf of the user. Similarly, unless a + /// option is specified, the working directory will default + /// to a new temporary directory provided by the OS. + /// + /// + /// A set of arguments to initialize a Stack with an inline program + /// that runs in process, as well as any additional customizations to be applied to the + /// workspace. + /// + public static Task SelectStackAsync(InlineProgramArgs args) + => SelectStackAsync(args, default); + + /// + /// Selects an existing Stack with a utilizing the specified + /// inline (in process) . This program + /// is fully debuggable and runs in process. If no + /// option is specified, default project settings will be created on behalf of the user. Similarly, unless a + /// option is specified, the working directory will default + /// to a new temporary directory provided by the OS. + /// + /// + /// A set of arguments to initialize a Stack with an inline program + /// that runs in process, as well as any additional customizations to be applied to the + /// workspace. + /// + /// A cancellation token. + public static Task SelectStackAsync(InlineProgramArgs args, CancellationToken cancellationToken) + => CreateStackHelperAsync(args, WorkspaceStack.SelectAsync, cancellationToken); + + /// + /// Selects an existing Stack with a utilizing the local Pulumi CLI program + /// from the specified . This is a way to create drivers + /// on top of pre-existing Pulumi programs. This Workspace will pick up any available Settings + /// files(Pulumi.yaml, Pulumi.{stack}.yaml). + /// + /// + /// A set of arguments to initialize a Stack with a pre-configured Pulumi CLI program that + /// already exists on disk, as well as any additional customizations to be applied to the + /// workspace. + /// + public static Task SelectStackAsync(LocalProgramArgs args) + => SelectStackAsync(args, default); + + /// + /// Selects an existing Stack with a utilizing the local Pulumi CLI program + /// from the specified . This is a way to create drivers + /// on top of pre-existing Pulumi programs. This Workspace will pick up any available Settings + /// files(Pulumi.yaml, Pulumi.{stack}.yaml). + /// + /// + /// A set of arguments to initialize a Stack with a pre-configured Pulumi CLI program that + /// already exists on disk, as well as any additional customizations to be applied to the + /// workspace. + /// + /// A cancellation token. + public static Task SelectStackAsync(LocalProgramArgs args, CancellationToken cancellationToken) + => CreateStackHelperAsync(args, WorkspaceStack.SelectAsync, cancellationToken); + + /// + /// Creates or selects an existing Stack with a utilizing the specified + /// inline (in process) . This program + /// is fully debuggable and runs in process. If no + /// option is specified, default project settings will be created on behalf of the user. Similarly, unless a + /// option is specified, the working directory will default + /// to a new temporary directory provided by the OS. + /// + /// + /// A set of arguments to initialize a Stack with an inline program + /// that runs in process, as well as any additional customizations to be applied to the + /// workspace. + /// + public static Task CreateOrSelectStackAsync(InlineProgramArgs args) + => CreateOrSelectStackAsync(args, default); + + /// + /// Creates or selects an existing Stack with a utilizing the specified + /// inline (in process) . This program + /// is fully debuggable and runs in process. If no + /// option is specified, default project settings will be created on behalf of the user. Similarly, unless a + /// option is specified, the working directory will default + /// to a new temporary directory provided by the OS. + /// + /// + /// A set of arguments to initialize a Stack with an inline program + /// that runs in process, as well as any additional customizations to be applied to the + /// workspace. + /// + /// A cancellation token. + public static Task CreateOrSelectStackAsync(InlineProgramArgs args, CancellationToken cancellationToken) + => CreateStackHelperAsync(args, WorkspaceStack.CreateOrSelectAsync, cancellationToken); + + /// + /// Creates or selects an existing Stack with a utilizing the local Pulumi CLI program + /// from the specified . This is a way to create drivers + /// on top of pre-existing Pulumi programs. This Workspace will pick up any available Settings + /// files(Pulumi.yaml, Pulumi.{stack}.yaml). + /// + /// + /// A set of arguments to initialize a Stack with a pre-configured Pulumi CLI program that + /// already exists on disk, as well as any additional customizations to be applied to the + /// workspace. + /// + public static Task CreateOrSelectStackAsync(LocalProgramArgs args) + => CreateOrSelectStackAsync(args, default); + + /// + /// Creates or selects an existing Stack with a utilizing the local Pulumi CLI program + /// from the specified . This is a way to create drivers + /// on top of pre-existing Pulumi programs. This Workspace will pick up any available Settings + /// files(Pulumi.yaml, Pulumi.{stack}.yaml). + /// + /// + /// A set of arguments to initialize a Stack with a pre-configured Pulumi CLI program that + /// already exists on disk, as well as any additional customizations to be applied to the + /// workspace. + /// + /// A cancellation token. + public static Task CreateOrSelectStackAsync(LocalProgramArgs args, CancellationToken cancellationToken) + => CreateStackHelperAsync(args, WorkspaceStack.CreateOrSelectAsync, cancellationToken); + + private static async Task CreateStackHelperAsync( + InlineProgramArgs args, + Func> initFunc, + CancellationToken cancellationToken) + { + if (args.ProjectSettings is null) + throw new ArgumentNullException(nameof(args.ProjectSettings)); + + var ws = new LocalWorkspace( + new LocalPulumiCmd(), + args, + cancellationToken); + await ws._readyTask.ConfigureAwait(false); + + return await initFunc(args.StackName, ws, cancellationToken).ConfigureAwait(false); + } + + private static async Task CreateStackHelperAsync( + LocalProgramArgs args, + Func> initFunc, + CancellationToken cancellationToken) + { + var ws = new LocalWorkspace( + new LocalPulumiCmd(), + args, + cancellationToken); + await ws._readyTask.ConfigureAwait(false); + + return await initFunc(args.StackName, ws, cancellationToken).ConfigureAwait(false); + } + + internal LocalWorkspace( + IPulumiCmd cmd, + LocalWorkspaceOptions? options, + CancellationToken cancellationToken) + : base(cmd) + { + string? dir = null; + var readyTasks = new List(); + + if (options != null) + { + if (!string.IsNullOrWhiteSpace(options.WorkDir)) + dir = options.WorkDir; + + this.PulumiHome = options.PulumiHome; + this.Program = options.Program; + this.SecretsProvider = options.SecretsProvider; + + if (options.EnvironmentVariables != null) + this.EnvironmentVariables = new Dictionary(options.EnvironmentVariables); + } + + if (string.IsNullOrWhiteSpace(dir)) + { + // note that csharp doesn't guarantee that Path.GetRandomFileName returns a name + // for a file or folder that doesn't already exist. + // we should be OK with the "automation-" prefix but a collision is still + // theoretically possible + dir = Path.Combine(Path.GetTempPath(), $"automation-{Path.GetRandomFileName()}"); + Directory.CreateDirectory(dir); + this._ownsWorkingDir = true; + } + + this.WorkDir = dir; + + // these are after working dir is set because they start immediately + if (options?.ProjectSettings != null) + readyTasks.Add(this.SaveProjectSettingsAsync(options.ProjectSettings, cancellationToken)); + + if (options?.StackSettings != null && options.StackSettings.Any()) + { + foreach (var pair in options.StackSettings) + readyTasks.Add(this.SaveStackSettingsAsync(pair.Key, pair.Value, cancellationToken)); + } + + this._readyTask = Task.WhenAll(readyTasks); + } + + private static readonly string[] SettingsExtensions = new string[] { ".yaml", ".yml", ".json" }; + + /// + public override async Task GetProjectSettingsAsync(CancellationToken cancellationToken = default) + { + foreach (var ext in SettingsExtensions) + { + var isJson = ext == ".json"; + var path = Path.Combine(this.WorkDir, $"Pulumi{ext}"); + if (!File.Exists(path)) + continue; + + var content = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false); + if (isJson) + return this._serializer.DeserializeJson(content); + + var model = this._serializer.DeserializeYaml(content); + return model.Convert(); + } + + return null; + } + + /// + public override Task SaveProjectSettingsAsync(ProjectSettings settings, CancellationToken cancellationToken = default) + { + var foundExt = ".yaml"; + foreach (var ext in SettingsExtensions) + { + var testPath = Path.Combine(this.WorkDir, $"Pulumi{ext}"); + if (File.Exists(testPath)) + { + foundExt = ext; + break; + } + } + + var path = Path.Combine(this.WorkDir, $"Pulumi{foundExt}"); + var content = foundExt == ".json" ? this._serializer.SerializeJson(settings) : this._serializer.SerializeYaml(settings); + return File.WriteAllTextAsync(path, content, cancellationToken); + } + + private static string GetStackSettingsName(string stackName) + { + var parts = stackName.Split('/'); + if (parts.Length < 1) + return stackName; + + return parts[^1]; + } + + /// + public override async Task GetStackSettingsAsync(string stackName, CancellationToken cancellationToken = default) + { + var settingsName = GetStackSettingsName(stackName); + + foreach (var ext in SettingsExtensions) + { + var isJson = ext == ".json"; + var path = Path.Combine(this.WorkDir, $"Pulumi.{settingsName}{ext}"); + if (!File.Exists(path)) + continue; + + var content = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false); + return isJson ? this._serializer.DeserializeJson(content) : this._serializer.DeserializeYaml(content); + } + + return null; + } + + /// + public override Task SaveStackSettingsAsync(string stackName, StackSettings settings, CancellationToken cancellationToken = default) + { + var settingsName = GetStackSettingsName(stackName); + + var foundExt = ".yaml"; + foreach (var ext in SettingsExtensions) + { + var testPath = Path.Combine(this.WorkDir, $"Pulumi.{settingsName}{ext}"); + if (File.Exists(testPath)) + { + foundExt = ext; + break; + } + } + + var path = Path.Combine(this.WorkDir, $"Pulumi.{settingsName}{foundExt}"); + var content = foundExt == ".json" ? this._serializer.SerializeJson(settings) : this._serializer.SerializeYaml(settings); + return File.WriteAllTextAsync(path, content, cancellationToken); + } + + /// + public override Task> SerializeArgsForOpAsync(string stackName, CancellationToken cancellationToken = default) + => Task.FromResult(ImmutableList.Empty); + + /// + public override Task PostCommandCallbackAsync(string stackName, CancellationToken cancellationToken = default) + => Task.CompletedTask; + + /// + public override async Task GetConfigValueAsync(string stackName, string key, CancellationToken cancellationToken = default) + { + await this.SelectStackAsync(stackName, cancellationToken).ConfigureAwait(false); + var result = await this.RunCommandAsync(new[] { "config", "get", key, "--json" }, cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(result.StandardOutput); + } + + /// + public override async Task> GetConfigAsync(string stackName, CancellationToken cancellationToken = default) + { + await this.SelectStackAsync(stackName, cancellationToken).ConfigureAwait(false); + return await this.GetConfigAsync(cancellationToken).ConfigureAwait(false); + } + + private async Task> GetConfigAsync(CancellationToken cancellationToken) + { + var result = await this.RunCommandAsync(new[] { "config", "--show-secrets", "--json" }, cancellationToken).ConfigureAwait(false); + var dict = this._serializer.DeserializeJson>(result.StandardOutput); + return dict.ToImmutableDictionary(); + } + + /// + public override async Task SetConfigValueAsync(string stackName, string key, ConfigValue value, CancellationToken cancellationToken = default) + { + await this.SelectStackAsync(stackName, cancellationToken).ConfigureAwait(false); + await this.SetConfigValueAsync(key, value, cancellationToken).ConfigureAwait(false); + } + + /// + public override async Task SetConfigAsync(string stackName, IDictionary configMap, CancellationToken cancellationToken = default) + { + // TODO: do this in parallel after this is fixed https://github.com/pulumi/pulumi/issues/3877 + await this.SelectStackAsync(stackName, cancellationToken).ConfigureAwait(false); + + foreach (var (key, value) in configMap) + await this.SetConfigValueAsync(key, value, cancellationToken).ConfigureAwait(false); + } + + private async Task SetConfigValueAsync(string key, ConfigValue value, CancellationToken cancellationToken) + { + var secretArg = value.IsSecret ? "--secret" : "--plaintext"; + await this.RunCommandAsync(new[] { "config", "set", key, value.Value, secretArg }, cancellationToken).ConfigureAwait(false); + } + + /// + public override async Task RemoveConfigValueAsync(string stackName, string key, CancellationToken cancellationToken = default) + { + await this.SelectStackAsync(stackName, cancellationToken).ConfigureAwait(false); + await this.RunCommandAsync(new[] { "config", "rm", key }, cancellationToken).ConfigureAwait(false); + } + + /// + public override async Task RemoveConfigAsync(string stackName, IEnumerable keys, CancellationToken cancellationToken = default) + { + // TODO: do this in parallel after this is fixed https://github.com/pulumi/pulumi/issues/3877 + await this.SelectStackAsync(stackName, cancellationToken).ConfigureAwait(false); + + foreach (var key in keys) + await this.RunCommandAsync(new[] { "config", "rm", key }, cancellationToken).ConfigureAwait(false); + } + + /// + public override async Task> RefreshConfigAsync(string stackName, CancellationToken cancellationToken = default) + { + await this.SelectStackAsync(stackName, cancellationToken).ConfigureAwait(false); + await this.RunCommandAsync(new[] { "config", "refresh", "--force" }, cancellationToken).ConfigureAwait(false); + return await this.GetConfigAsync(cancellationToken).ConfigureAwait(false); + } + + /// + public override async Task WhoAmIAsync(CancellationToken cancellationToken = default) + { + var result = await this.RunCommandAsync(new[] { "whoami" }, cancellationToken).ConfigureAwait(false); + return new WhoAmIResult(result.StandardOutput.Trim()); + } + + /// + public override Task CreateStackAsync(string stackName, CancellationToken cancellationToken) + { + var args = new List() + { + "stack", + "init", + stackName, + }; + + if (!string.IsNullOrWhiteSpace(this.SecretsProvider)) + args.AddRange(new[] { "--secrets-provider", this.SecretsProvider }); + + return this.RunCommandAsync(args, cancellationToken); + } + + /// + public override Task SelectStackAsync(string stackName, CancellationToken cancellationToken) + => this.RunCommandAsync(new[] { "stack", "select", stackName }, cancellationToken); + + /// + public override Task RemoveStackAsync(string stackName, CancellationToken cancellationToken = default) + => this.RunCommandAsync(new[] { "stack", "rm", "--yes", stackName }, cancellationToken); + + /// + public override async Task> ListStacksAsync(CancellationToken cancellationToken = default) + { + var result = await this.RunCommandAsync(new[] { "stack", "ls", "--json" }, cancellationToken).ConfigureAwait(false); + var stacks = this._serializer.DeserializeJson>(result.StandardOutput); + return stacks.ToImmutableList(); + } + + /// + public override Task InstallPluginAsync(string name, string version, PluginKind kind = PluginKind.Resource, CancellationToken cancellationToken = default) + => this.RunCommandAsync(new[] { "plugin", "install", kind.ToString().ToLower(), name, version }, cancellationToken); + + /// + public override Task RemovePluginAsync(string? name = null, string? versionRange = null, PluginKind kind = PluginKind.Resource, CancellationToken cancellationToken = default) + { + var args = new List() + { + "plugin", + "rm", + kind.ToString().ToLower(), + }; + + if (!string.IsNullOrWhiteSpace(name)) + args.Add(name); + + if (!string.IsNullOrWhiteSpace(versionRange)) + args.Add(versionRange); + + args.Add("--yes"); + return this.RunCommandAsync(args, cancellationToken); + } + + /// + public override async Task> ListPluginsAsync(CancellationToken cancellationToken = default) + { + var result = await this.RunCommandAsync(new[] { "plugin", "ls", "--json" }, cancellationToken).ConfigureAwait(false); + var plugins = this._serializer.DeserializeJson>(result.StandardOutput); + return plugins.ToImmutableList(); + } + + public override void Dispose() + { + base.Dispose(); + + if (this._ownsWorkingDir + && !string.IsNullOrWhiteSpace(this.WorkDir) + && Directory.Exists(this.WorkDir)) + { + try + { + Directory.Delete(this.WorkDir, true); + } + catch + { + // allow graceful exit if for some reason + // we're not able to delete the directory + // will rely on OS to clean temp directory + // in this case. + } + } + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/LocalWorkspaceOptions.cs b/sdk/dotnet/Pulumi.Automation/LocalWorkspaceOptions.cs new file mode 100644 index 000000000..bbafc71d0 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/LocalWorkspaceOptions.cs @@ -0,0 +1,61 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System.Collections.Generic; + +namespace Pulumi.Automation +{ + /// + /// Extensibility options to configure a LocalWorkspace; e.g: settings to seed + /// and environment variables to pass through to every command. + /// + public class LocalWorkspaceOptions + { + /// + /// The directory to run Pulumi commands and read settings (Pulumi.yaml and Pulumi.{stack}.yaml). + /// + public string? WorkDir { get; set; } + + /// + /// The directory to override for CLI metadata. + /// + public string? PulumiHome { get; set; } + + /// + /// The secrets provider to user for encryption and decryption of stack secrets. + /// + /// See: https://www.pulumi.com/docs/intro/concepts/config/#available-encryption-providers + /// + public string? SecretsProvider { get; set; } + + /// + /// The inline program to be used for Preview/Update operations if any. + /// + /// If none is specified, the stack will refer to for this information. + /// + public PulumiFn? Program { get; set; } + + /// + /// Environment values scoped to the current workspace. These will be supplied to every + /// Pulumi command. + /// + public IDictionary? EnvironmentVariables { get; set; } + + /// + /// The settings object for the current project. + /// + /// If provided when initializing a project settings + /// file will be written to when the workspace is initialized via + /// . + /// + public ProjectSettings? ProjectSettings { get; set; } + + /// + /// A map of Stack names and corresponding settings objects. + /// + /// If provided when initializing stack settings + /// file(s) will be written to when the workspace is initialized via + /// . + /// + public IDictionary? StackSettings { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/OperationType.cs b/sdk/dotnet/Pulumi.Automation/OperationType.cs new file mode 100644 index 000000000..7740fa11c --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/OperationType.cs @@ -0,0 +1,15 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + public enum OperationType + { + Same, + Create, + Update, + Delete, + Replace, + CreateReplacement, + DeleteReplaced, + } +} diff --git a/sdk/dotnet/Pulumi.Automation/OutputValue.cs b/sdk/dotnet/Pulumi.Automation/OutputValue.cs new file mode 100644 index 000000000..84d5e77f1 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/OutputValue.cs @@ -0,0 +1,19 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + public sealed class OutputValue + { + public object Value { get; } + + public bool IsSecret { get; } + + internal OutputValue( + object value, + bool isSecret) + { + this.Value = value; + this.IsSecret = isSecret; + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/PluginInfo.cs b/sdk/dotnet/Pulumi.Automation/PluginInfo.cs new file mode 100644 index 000000000..f907bf828 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/PluginInfo.cs @@ -0,0 +1,45 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; + +namespace Pulumi.Automation +{ + public class PluginInfo + { + public string Name { get; } + + public string? Path { get; } + + public PluginKind Kind { get; } + + public string? Version { get; } + + public long Size { get; } + + public DateTimeOffset InstallTime { get; } + + public DateTimeOffset LastUsedTime { get; } + + public string? ServerUrl { get; } + + internal PluginInfo( + string name, + string? path, + PluginKind kind, + string? version, + long size, + DateTimeOffset installTime, + DateTimeOffset lastUsedTime, + string? serverUrl) + { + this.Name = name; + this.Path = path; + this.Kind = kind; + this.Version = version; + this.Size = size; + this.InstallTime = installTime; + this.LastUsedTime = lastUsedTime; + this.ServerUrl = serverUrl; + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/PluginKind.cs b/sdk/dotnet/Pulumi.Automation/PluginKind.cs new file mode 100644 index 000000000..5ecee37fb --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/PluginKind.cs @@ -0,0 +1,11 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + public enum PluginKind + { + Analyzer, + Language, + Resource, + } +} diff --git a/sdk/dotnet/Pulumi.Automation/PreviewOptions.cs b/sdk/dotnet/Pulumi.Automation/PreviewOptions.cs new file mode 100644 index 000000000..c4fd3fd39 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/PreviewOptions.cs @@ -0,0 +1,20 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System.Collections.Generic; + +namespace Pulumi.Automation +{ + /// + /// Options controlling the behavior of an operation. + /// + public sealed class PreviewOptions : UpdateOptions + { + public bool? ExpectNoChanges { get; set; } + + public List? Replace { get; set; } + + public bool? TargetDependents { get; set; } + + public PulumiFn? Program { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/ProjectBackend.cs b/sdk/dotnet/Pulumi.Automation/ProjectBackend.cs new file mode 100644 index 000000000..c1efa4063 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/ProjectBackend.cs @@ -0,0 +1,12 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + /// + /// Configuration for the project's Pulumi state storage backend. + /// + public class ProjectBackend + { + public string? Url { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/ProjectRuntime.cs b/sdk/dotnet/Pulumi.Automation/ProjectRuntime.cs new file mode 100644 index 000000000..9f23a2b45 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/ProjectRuntime.cs @@ -0,0 +1,19 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + /// + /// A description of the Project's program runtime and associated metadata. + /// + public class ProjectRuntime + { + public ProjectRuntimeName Name { get; set; } + + public ProjectRuntimeOptions? Options { get; set; } + + public ProjectRuntime(ProjectRuntimeName name) + { + this.Name = name; + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/ProjectRuntimeName.cs b/sdk/dotnet/Pulumi.Automation/ProjectRuntimeName.cs new file mode 100644 index 000000000..36ca98a1c --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/ProjectRuntimeName.cs @@ -0,0 +1,15 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + /// + /// Supported Pulumi program language runtimes. + /// + public enum ProjectRuntimeName + { + NodeJS, + Go, + Python, + Dotnet, + } +} diff --git a/sdk/dotnet/Pulumi.Automation/ProjectRuntimeOptions.cs b/sdk/dotnet/Pulumi.Automation/ProjectRuntimeOptions.cs new file mode 100644 index 000000000..9405eb74e --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/ProjectRuntimeOptions.cs @@ -0,0 +1,33 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + /// + /// Various configuration options that apply to different language runtimes. + /// + public class ProjectRuntimeOptions + { + /// + /// Applies to NodeJS projects only. + /// + /// A boolean that controls whether to use ts-node to execute sources. + /// + public bool? TypeScript { get; set; } + + /// + /// Applies to Go and .NET project only. + /// + /// Go: A string that specifies the name of a pre-build executable to look for on your path. + /// + /// .NET: A string that specifies the path of a pre-build .NET assembly. + /// + public string? Binary { get; set; } + + /// + /// Applies to Python projects only. + /// + /// A string that specifies the path to a virtual environment to use when running the program. + /// + public string? VirtualEnv { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/ProjectSettings.cs b/sdk/dotnet/Pulumi.Automation/ProjectSettings.cs new file mode 100644 index 000000000..67c3b41c9 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/ProjectSettings.cs @@ -0,0 +1,48 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + /// + /// A Pulumi project manifest. It describes metadata applying to all sub-stacks created from the project. + /// + public class ProjectSettings + { + public string Name { get; set; } + + public ProjectRuntime Runtime { get; set; } + + public string? Main { get; set; } + + public string? Description { get; set; } + + public string? Author { get; set; } + + public string? Website { get; set; } + + public string? License { get; set; } + + public string? Config { get; set; } + + public ProjectTemplate? Template { get; set; } + + public ProjectBackend? Backend { get; set; } + + public ProjectSettings( + string name, + ProjectRuntime runtime) + { + this.Name = name; + this.Runtime = runtime; + } + + public ProjectSettings( + string name, + ProjectRuntimeName runtime) + : this(name, new ProjectRuntime(runtime)) + { + } + + internal static ProjectSettings Default(string name) + => new ProjectSettings(name, new ProjectRuntime(ProjectRuntimeName.NodeJS)); + } +} diff --git a/sdk/dotnet/Pulumi.Automation/ProjectTemplate.cs b/sdk/dotnet/Pulumi.Automation/ProjectTemplate.cs new file mode 100644 index 000000000..d8a58f6a3 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/ProjectTemplate.cs @@ -0,0 +1,20 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System.Collections.Generic; + +namespace Pulumi.Automation +{ + /// + /// A template used to seed new stacks created from this project. + /// + public class ProjectTemplate + { + public string? Description { get; set; } + + public string? QuickStart { get; set; } + + public IDictionary? Config { get; set; } + + public bool? Important { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/ProjectTemplateConfigValue.cs b/sdk/dotnet/Pulumi.Automation/ProjectTemplateConfigValue.cs new file mode 100644 index 000000000..8a63fffcd --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/ProjectTemplateConfigValue.cs @@ -0,0 +1,16 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + /// + /// A placeholder config value for a project template. + /// + public class ProjectTemplateConfigValue + { + public string? Description { get; set; } + + public string? Default { get; set; } + + public bool? Secret { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/PublicAPI.Shipped.txt b/sdk/dotnet/Pulumi.Automation/PublicAPI.Shipped.txt new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/PublicAPI.Shipped.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/dotnet/Pulumi.Automation/PublicAPI.Unshipped.txt b/sdk/dotnet/Pulumi.Automation/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..3e9a89fc4 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/PublicAPI.Unshipped.txt @@ -0,0 +1,327 @@ +Pulumi.Automation.Commands.Exceptions.CommandException +Pulumi.Automation.Commands.Exceptions.CommandException.Name.get -> string +Pulumi.Automation.Commands.Exceptions.ConcurrentUpdateException +Pulumi.Automation.Commands.Exceptions.StackAlreadyExistsException +Pulumi.Automation.Commands.Exceptions.StackNotFoundException +Pulumi.Automation.ConfigValue +Pulumi.Automation.ConfigValue.ConfigValue(string value, bool isSecret = false) -> void +Pulumi.Automation.ConfigValue.IsSecret.get -> bool +Pulumi.Automation.ConfigValue.IsSecret.set -> void +Pulumi.Automation.ConfigValue.Value.get -> string +Pulumi.Automation.ConfigValue.Value.set -> void +Pulumi.Automation.DestroyOptions +Pulumi.Automation.DestroyOptions.DestroyOptions() -> void +Pulumi.Automation.DestroyOptions.OnOutput.get -> System.Action +Pulumi.Automation.DestroyOptions.OnOutput.set -> void +Pulumi.Automation.DestroyOptions.TargetDependents.get -> bool? +Pulumi.Automation.DestroyOptions.TargetDependents.set -> void +Pulumi.Automation.HistoryOptions +Pulumi.Automation.HistoryOptions.HistoryOptions() -> void +Pulumi.Automation.HistoryOptions.Page.get -> int? +Pulumi.Automation.HistoryOptions.Page.set -> void +Pulumi.Automation.HistoryOptions.PageSize.get -> int? +Pulumi.Automation.HistoryOptions.PageSize.set -> void +Pulumi.Automation.InlineProgramArgs +Pulumi.Automation.InlineProgramArgs.InlineProgramArgs(string projectName, string stackName, Pulumi.Automation.PulumiFn program) -> void +Pulumi.Automation.InlineProgramArgs.StackName.get -> string +Pulumi.Automation.LocalProgramArgs +Pulumi.Automation.LocalProgramArgs.LocalProgramArgs(string stackName, string workDir) -> void +Pulumi.Automation.LocalProgramArgs.StackName.get -> string +Pulumi.Automation.LocalWorkspace +Pulumi.Automation.LocalWorkspaceOptions +Pulumi.Automation.LocalWorkspaceOptions.EnvironmentVariables.get -> System.Collections.Generic.IDictionary +Pulumi.Automation.LocalWorkspaceOptions.EnvironmentVariables.set -> void +Pulumi.Automation.LocalWorkspaceOptions.LocalWorkspaceOptions() -> void +Pulumi.Automation.LocalWorkspaceOptions.Program.get -> Pulumi.Automation.PulumiFn +Pulumi.Automation.LocalWorkspaceOptions.Program.set -> void +Pulumi.Automation.LocalWorkspaceOptions.ProjectSettings.get -> Pulumi.Automation.ProjectSettings +Pulumi.Automation.LocalWorkspaceOptions.ProjectSettings.set -> void +Pulumi.Automation.LocalWorkspaceOptions.PulumiHome.get -> string +Pulumi.Automation.LocalWorkspaceOptions.PulumiHome.set -> void +Pulumi.Automation.LocalWorkspaceOptions.SecretsProvider.get -> string +Pulumi.Automation.LocalWorkspaceOptions.SecretsProvider.set -> void +Pulumi.Automation.LocalWorkspaceOptions.StackSettings.get -> System.Collections.Generic.IDictionary +Pulumi.Automation.LocalWorkspaceOptions.StackSettings.set -> void +Pulumi.Automation.LocalWorkspaceOptions.WorkDir.get -> string +Pulumi.Automation.LocalWorkspaceOptions.WorkDir.set -> void +Pulumi.Automation.OperationType +Pulumi.Automation.OperationType.Create = 1 -> Pulumi.Automation.OperationType +Pulumi.Automation.OperationType.CreateReplacement = 5 -> Pulumi.Automation.OperationType +Pulumi.Automation.OperationType.Delete = 3 -> Pulumi.Automation.OperationType +Pulumi.Automation.OperationType.DeleteReplaced = 6 -> Pulumi.Automation.OperationType +Pulumi.Automation.OperationType.Replace = 4 -> Pulumi.Automation.OperationType +Pulumi.Automation.OperationType.Same = 0 -> Pulumi.Automation.OperationType +Pulumi.Automation.OperationType.Update = 2 -> Pulumi.Automation.OperationType +Pulumi.Automation.OutputValue +Pulumi.Automation.OutputValue.IsSecret.get -> bool +Pulumi.Automation.OutputValue.Value.get -> object +Pulumi.Automation.PluginInfo +Pulumi.Automation.PluginInfo.InstallTime.get -> System.DateTimeOffset +Pulumi.Automation.PluginInfo.Kind.get -> Pulumi.Automation.PluginKind +Pulumi.Automation.PluginInfo.LastUsedTime.get -> System.DateTimeOffset +Pulumi.Automation.PluginInfo.Name.get -> string +Pulumi.Automation.PluginInfo.Path.get -> string +Pulumi.Automation.PluginInfo.ServerUrl.get -> string +Pulumi.Automation.PluginInfo.Size.get -> long +Pulumi.Automation.PluginInfo.Version.get -> string +Pulumi.Automation.PluginKind +Pulumi.Automation.PluginKind.Analyzer = 0 -> Pulumi.Automation.PluginKind +Pulumi.Automation.PluginKind.Language = 1 -> Pulumi.Automation.PluginKind +Pulumi.Automation.PluginKind.Resource = 2 -> Pulumi.Automation.PluginKind +Pulumi.Automation.PreviewOptions +Pulumi.Automation.PreviewOptions.ExpectNoChanges.get -> bool? +Pulumi.Automation.PreviewOptions.ExpectNoChanges.set -> void +Pulumi.Automation.PreviewOptions.PreviewOptions() -> void +Pulumi.Automation.PreviewOptions.Program.get -> Pulumi.Automation.PulumiFn +Pulumi.Automation.PreviewOptions.Program.set -> void +Pulumi.Automation.PreviewOptions.Replace.get -> System.Collections.Generic.List +Pulumi.Automation.PreviewOptions.Replace.set -> void +Pulumi.Automation.PreviewOptions.TargetDependents.get -> bool? +Pulumi.Automation.PreviewOptions.TargetDependents.set -> void +Pulumi.Automation.ProjectBackend +Pulumi.Automation.ProjectBackend.ProjectBackend() -> void +Pulumi.Automation.ProjectBackend.Url.get -> string +Pulumi.Automation.ProjectBackend.Url.set -> void +Pulumi.Automation.ProjectRuntime +Pulumi.Automation.ProjectRuntime.Name.get -> Pulumi.Automation.ProjectRuntimeName +Pulumi.Automation.ProjectRuntime.Name.set -> void +Pulumi.Automation.ProjectRuntime.Options.get -> Pulumi.Automation.ProjectRuntimeOptions +Pulumi.Automation.ProjectRuntime.Options.set -> void +Pulumi.Automation.ProjectRuntime.ProjectRuntime(Pulumi.Automation.ProjectRuntimeName name) -> void +Pulumi.Automation.ProjectRuntimeName +Pulumi.Automation.ProjectRuntimeName.Dotnet = 3 -> Pulumi.Automation.ProjectRuntimeName +Pulumi.Automation.ProjectRuntimeName.Go = 1 -> Pulumi.Automation.ProjectRuntimeName +Pulumi.Automation.ProjectRuntimeName.NodeJS = 0 -> Pulumi.Automation.ProjectRuntimeName +Pulumi.Automation.ProjectRuntimeName.Python = 2 -> Pulumi.Automation.ProjectRuntimeName +Pulumi.Automation.ProjectRuntimeOptions +Pulumi.Automation.ProjectRuntimeOptions.Binary.get -> string +Pulumi.Automation.ProjectRuntimeOptions.Binary.set -> void +Pulumi.Automation.ProjectRuntimeOptions.ProjectRuntimeOptions() -> void +Pulumi.Automation.ProjectRuntimeOptions.TypeScript.get -> bool? +Pulumi.Automation.ProjectRuntimeOptions.TypeScript.set -> void +Pulumi.Automation.ProjectRuntimeOptions.VirtualEnv.get -> string +Pulumi.Automation.ProjectRuntimeOptions.VirtualEnv.set -> void +Pulumi.Automation.ProjectSettings +Pulumi.Automation.ProjectSettings.Author.get -> string +Pulumi.Automation.ProjectSettings.Author.set -> void +Pulumi.Automation.ProjectSettings.Backend.get -> Pulumi.Automation.ProjectBackend +Pulumi.Automation.ProjectSettings.Backend.set -> void +Pulumi.Automation.ProjectSettings.Config.get -> string +Pulumi.Automation.ProjectSettings.Config.set -> void +Pulumi.Automation.ProjectSettings.Description.get -> string +Pulumi.Automation.ProjectSettings.Description.set -> void +Pulumi.Automation.ProjectSettings.License.get -> string +Pulumi.Automation.ProjectSettings.License.set -> void +Pulumi.Automation.ProjectSettings.Main.get -> string +Pulumi.Automation.ProjectSettings.Main.set -> void +Pulumi.Automation.ProjectSettings.Name.get -> string +Pulumi.Automation.ProjectSettings.Name.set -> void +Pulumi.Automation.ProjectSettings.ProjectSettings(string name, Pulumi.Automation.ProjectRuntime runtime) -> void +Pulumi.Automation.ProjectSettings.ProjectSettings(string name, Pulumi.Automation.ProjectRuntimeName runtime) -> void +Pulumi.Automation.ProjectSettings.Runtime.get -> Pulumi.Automation.ProjectRuntime +Pulumi.Automation.ProjectSettings.Runtime.set -> void +Pulumi.Automation.ProjectSettings.Template.get -> Pulumi.Automation.ProjectTemplate +Pulumi.Automation.ProjectSettings.Template.set -> void +Pulumi.Automation.ProjectSettings.Website.get -> string +Pulumi.Automation.ProjectSettings.Website.set -> void +Pulumi.Automation.ProjectTemplate +Pulumi.Automation.ProjectTemplate.Config.get -> System.Collections.Generic.IDictionary +Pulumi.Automation.ProjectTemplate.Config.set -> void +Pulumi.Automation.ProjectTemplate.Description.get -> string +Pulumi.Automation.ProjectTemplate.Description.set -> void +Pulumi.Automation.ProjectTemplate.Important.get -> bool? +Pulumi.Automation.ProjectTemplate.Important.set -> void +Pulumi.Automation.ProjectTemplate.ProjectTemplate() -> void +Pulumi.Automation.ProjectTemplate.QuickStart.get -> string +Pulumi.Automation.ProjectTemplate.QuickStart.set -> void +Pulumi.Automation.ProjectTemplateConfigValue +Pulumi.Automation.ProjectTemplateConfigValue.Default.get -> string +Pulumi.Automation.ProjectTemplateConfigValue.Default.set -> void +Pulumi.Automation.ProjectTemplateConfigValue.Description.get -> string +Pulumi.Automation.ProjectTemplateConfigValue.Description.set -> void +Pulumi.Automation.ProjectTemplateConfigValue.ProjectTemplateConfigValue() -> void +Pulumi.Automation.ProjectTemplateConfigValue.Secret.get -> bool? +Pulumi.Automation.ProjectTemplateConfigValue.Secret.set -> void +Pulumi.Automation.PulumiFn +Pulumi.Automation.RefreshOptions +Pulumi.Automation.RefreshOptions.ExpectNoChanges.get -> bool? +Pulumi.Automation.RefreshOptions.ExpectNoChanges.set -> void +Pulumi.Automation.RefreshOptions.OnOutput.get -> System.Action +Pulumi.Automation.RefreshOptions.OnOutput.set -> void +Pulumi.Automation.RefreshOptions.RefreshOptions() -> void +Pulumi.Automation.StackSettings +Pulumi.Automation.StackSettings.Config.get -> System.Collections.Generic.IDictionary +Pulumi.Automation.StackSettings.Config.set -> void +Pulumi.Automation.StackSettings.EncryptedKey.get -> string +Pulumi.Automation.StackSettings.EncryptedKey.set -> void +Pulumi.Automation.StackSettings.EncryptionSalt.get -> string +Pulumi.Automation.StackSettings.EncryptionSalt.set -> void +Pulumi.Automation.StackSettings.SecretsProvider.get -> string +Pulumi.Automation.StackSettings.SecretsProvider.set -> void +Pulumi.Automation.StackSettings.StackSettings() -> void +Pulumi.Automation.StackSettingsConfigValue +Pulumi.Automation.StackSettingsConfigValue.IsSecure.get -> bool +Pulumi.Automation.StackSettingsConfigValue.StackSettingsConfigValue(string value, bool isSecure) -> void +Pulumi.Automation.StackSettingsConfigValue.Value.get -> string +Pulumi.Automation.StackSummary +Pulumi.Automation.StackSummary.IsCurrent.get -> bool +Pulumi.Automation.StackSummary.IsUpdateInProgress.get -> bool +Pulumi.Automation.StackSummary.LastUpdate.get -> System.DateTimeOffset? +Pulumi.Automation.StackSummary.Name.get -> string +Pulumi.Automation.StackSummary.ResourceCount.get -> int? +Pulumi.Automation.StackSummary.Url.get -> string +Pulumi.Automation.UpOptions +Pulumi.Automation.UpOptions.ExpectNoChanges.get -> bool? +Pulumi.Automation.UpOptions.ExpectNoChanges.set -> void +Pulumi.Automation.UpOptions.OnOutput.get -> System.Action +Pulumi.Automation.UpOptions.OnOutput.set -> void +Pulumi.Automation.UpOptions.Program.get -> Pulumi.Automation.PulumiFn +Pulumi.Automation.UpOptions.Program.set -> void +Pulumi.Automation.UpOptions.Replace.get -> System.Collections.Generic.List +Pulumi.Automation.UpOptions.Replace.set -> void +Pulumi.Automation.UpOptions.TargetDependents.get -> bool? +Pulumi.Automation.UpOptions.TargetDependents.set -> void +Pulumi.Automation.UpOptions.UpOptions() -> void +Pulumi.Automation.UpResult +Pulumi.Automation.UpResult.Outputs.get -> System.Collections.Immutable.IImmutableDictionary +Pulumi.Automation.UpdateKind +Pulumi.Automation.UpdateKind.Destroy = 4 -> Pulumi.Automation.UpdateKind +Pulumi.Automation.UpdateKind.Import = 5 -> Pulumi.Automation.UpdateKind +Pulumi.Automation.UpdateKind.Preview = 1 -> Pulumi.Automation.UpdateKind +Pulumi.Automation.UpdateKind.Refresh = 2 -> Pulumi.Automation.UpdateKind +Pulumi.Automation.UpdateKind.Rename = 3 -> Pulumi.Automation.UpdateKind +Pulumi.Automation.UpdateKind.Update = 0 -> Pulumi.Automation.UpdateKind +Pulumi.Automation.UpdateOptions +Pulumi.Automation.UpdateOptions.Message.get -> string +Pulumi.Automation.UpdateOptions.Message.set -> void +Pulumi.Automation.UpdateOptions.Parallel.get -> int? +Pulumi.Automation.UpdateOptions.Parallel.set -> void +Pulumi.Automation.UpdateOptions.Target.get -> System.Collections.Generic.List +Pulumi.Automation.UpdateOptions.Target.set -> void +Pulumi.Automation.UpdateOptions.UpdateOptions() -> void +Pulumi.Automation.UpdateResult +Pulumi.Automation.UpdateResult.StandardError.get -> string +Pulumi.Automation.UpdateResult.StandardOutput.get -> string +Pulumi.Automation.UpdateResult.Summary.get -> Pulumi.Automation.UpdateSummary +Pulumi.Automation.UpdateState +Pulumi.Automation.UpdateState.Failed = 3 -> Pulumi.Automation.UpdateState +Pulumi.Automation.UpdateState.InProgress = 1 -> Pulumi.Automation.UpdateState +Pulumi.Automation.UpdateState.NotStarted = 0 -> Pulumi.Automation.UpdateState +Pulumi.Automation.UpdateState.Succeeded = 2 -> Pulumi.Automation.UpdateState +Pulumi.Automation.UpdateSummary +Pulumi.Automation.UpdateSummary.Config.get -> System.Collections.Immutable.IImmutableDictionary +Pulumi.Automation.UpdateSummary.Deployment.get -> string +Pulumi.Automation.UpdateSummary.EndTime.get -> System.DateTimeOffset +Pulumi.Automation.UpdateSummary.Environment.get -> System.Collections.Immutable.IImmutableDictionary +Pulumi.Automation.UpdateSummary.Kind.get -> Pulumi.Automation.UpdateKind +Pulumi.Automation.UpdateSummary.Message.get -> string +Pulumi.Automation.UpdateSummary.ResourceChanges.get -> System.Collections.Immutable.IImmutableDictionary +Pulumi.Automation.UpdateSummary.Result.get -> Pulumi.Automation.UpdateState +Pulumi.Automation.UpdateSummary.StartTime.get -> System.DateTimeOffset +Pulumi.Automation.UpdateSummary.Version.get -> int? +Pulumi.Automation.WhoAmIResult +Pulumi.Automation.WhoAmIResult.User.get -> string +Pulumi.Automation.WhoAmIResult.WhoAmIResult(string user) -> void +Pulumi.Automation.Workspace +Pulumi.Automation.Workspace.CreateStackAsync(string stackName) -> System.Threading.Tasks.Task +Pulumi.Automation.Workspace.SelectStackAsync(string stackName) -> System.Threading.Tasks.Task +Pulumi.Automation.WorkspaceStack +Pulumi.Automation.WorkspaceStack.DestroyAsync(Pulumi.Automation.DestroyOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Pulumi.Automation.WorkspaceStack.Dispose() -> void +Pulumi.Automation.WorkspaceStack.GetConfigAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task> +Pulumi.Automation.WorkspaceStack.GetConfigValueAsync(string key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Pulumi.Automation.WorkspaceStack.GetHistoryAsync(Pulumi.Automation.HistoryOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task> +Pulumi.Automation.WorkspaceStack.GetInfoAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Pulumi.Automation.WorkspaceStack.Name.get -> string +Pulumi.Automation.WorkspaceStack.PreviewAsync(Pulumi.Automation.PreviewOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Pulumi.Automation.WorkspaceStack.RefreshAsync(Pulumi.Automation.RefreshOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Pulumi.Automation.WorkspaceStack.RefreshConfigAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task> +Pulumi.Automation.WorkspaceStack.RemoveConfigAsync(System.Collections.Generic.IEnumerable keys, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Pulumi.Automation.WorkspaceStack.RemoveConfigValueAsync(string key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Pulumi.Automation.WorkspaceStack.SetConfigAsync(System.Collections.Generic.IDictionary configMap, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Pulumi.Automation.WorkspaceStack.SetConfigValueAsync(string key, Pulumi.Automation.ConfigValue value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Pulumi.Automation.WorkspaceStack.UpAsync(Pulumi.Automation.UpOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Pulumi.Automation.WorkspaceStack.Workspace.get -> Pulumi.Automation.Workspace +abstract Pulumi.Automation.Workspace.CreateStackAsync(string stackName, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +abstract Pulumi.Automation.Workspace.EnvironmentVariables.get -> System.Collections.Generic.IDictionary +abstract Pulumi.Automation.Workspace.EnvironmentVariables.set -> void +abstract Pulumi.Automation.Workspace.GetConfigAsync(string stackName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task> +abstract Pulumi.Automation.Workspace.GetConfigValueAsync(string stackName, string key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +abstract Pulumi.Automation.Workspace.GetProjectSettingsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +abstract Pulumi.Automation.Workspace.GetStackSettingsAsync(string stackName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +abstract Pulumi.Automation.Workspace.InstallPluginAsync(string name, string version, Pulumi.Automation.PluginKind kind = Pulumi.Automation.PluginKind.Resource, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +abstract Pulumi.Automation.Workspace.ListPluginsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task> +abstract Pulumi.Automation.Workspace.ListStacksAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task> +abstract Pulumi.Automation.Workspace.PostCommandCallbackAsync(string stackName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +abstract Pulumi.Automation.Workspace.Program.get -> Pulumi.Automation.PulumiFn +abstract Pulumi.Automation.Workspace.Program.set -> void +abstract Pulumi.Automation.Workspace.PulumiHome.get -> string +abstract Pulumi.Automation.Workspace.RefreshConfigAsync(string stackName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task> +abstract Pulumi.Automation.Workspace.RemoveConfigAsync(string stackName, System.Collections.Generic.IEnumerable keys, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +abstract Pulumi.Automation.Workspace.RemoveConfigValueAsync(string stackName, string key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +abstract Pulumi.Automation.Workspace.RemovePluginAsync(string name = null, string versionRange = null, Pulumi.Automation.PluginKind kind = Pulumi.Automation.PluginKind.Resource, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +abstract Pulumi.Automation.Workspace.RemoveStackAsync(string stackName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +abstract Pulumi.Automation.Workspace.SaveProjectSettingsAsync(Pulumi.Automation.ProjectSettings settings, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +abstract Pulumi.Automation.Workspace.SaveStackSettingsAsync(string stackName, Pulumi.Automation.StackSettings settings, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +abstract Pulumi.Automation.Workspace.SecretsProvider.get -> string +abstract Pulumi.Automation.Workspace.SelectStackAsync(string stackName, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +abstract Pulumi.Automation.Workspace.SerializeArgsForOpAsync(string stackName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task> +abstract Pulumi.Automation.Workspace.SetConfigAsync(string stackName, System.Collections.Generic.IDictionary configMap, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +abstract Pulumi.Automation.Workspace.SetConfigValueAsync(string stackName, string key, Pulumi.Automation.ConfigValue value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +abstract Pulumi.Automation.Workspace.WhoAmIAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +abstract Pulumi.Automation.Workspace.WorkDir.get -> string +override Pulumi.Automation.LocalWorkspace.CreateStackAsync(string stackName, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +override Pulumi.Automation.LocalWorkspace.Dispose() -> void +override Pulumi.Automation.LocalWorkspace.EnvironmentVariables.get -> System.Collections.Generic.IDictionary +override Pulumi.Automation.LocalWorkspace.EnvironmentVariables.set -> void +override Pulumi.Automation.LocalWorkspace.GetConfigAsync(string stackName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task> +override Pulumi.Automation.LocalWorkspace.GetConfigValueAsync(string stackName, string key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Pulumi.Automation.LocalWorkspace.GetProjectSettingsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Pulumi.Automation.LocalWorkspace.GetStackSettingsAsync(string stackName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Pulumi.Automation.LocalWorkspace.InstallPluginAsync(string name, string version, Pulumi.Automation.PluginKind kind = Pulumi.Automation.PluginKind.Resource, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Pulumi.Automation.LocalWorkspace.ListPluginsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task> +override Pulumi.Automation.LocalWorkspace.ListStacksAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task> +override Pulumi.Automation.LocalWorkspace.PostCommandCallbackAsync(string stackName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Pulumi.Automation.LocalWorkspace.Program.get -> Pulumi.Automation.PulumiFn +override Pulumi.Automation.LocalWorkspace.Program.set -> void +override Pulumi.Automation.LocalWorkspace.PulumiHome.get -> string +override Pulumi.Automation.LocalWorkspace.RefreshConfigAsync(string stackName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task> +override Pulumi.Automation.LocalWorkspace.RemoveConfigAsync(string stackName, System.Collections.Generic.IEnumerable keys, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Pulumi.Automation.LocalWorkspace.RemoveConfigValueAsync(string stackName, string key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Pulumi.Automation.LocalWorkspace.RemovePluginAsync(string name = null, string versionRange = null, Pulumi.Automation.PluginKind kind = Pulumi.Automation.PluginKind.Resource, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Pulumi.Automation.LocalWorkspace.RemoveStackAsync(string stackName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Pulumi.Automation.LocalWorkspace.SaveProjectSettingsAsync(Pulumi.Automation.ProjectSettings settings, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Pulumi.Automation.LocalWorkspace.SaveStackSettingsAsync(string stackName, Pulumi.Automation.StackSettings settings, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Pulumi.Automation.LocalWorkspace.SecretsProvider.get -> string +override Pulumi.Automation.LocalWorkspace.SelectStackAsync(string stackName, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +override Pulumi.Automation.LocalWorkspace.SerializeArgsForOpAsync(string stackName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task> +override Pulumi.Automation.LocalWorkspace.SetConfigAsync(string stackName, System.Collections.Generic.IDictionary configMap, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Pulumi.Automation.LocalWorkspace.SetConfigValueAsync(string stackName, string key, Pulumi.Automation.ConfigValue value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Pulumi.Automation.LocalWorkspace.WhoAmIAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Pulumi.Automation.LocalWorkspace.WorkDir.get -> string +static Pulumi.Automation.LocalWorkspace.CreateAsync(Pulumi.Automation.LocalWorkspaceOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +static Pulumi.Automation.LocalWorkspace.CreateOrSelectStackAsync(Pulumi.Automation.InlineProgramArgs args) -> System.Threading.Tasks.Task +static Pulumi.Automation.LocalWorkspace.CreateOrSelectStackAsync(Pulumi.Automation.InlineProgramArgs args, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +static Pulumi.Automation.LocalWorkspace.CreateOrSelectStackAsync(Pulumi.Automation.LocalProgramArgs args) -> System.Threading.Tasks.Task +static Pulumi.Automation.LocalWorkspace.CreateOrSelectStackAsync(Pulumi.Automation.LocalProgramArgs args, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +static Pulumi.Automation.LocalWorkspace.CreateStackAsync(Pulumi.Automation.InlineProgramArgs args) -> System.Threading.Tasks.Task +static Pulumi.Automation.LocalWorkspace.CreateStackAsync(Pulumi.Automation.InlineProgramArgs args, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +static Pulumi.Automation.LocalWorkspace.CreateStackAsync(Pulumi.Automation.LocalProgramArgs args) -> System.Threading.Tasks.Task +static Pulumi.Automation.LocalWorkspace.CreateStackAsync(Pulumi.Automation.LocalProgramArgs args, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +static Pulumi.Automation.LocalWorkspace.SelectStackAsync(Pulumi.Automation.InlineProgramArgs args) -> System.Threading.Tasks.Task +static Pulumi.Automation.LocalWorkspace.SelectStackAsync(Pulumi.Automation.InlineProgramArgs args, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +static Pulumi.Automation.LocalWorkspace.SelectStackAsync(Pulumi.Automation.LocalProgramArgs args) -> System.Threading.Tasks.Task +static Pulumi.Automation.LocalWorkspace.SelectStackAsync(Pulumi.Automation.LocalProgramArgs args, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +static Pulumi.Automation.PulumiFn.Create(System.Action program) -> Pulumi.Automation.PulumiFn +static Pulumi.Automation.PulumiFn.Create(System.Func> program) -> Pulumi.Automation.PulumiFn +static Pulumi.Automation.PulumiFn.Create(System.Func>> program) -> Pulumi.Automation.PulumiFn +static Pulumi.Automation.PulumiFn.Create(System.Func program) -> Pulumi.Automation.PulumiFn +static Pulumi.Automation.PulumiFn.Create(System.Func>> program) -> Pulumi.Automation.PulumiFn +static Pulumi.Automation.PulumiFn.Create(System.Func program) -> Pulumi.Automation.PulumiFn +static Pulumi.Automation.PulumiFn.Create() -> Pulumi.Automation.PulumiFn +static Pulumi.Automation.PulumiFn.Create(System.IServiceProvider serviceProvider) -> Pulumi.Automation.PulumiFn +static Pulumi.Automation.WorkspaceStack.CreateAsync(string name, Pulumi.Automation.Workspace workspace, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +static Pulumi.Automation.WorkspaceStack.CreateOrSelectAsync(string name, Pulumi.Automation.Workspace workspace, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +static Pulumi.Automation.WorkspaceStack.SelectAsync(string name, Pulumi.Automation.Workspace workspace, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +virtual Pulumi.Automation.Workspace.Dispose() -> void +virtual Pulumi.Automation.Workspace.GetStackAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task \ No newline at end of file diff --git a/sdk/dotnet/Pulumi.Automation/Pulumi.Automation.csproj b/sdk/dotnet/Pulumi.Automation/Pulumi.Automation.csproj new file mode 100644 index 000000000..af0b4a67b --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Pulumi.Automation.csproj @@ -0,0 +1,64 @@ + + + + netcoreapp3.1 + enable + false + false + Pulumi + Pulumi Corp. + Pulumi Automation API, the programmatic interface for driving Pulumi programs without the CLI. + https://www.pulumi.com + https://github.com/pulumi/pulumi + Apache-2.0 + pulumi_logo_64x64.png + true + + + + .\Pulumi.Automation.xml + 1701;1702;1591;NU5105 + + + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + true + true + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + True + + + + + + + + + diff --git a/sdk/dotnet/Pulumi.Automation/Pulumi.Automation.xml b/sdk/dotnet/Pulumi.Automation/Pulumi.Automation.xml new file mode 100644 index 000000000..e7ef44f88 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Pulumi.Automation.xml @@ -0,0 +1,881 @@ + + + + Pulumi.Automation + + + + + Options controlling the behavior of an operation. + + + + + Options controlling the behavior of a operation. + + + + + Description of a stack backed by pre-existing local Pulumi CLI program. + + + + + LocalWorkspace is a default implementation of the Workspace interface. + + A Workspace is the execution context containing a single Pulumi project, a program, + and multiple stacks.Workspaces are used to manage the execution environment, + providing various utilities such as plugin installation, environment configuration + ($PULUMI_HOME), and creation, deletion, and listing of Stacks. + + LocalWorkspace relies on Pulumi.yaml and Pulumi.{stack}.yaml as the intermediate format + for Project and Stack settings.Modifying ProjectSettings will + alter the Workspace Pulumi.yaml file, and setting config on a Stack will modify the Pulumi.{stack}.yaml file. + This is identical to the behavior of Pulumi CLI driven workspaces. + + If not provided a working directory - causing LocalWorkspace to create a temp directory, + than the temp directory will be cleaned up on . + + + + + + + + + + + + + + + + + + + + Creates a workspace using the specified options. Used for maximal control and + customization of the underlying environment before any stacks are created or selected. + + Options used to configure the workspace. + A cancellation token. + + + + Creates a Stack with a utilizing the specified + inline (in process) . This program + is fully debuggable and runs in process. If no + option is specified, default project settings will be created on behalf of the user. Similarly, unless a + option is specified, the working directory will default + to a new temporary directory provided by the OS. + + + A set of arguments to initialize a Stack with an inline program + that runs in process, as well as any additional customizations to be applied to the + workspace. + + + + + Creates a Stack with a utilizing the specified + inline (in process) . This program + is fully debuggable and runs in process. If no + option is specified, default project settings will be created on behalf of the user. Similarly, unless a + option is specified, the working directory will default + to a new temporary directory provided by the OS. + + + A set of arguments to initialize a Stack with an inline program + that runs in process, as well as any additional customizations to be applied to the + workspace. + + A cancellation token. + + + + Creates a Stack with a utilizing the local Pulumi CLI program + from the specified . This is a way to create drivers + on top of pre-existing Pulumi programs. This Workspace will pick up any available Settings + files(Pulumi.yaml, Pulumi.{stack}.yaml). + + + A set of arguments to initialize a Stack with a pre-configured Pulumi CLI program that + already exists on disk, as well as any additional customizations to be applied to the + workspace. + + + + + Creates a Stack with a utilizing the local Pulumi CLI program + from the specified . This is a way to create drivers + on top of pre-existing Pulumi programs. This Workspace will pick up any available Settings + files(Pulumi.yaml, Pulumi.{stack}.yaml). + + + A set of arguments to initialize a Stack with a pre-configured Pulumi CLI program that + already exists on disk, as well as any additional customizations to be applied to the + workspace. + + A cancellation token. + + + + Selects an existing Stack with a utilizing the specified + inline (in process) . This program + is fully debuggable and runs in process. If no + option is specified, default project settings will be created on behalf of the user. Similarly, unless a + option is specified, the working directory will default + to a new temporary directory provided by the OS. + + + A set of arguments to initialize a Stack with an inline program + that runs in process, as well as any additional customizations to be applied to the + workspace. + + + + + Selects an existing Stack with a utilizing the specified + inline (in process) . This program + is fully debuggable and runs in process. If no + option is specified, default project settings will be created on behalf of the user. Similarly, unless a + option is specified, the working directory will default + to a new temporary directory provided by the OS. + + + A set of arguments to initialize a Stack with an inline program + that runs in process, as well as any additional customizations to be applied to the + workspace. + + A cancellation token. + + + + Selects an existing Stack with a utilizing the local Pulumi CLI program + from the specified . This is a way to create drivers + on top of pre-existing Pulumi programs. This Workspace will pick up any available Settings + files(Pulumi.yaml, Pulumi.{stack}.yaml). + + + A set of arguments to initialize a Stack with a pre-configured Pulumi CLI program that + already exists on disk, as well as any additional customizations to be applied to the + workspace. + + + + + Selects an existing Stack with a utilizing the local Pulumi CLI program + from the specified . This is a way to create drivers + on top of pre-existing Pulumi programs. This Workspace will pick up any available Settings + files(Pulumi.yaml, Pulumi.{stack}.yaml). + + + A set of arguments to initialize a Stack with a pre-configured Pulumi CLI program that + already exists on disk, as well as any additional customizations to be applied to the + workspace. + + A cancellation token. + + + + Creates or selects an existing Stack with a utilizing the specified + inline (in process) . This program + is fully debuggable and runs in process. If no + option is specified, default project settings will be created on behalf of the user. Similarly, unless a + option is specified, the working directory will default + to a new temporary directory provided by the OS. + + + A set of arguments to initialize a Stack with an inline program + that runs in process, as well as any additional customizations to be applied to the + workspace. + + + + + Creates or selects an existing Stack with a utilizing the specified + inline (in process) . This program + is fully debuggable and runs in process. If no + option is specified, default project settings will be created on behalf of the user. Similarly, unless a + option is specified, the working directory will default + to a new temporary directory provided by the OS. + + + A set of arguments to initialize a Stack with an inline program + that runs in process, as well as any additional customizations to be applied to the + workspace. + + A cancellation token. + + + + Creates or selects an existing Stack with a utilizing the local Pulumi CLI program + from the specified . This is a way to create drivers + on top of pre-existing Pulumi programs. This Workspace will pick up any available Settings + files(Pulumi.yaml, Pulumi.{stack}.yaml). + + + A set of arguments to initialize a Stack with a pre-configured Pulumi CLI program that + already exists on disk, as well as any additional customizations to be applied to the + workspace. + + + + + Creates or selects an existing Stack with a utilizing the local Pulumi CLI program + from the specified . This is a way to create drivers + on top of pre-existing Pulumi programs. This Workspace will pick up any available Settings + files(Pulumi.yaml, Pulumi.{stack}.yaml). + + + A set of arguments to initialize a Stack with a pre-configured Pulumi CLI program that + already exists on disk, as well as any additional customizations to be applied to the + workspace. + + A cancellation token. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Extensibility options to configure a LocalWorkspace; e.g: settings to seed + and environment variables to pass through to every command. + + + + + The directory to run Pulumi commands and read settings (Pulumi.yaml and Pulumi.{stack}.yaml). + + + + + The directory to override for CLI metadata. + + + + + The secrets provider to user for encryption and decryption of stack secrets. + + See: https://www.pulumi.com/docs/intro/concepts/config/#available-encryption-providers + + + + + The inline program to be used for Preview/Update operations if any. + + If none is specified, the stack will refer to for this information. + + + + + Environment values scoped to the current workspace. These will be supplied to every + Pulumi command. + + + + + The settings object for the current project. + + If provided when initializing a project settings + file will be written to when the workspace is initialized via + . + + + + + A map of Stack names and corresponding settings objects. + + If provided when initializing stack settings + file(s) will be written to when the workspace is initialized via + . + + + + + Options controlling the behavior of an operation. + + + + + Configuration for the project's Pulumi state storage backend. + + + + + A description of the Project's program runtime and associated metadata. + + + + + Supported Pulumi program language runtimes. + + + + + Various configuration options that apply to different language runtimes. + + + + + Applies to NodeJS projects only. + + A boolean that controls whether to use ts-node to execute sources. + + + + + Applies to Go and .NET project only. + + Go: A string that specifies the name of a pre-build executable to look for on your path. + + .NET: A string that specifies the path of a pre-build .NET assembly. + + + + + Applies to Python projects only. + + A string that specifies the path to a virtual environment to use when running the program. + + + + + A Pulumi project manifest. It describes metadata applying to all sub-stacks created from the project. + + + + + A template used to seed new stacks created from this project. + + + + + A placeholder config value for a project template. + + + + + A Pulumi program as an inline function (in process). + + + + + Creates an asynchronous inline (in process) pulumi program. + + An asynchronous pulumi program that takes in a and returns an output. + + + + Creates an asynchronous inline (in process) pulumi program. + + An asynchronous pulumi program that returns an output. + + + + Creates an asynchronous inline (in process) pulumi program. + + An asynchronous pulumi program that takes in a . + + + + Creates an asynchronous inline (in process) pulumi program. + + An asynchronous pulumi program. + + + + Creates an inline (in process) pulumi program. + + A pulumi program that returns an output. + + + + Creates an inline (in process) pulumi program. + + A pulumi program. + + + + Creates an inline (in process) pulumi program via a traditional implementation. + + The type. + + + + Creates an inline (in process) pulumi program via a traditional implementation. + + When invoked, a new stack instance will be resolved based + on the provided type parameter + using the . + + The type. + + + + Options controlling the behavior of an operation. + + + + + This stack's secrets provider. + + + + + This is the KMS-encrypted ciphertext for the data key used for secrets + encryption. Only used for cloud-based secrets providers. + + + + + This is this stack's base64 encoded encryption salt. Only used for + passphrase-based secrets providers. + + + + + This is an optional configuration bag. + + + + + Common options controlling the behavior of update actions taken + against an instance of . + + + + + Options controlling the behavior of an operation. + + + + + Workspace is the execution context containing a single Pulumi project, a program, and multiple stacks. + + Workspaces are used to manage the execution environment, providing various utilities such as plugin + installation, environment configuration ($PULUMI_HOME), and creation, deletion, and listing of Stacks. + + + + + The working directory to run Pulumi CLI commands. + + + + + The directory override for CLI metadata if set. + + This customizes the location of $PULUMI_HOME where metadata is stored and plugins are installed. + + + + + The secrets provider to use for encryption and decryption of stack secrets. + + See: https://www.pulumi.com/docs/intro/concepts/config/#available-encryption-providers + + + + + The inline program to be used for Preview/Update operations if any. + + If none is specified, the stack will refer to for this information. + + + + + Environment values scoped to the current workspace. These will be supplied to every Pulumi command. + + + + + Returns project settings for the current project if any. + + + + + Overwrites the settings for the current project. + + There can only be a single project per workspace. Fails if new project name does not match old. + + The settings object to save. + A cancellation token. + + + + Returns stack settings for the stack matching the specified stack name if any. + + The name of the stack. + A cancellation token. + + + + Overwrite the settings for the stack matching the specified stack name. + + The name of the stack to operate on. + The settings object to save. + A cancellation token. + + + + Hook to provide additional args to every CLI command before they are executed. + + Provided with a stack name, returns an array of args to append to an invoked command ["--config=...", ]. + + does not utilize this extensibility point. + + The name of the stack. + A cancellation token. + + + + Hook executed after every command. Called with the stack name. + + An extensibility point to perform workspace cleanup (CLI operations may create/modify a Pulumi.stack.yaml). + + does not utilize this extensibility point. + + The name of the stack. + A cancellation token. + + + + Returns the value associated with the specified stack name and key, scoped + to the Workspace. + + The name of the stack to read config from. + The key to use for the config lookup. + A cancellation token. + + + + Returns the config map for the specified stack name, scoped to the current Workspace. + + The name of the stack to read config from. + A cancellation token. + + + + Sets the specified key-value pair in the provided stack's config. + + The name of the stack to operate on. + The config key to set. + The config value to set. + A cancellation token. + + + + Sets all values in the provided config map for the specified stack name. + + The name of the stack to operate on. + The config map to upsert against the existing config. + A cancellation token. + + + + Removes the specified key-value pair from the provided stack's config. + + The name of the stack to operate on. + The config key to remove. + A cancellation token. + + + + Removes all values in the provided key collection from the config map for the specified stack name. + + The name of the stack to operate on. + The collection of keys to remove from the underlying config map. + A cancellation token. + + + + Gets and sets the config map used with the last update for the stack matching the specified stack name. + + The name of the stack to operate on. + A cancellation token. + + + + Returns the currently authenticated user. + + + + + Returns a summary of the currently selected stack, if any. + + + + + Creates and sets a new stack with the specified stack name, failing if one already exists. + + The stack to create. + + + + Creates and sets a new stack with the specified stack name, failing if one already exists. + + The stack to create. + A cancellation token. + If a stack already exists by the provided name. + + + + Selects and sets an existing stack matching the stack name, failing if none exists. + + The stack to select. + If no stack was found by the provided name. + + + + Selects and sets an existing stack matching the stack name, failing if none exists. + + The stack to select. + A cancellation token. + + + + Deletes the stack and all associated configuration and history. + + The stack to remove. + A cancellation token. + + + + Returns all stacks created under the current project. + + This queries underlying backend and may return stacks not present in the Workspace (as Pulumi.{stack}.yaml files). + + + + + Installs a plugin in the Workspace, for example to use cloud providers like AWS or GCP. + + The name of the plugin. + The version of the plugin e.g. "v1.0.0". + The kind of plugin e.g. "resource". + A cancellation token. + + + + Removes a plugin from the Workspace matching the specified name and version. + + The optional name of the plugin. + The optional semver range to check when removing plugins matching the given name e.g. "1.0.0", ">1.0.0". + The kind of plugin e.g. "resource". + A cancellation token. + + + + Returns a list of all plugins installed in the Workspace. + + + + + is an isolated, independently configurable instance of a + Pulumi program. exposes methods for the full pulumi lifecycle + (up/preview/refresh/destroy), as well as managing configuration. + + Multiple stacks are commonly used to denote different phases of development + (such as development, staging, and production) or feature branches (such as + feature-x-dev, jane-feature-x-dev). + + Will dispose the on . + + + + + The name identifying the Stack. + + + + + The Workspace the Stack was created from. + + + + + Creates a new stack using the given workspace, and stack name. + It fails if a stack with that name already exists. + + The name identifying the stack. + The Workspace the Stack was created from. + A cancellation token. + If a stack with the provided name already exists. + + + + Selects stack using the given workspace, and stack name. + It returns an error if the given Stack does not exist. + + The name identifying the stack. + The Workspace the Stack was created from. + A cancellation token. + If a stack with the provided name does not exists. + + + + Tries to create a new Stack using the given workspace, and stack name + if the stack does not already exist, or falls back to selecting an + existing stack. If the stack does not exist, it will be created and + selected. + + The name of the identifying stack. + The Workspace the Stack was created from. + A cancellation token. + + + + Returns the config value associated with the specified key. + + The key to use for the config lookup. + A cancellation token. + + + + Returns the full config map associated with the stack in the Workspace. + + A cancellation token. + + + + Sets the config key-value pair on the Stack in the associated Workspace. + + The key to set. + The config value to set. + A cancellation token. + + + + Sets all specified config values on the stack in the associated Workspace. + + The map of config key-value pairs to set. + A cancellation token. + + + + Removes the specified config key from the Stack in the associated Workspace. + + The config key to remove. + A cancellation token. + + + + Removes the specified config keys from the Stack in the associated Workspace. + + The config keys to remove. + A cancellation token. + + + + Gets and sets the config map used with the last update. + + A cancellation token. + + + + Creates or updates the resources in a stack by executing the program in the Workspace. + + https://www.pulumi.com/docs/reference/cli/pulumi_up/ + + Options to customize the behavior of the update. + A cancellation token. + + + + Performs a dry-run update to a stack, returning pending changes. + + https://www.pulumi.com/docs/reference/cli/pulumi_preview/ + + Options to customize the behavior of the update. + A cancellation token. + + + + Compares the current stack’s resource state with the state known to exist in the actual + cloud provider. Any such changes are adopted into the current stack. + + Options to customize the behavior of the refresh. + A cancellation token. + + + + Destroy deletes all resources in a stack, leaving all history and configuration intact. + + Options to customize the behavior of the destroy. + A cancellation token. + + + + Gets the current set of Stack outputs from the last . + + + + + Returns a list summarizing all previews and current results from Stack lifecycle operations (up/preview/refresh/destroy). + + Options to customize the behavior of the fetch history action. + A cancellation token. + + + diff --git a/sdk/dotnet/Pulumi.Automation/PulumiFn.Inline.cs b/sdk/dotnet/Pulumi.Automation/PulumiFn.Inline.cs new file mode 100644 index 000000000..a4dee895d --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/PulumiFn.Inline.cs @@ -0,0 +1,41 @@ +// Copyright 2016-2021, Pulumi Corporation + + +using System; +using System.Collections.Generic; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Pulumi.Automation +{ + internal sealed class PulumiFnInline : PulumiFn + { + private readonly Func>> _program; + + public PulumiFnInline(Func>> program) + { + this._program = program; + } + + internal override async Task InvokeAsync(IRunner runner, CancellationToken cancellationToken) + { + ExceptionDispatchInfo? info = null; + + await runner.RunAsync(async () => + { + try + { + return await this._program(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + info = ExceptionDispatchInfo.Capture(ex); + throw; + } + }, null).ConfigureAwait(false); + + return info; + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/PulumiFn.TStack.cs b/sdk/dotnet/Pulumi.Automation/PulumiFn.TStack.cs new file mode 100644 index 000000000..896207756 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/PulumiFn.TStack.cs @@ -0,0 +1,52 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Pulumi.Automation +{ + internal class PulumiFn : PulumiFn where TStack : Pulumi.Stack + { + private readonly Func _stackFactory; + + public PulumiFn(Func stackFactory) + { + this._stackFactory = stackFactory; + } + + internal override async Task InvokeAsync(IRunner runner, CancellationToken cancellationToken) + { + ExceptionDispatchInfo? info = null; + + await runner.RunAsync(() => + { + try + { + return this._stackFactory(); + } + // because we are newing a generic, reflection comes in to + // construct the instance. And if there is an exception in + // the constructor of the user-provided TStack, it will be wrapped + // in TargetInvocationException - which is not the exception + // we want to throw to the consumer. + catch (TargetInvocationException ex) + { + info = ex.InnerException != null + ? ExceptionDispatchInfo.Capture(ex.InnerException) + : ExceptionDispatchInfo.Capture(ex); + throw; + } + catch (Exception ex) + { + info = ExceptionDispatchInfo.Capture(ex); + throw; + } + }).ConfigureAwait(false); + + return info; + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/PulumiFn.cs b/sdk/dotnet/Pulumi.Automation/PulumiFn.cs new file mode 100644 index 000000000..5876e88c5 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/PulumiFn.cs @@ -0,0 +1,123 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Pulumi.Automation +{ + /// + /// A Pulumi program as an inline function (in process). + /// + public abstract class PulumiFn + { + internal PulumiFn() + { + } + + internal abstract Task InvokeAsync(IRunner runner, CancellationToken cancellationToken); + + /// + /// Creates an asynchronous inline (in process) pulumi program. + /// + /// An asynchronous pulumi program that takes in a and returns an output. + public static PulumiFn Create(Func>> program) + => new PulumiFnInline(program); + + /// + /// Creates an asynchronous inline (in process) pulumi program. + /// + /// An asynchronous pulumi program that returns an output. + public static PulumiFn Create(Func>> program) + => new PulumiFnInline(cancellationToken => program()); + + /// + /// Creates an asynchronous inline (in process) pulumi program. + /// + /// An asynchronous pulumi program that takes in a . + public static PulumiFn Create(Func program) + { + Func>> wrapper = async cancellationToken => + { + await program(cancellationToken).ConfigureAwait(false); + return ImmutableDictionary.Empty; + }; + + return new PulumiFnInline(wrapper); + } + + /// + /// Creates an asynchronous inline (in process) pulumi program. + /// + /// An asynchronous pulumi program. + public static PulumiFn Create(Func program) + { + Func>> wrapper = async cancellationToken => + { + await program().ConfigureAwait(false); + return ImmutableDictionary.Empty; + }; + + return new PulumiFnInline(wrapper); + } + + /// + /// Creates an inline (in process) pulumi program. + /// + /// A pulumi program that returns an output. + public static PulumiFn Create(Func> program) + { + Func>> wrapper = cancellationToken => + { + var output = program(); + return Task.FromResult(output); + }; + + return new PulumiFnInline(wrapper); + } + + /// + /// Creates an inline (in process) pulumi program. + /// + /// A pulumi program. + public static PulumiFn Create(Action program) + => Create(() => { program(); return ImmutableDictionary.Empty; }); + + /// + /// Creates an inline (in process) pulumi program via a traditional implementation. + /// + /// The type. + public static PulumiFn Create() + where TStack : Pulumi.Stack, new() + => new PulumiFn(() => new TStack()); + + /// + /// Creates an inline (in process) pulumi program via a traditional implementation. + /// + /// When invoked, a new stack instance will be resolved based + /// on the provided type parameter + /// using the . + /// + /// The type. + public static PulumiFn Create(IServiceProvider serviceProvider) + where TStack : Pulumi.Stack + { + if (serviceProvider is null) + throw new ArgumentNullException(nameof(serviceProvider)); + + return new PulumiFn( + () => + { + if (serviceProvider is null) + throw new ArgumentNullException(nameof(serviceProvider), $"The provided service provider was null by the time this {nameof(PulumiFn)} was invoked."); + + return serviceProvider.GetService(typeof(TStack)) as TStack + ?? throw new ApplicationException( + $"Failed to resolve instance of type {typeof(TStack)} from service provider. Register the type with the service provider before this {nameof(PulumiFn)} is invoked."); + }); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/RefreshOptions.cs b/sdk/dotnet/Pulumi.Automation/RefreshOptions.cs new file mode 100644 index 000000000..2902f4981 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/RefreshOptions.cs @@ -0,0 +1,16 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; + +namespace Pulumi.Automation +{ + /// + /// Options controlling the behavior of an operation. + /// + public sealed class RefreshOptions : UpdateOptions + { + public bool? ExpectNoChanges { get; set; } + + public Action? OnOutput { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Runtime/LanguageRuntimeService.cs b/sdk/dotnet/Pulumi.Automation/Runtime/LanguageRuntimeService.cs new file mode 100644 index 000000000..0075fd91e --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Runtime/LanguageRuntimeService.cs @@ -0,0 +1,82 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; +using Pulumirpc; + +namespace Pulumi.Automation +{ + internal class LanguageRuntimeService : LanguageRuntime.LanguageRuntimeBase + { + // MaxRpcMesageSize raises the gRPC Max Message size from `4194304` (4mb) to `419430400` (400mb) + public const int MaxRpcMesageSize = 1024 * 1024 * 400; + + private readonly CallerContext _callerContext; + + public LanguageRuntimeService(CallerContext callerContext) + { + this._callerContext = callerContext; + } + + public override Task GetRequiredPlugins(GetRequiredPluginsRequest request, ServerCallContext context) + { + var response = new GetRequiredPluginsResponse(); + return Task.FromResult(response); + } + + public override async Task Run(RunRequest request, ServerCallContext context) + { + if (this._callerContext.CancellationToken.IsCancellationRequested // if caller of UpAsync has cancelled + || context.CancellationToken.IsCancellationRequested) // if CLI has cancelled + { + return new RunResponse(); + } + + var args = request.Args; + var engineAddr = args != null && args.Any() ? args[0] : ""; + + var settings = new InlineDeploymentSettings( + engineAddr, + request.MonitorAddress, + request.Config, + request.Project, + request.Stack, + request.Parallel, + request.DryRun); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource( + this._callerContext.CancellationToken, + context.CancellationToken); + + this._callerContext.ExceptionDispatchInfo = await Deployment.RunInlineAsync( + settings, + runner => this._callerContext.Program.InvokeAsync(runner, cts.Token)) + .ConfigureAwait(false); + + Deployment.Instance = null!; + return new RunResponse(); + } + + public class CallerContext + { + public PulumiFn Program { get; } + + public CancellationToken CancellationToken { get; } + + public ExceptionDispatchInfo? ExceptionDispatchInfo { get; set; } + + public CallerContext( + PulumiFn program, + CancellationToken cancellationToken) + { + this.Program = program; + this.CancellationToken = cancellationToken; + } + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/ConfigValueModel.cs b/sdk/dotnet/Pulumi.Automation/Serialization/ConfigValueModel.cs new file mode 100644 index 000000000..7ba6eed78 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/ConfigValueModel.cs @@ -0,0 +1,17 @@ +// Copyright 2016-2021, Pulumi Corporation + +using Pulumi.Automation.Serialization.Json; + +namespace Pulumi.Automation.Serialization +{ + // necessary for constructor deserialization + internal class ConfigValueModel : IJsonModel + { + public string Value { get; set; } = null!; + + public bool Secret { get; set; } + + public ConfigValue Convert() + => new ConfigValue(this.Value, this.Secret); + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/Json/IJsonModel.cs b/sdk/dotnet/Pulumi.Automation/Serialization/Json/IJsonModel.cs new file mode 100644 index 000000000..63bf94c80 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/Json/IJsonModel.cs @@ -0,0 +1,9 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation.Serialization.Json +{ + internal interface IJsonModel + { + T Convert(); + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/Json/LowercaseJsonNamingPolicy.cs b/sdk/dotnet/Pulumi.Automation/Serialization/Json/LowercaseJsonNamingPolicy.cs new file mode 100644 index 000000000..190586d80 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/Json/LowercaseJsonNamingPolicy.cs @@ -0,0 +1,12 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System.Text.Json; + +namespace Pulumi.Automation.Serialization.Json +{ + internal class LowercaseJsonNamingPolicy : JsonNamingPolicy + { + public override string ConvertName(string name) + => name.ToLower(); + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/Json/MapToModelJsonConverter.cs b/sdk/dotnet/Pulumi.Automation/Serialization/Json/MapToModelJsonConverter.cs new file mode 100644 index 000000000..1df4dfbd7 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/Json/MapToModelJsonConverter.cs @@ -0,0 +1,28 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Pulumi.Automation.Serialization.Json +{ + // necessary because this version of System.Text.Json + // can't deserialize a type that doesn't have a parameterless constructor + internal class MapToModelJsonConverter : JsonConverter + where TModel : IJsonModel + { + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var model = JsonSerializer.Deserialize(ref reader, options); + if (model is null) + throw new JsonException($"Unable to deserialize [{typeToConvert.FullName}]. Expecting object."); + + return model.Convert(); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, options); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/Json/ProjectRuntimeJsonConverter.cs b/sdk/dotnet/Pulumi.Automation/Serialization/Json/ProjectRuntimeJsonConverter.cs new file mode 100644 index 000000000..f264c78f4 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/Json/ProjectRuntimeJsonConverter.cs @@ -0,0 +1,75 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Pulumi.Automation.Serialization.Json +{ + internal class ProjectRuntimeJsonConverter : JsonConverter + { + public override ProjectRuntime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + var runtimeName = DeserializeName(ref reader, typeToConvert); + return new ProjectRuntime(runtimeName); + } + + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException($"Unable to deserialize [{typeToConvert.FullName}]. Expecting string or object."); + + reader.Read(); + if (reader.TokenType != JsonTokenType.PropertyName + || !string.Equals(nameof(ProjectRuntime.Name), reader.GetString(), StringComparison.OrdinalIgnoreCase)) + throw new JsonException($"Unable to deserialize [{typeToConvert.FullName}]. Expecting runtime name property."); + + reader.Read(); + if (reader.TokenType != JsonTokenType.String) + throw new JsonException($"Unable to deserialize [{typeToConvert.FullName}]. Runtime name property should be a string."); + + var name = DeserializeName(ref reader, typeToConvert); + + reader.Read(); + if (reader.TokenType == JsonTokenType.EndObject) + return new ProjectRuntime(name); + + if (reader.TokenType != JsonTokenType.PropertyName + || !string.Equals(nameof(ProjectRuntime.Options), reader.GetString(), StringComparison.OrdinalIgnoreCase)) + throw new JsonException($"Unable to deserialize [{typeToConvert.FullName}]. Expecting runtime options property."); + + reader.Read(); + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException($"Unable to deserialize [{typeToConvert.FullName}]. Runtime options property should be an object."); + + var runtimeOptions = JsonSerializer.Deserialize(ref reader, options); + reader.Read(); // read final EndObject token + + return new ProjectRuntime(name) { Options = runtimeOptions }; + + static ProjectRuntimeName DeserializeName(ref Utf8JsonReader reader, Type typeToConvert) + { + var runtimeStr = reader.GetString(); + if (string.IsNullOrWhiteSpace(runtimeStr)) + throw new JsonException($"A valid runtime name was not provided when deserializing [{typeToConvert.FullName}]."); + + if (Enum.TryParse(runtimeStr, true, out var runtimeName)) + return runtimeName; + + throw new JsonException($"Unexpected runtime name of \"{runtimeStr}\" provided when deserializing [{typeToConvert.FullName}]."); + } + } + + public override void Write(Utf8JsonWriter writer, ProjectRuntime value, JsonSerializerOptions options) + { + if (value.Options is null) + { + writer.WriteStringValue(value.Name.ToString().ToLower()); + } + else + { + JsonSerializer.Serialize(writer, value); + } + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/Json/ResourceChangesJsonConverter.cs b/sdk/dotnet/Pulumi.Automation/Serialization/Json/ResourceChangesJsonConverter.cs new file mode 100644 index 000000000..09ac954e4 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/Json/ResourceChangesJsonConverter.cs @@ -0,0 +1,71 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Pulumi.Automation.Serialization.Json +{ + internal class ResourceChangesJsonConverter : JsonConverter> + { + public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException($"Cannot deserialize [{typeToConvert.FullName}]. Expecing object."); + + var dictionary = new Dictionary(); + + reader.Read(); + while (reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType != JsonTokenType.PropertyName) + throw new JsonException("Expecting property name."); + + var propertyName = reader.GetString(); + if (string.IsNullOrWhiteSpace(propertyName)) + throw new JsonException("Unable to retrieve property name."); + + var operationType = ConvertToOperationType(propertyName); + + reader.Read(); + if (reader.TokenType != JsonTokenType.Number + || !reader.TryGetInt32(out var count)) + throw new JsonException("Expecting number."); + + dictionary[operationType] = count; + reader.Read(); + } + + return dictionary; + } + + public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + private static OperationType ConvertToOperationType(string opType) + { + switch (opType) + { + case "create": + return OperationType.Create; + case "create-replacement": + return OperationType.CreateReplacement; + case "delete": + return OperationType.Delete; + case "delete-replaced": + return OperationType.DeleteReplaced; + case "replace": + return OperationType.Replace; + case "same": + return OperationType.Same; + case "update": + return OperationType.Update; + default: + throw new JsonException($"Invalid operation type: {opType}"); + } + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/Json/StackSettingsConfigValueJsonConverter.cs b/sdk/dotnet/Pulumi.Automation/Serialization/Json/StackSettingsConfigValueJsonConverter.cs new file mode 100644 index 000000000..30e179719 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/Json/StackSettingsConfigValueJsonConverter.cs @@ -0,0 +1,57 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Pulumi.Automation.Serialization.Json +{ + internal class StackSettingsConfigValueJsonConverter : JsonConverter + { + public override StackSettingsConfigValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var element = JsonSerializer.Deserialize(ref reader, options); + + // check if plain string + if (element.ValueKind == JsonValueKind.String) + { + var value = element.GetString(); + return new StackSettingsConfigValue(value, false); + } + + // confirm object + if (element.ValueKind != JsonValueKind.Object) + throw new JsonException($"Unable to deserialize [{typeToConvert.FullName}]. Expecting object if not plain string."); + + // check if secure string + var securePropertyName = options.PropertyNamingPolicy?.ConvertName("Secure") ?? "Secure"; + if (element.TryGetProperty(securePropertyName, out var secureProperty)) + { + if (secureProperty.ValueKind != JsonValueKind.String) + throw new JsonException($"Unable to deserialize [{typeToConvert.FullName}] as a secure string. Expecting a string secret."); + + var secret = secureProperty.GetString(); + return new StackSettingsConfigValue(secret, true); + } + + throw new NotSupportedException("Automation API does not currently support deserializing complex objects from stack settings."); + } + + public override void Write(Utf8JsonWriter writer, StackSettingsConfigValue value, JsonSerializerOptions options) + { + // secure string + if (value.IsSecure) + { + var securePropertyName = options.PropertyNamingPolicy?.ConvertName("Secure") ?? "Secure"; + writer.WriteStartObject(); + writer.WriteString(securePropertyName, value.Value); + writer.WriteEndObject(); + } + // plain string + else + { + writer.WriteStringValue(value.Value); + } + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/Json/SystemObjectJsonConverter.cs b/sdk/dotnet/Pulumi.Automation/Serialization/Json/SystemObjectJsonConverter.cs new file mode 100644 index 000000000..a4a94f663 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/Json/SystemObjectJsonConverter.cs @@ -0,0 +1,80 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Pulumi.Automation.Serialization.Json +{ + internal class SystemObjectJsonConverter : JsonConverter + { + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.True) + { + return true; + } + + if (reader.TokenType == JsonTokenType.False) + { + return false; + } + + if (reader.TokenType == JsonTokenType.Number) + { + if (reader.TryGetInt64(out long l)) + { + return l; + } + + return reader.GetDouble(); + } + + if (reader.TokenType == JsonTokenType.String) + { + if (reader.TryGetDateTime(out DateTime datetime)) + { + return datetime; + } + + return reader.GetString(); + } + + if (reader.TokenType == JsonTokenType.StartArray) + { + return JsonSerializer.Deserialize(ref reader, options); + } + + if (reader.TokenType == JsonTokenType.StartObject) + { + var dictionary = new Dictionary(); + + reader.Read(); + while (reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType != JsonTokenType.PropertyName) + throw new JsonException("Expecting property name."); + + var propertyName = reader.GetString(); + if (string.IsNullOrWhiteSpace(propertyName)) + throw new JsonException("Unable to retrieve property name."); + + reader.Read(); + dictionary[propertyName] = JsonSerializer.Deserialize(ref reader, options); + + reader.Read(); + } + + return dictionary; + } + + throw new JsonException("Invalid JSON element."); + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + throw new NotSupportedException($"Writing as [{typeof(object).FullName}] is not supported."); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/LocalSerializer.cs b/sdk/dotnet/Pulumi.Automation/Serialization/LocalSerializer.cs new file mode 100644 index 000000000..da7d6b893 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/LocalSerializer.cs @@ -0,0 +1,81 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System.Text.Json; +using System.Text.Json.Serialization; +using Pulumi.Automation.Serialization.Json; +using Pulumi.Automation.Serialization.Yaml; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Pulumi.Automation.Serialization +{ + internal class LocalSerializer + { + private readonly JsonSerializerOptions _jsonOptions; + private readonly IDeserializer _yamlDeserializer; + private readonly ISerializer _yamlSerializer; + + public LocalSerializer() + { + // configure json + this._jsonOptions = BuildJsonSerializerOptions(); + + // configure yaml + this._yamlDeserializer = BuildYamlDeserializer(); + this._yamlSerializer = BuildYamlSerializer(); + } + + public T DeserializeJson(string content) + => JsonSerializer.Deserialize(content, this._jsonOptions); + + public T DeserializeYaml(string content) + where T : class + => this._yamlDeserializer.Deserialize(content); + + public string SerializeJson(T @object) + => JsonSerializer.Serialize(@object, this._jsonOptions); + + public string SerializeYaml(T @object) + where T : class + => this._yamlSerializer.Serialize(@object); + + public static JsonSerializerOptions BuildJsonSerializerOptions() + { + var options = new JsonSerializerOptions + { + AllowTrailingCommas = true, + IgnoreNullValues = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + options.Converters.Add(new JsonStringEnumConverter(new LowercaseJsonNamingPolicy())); + options.Converters.Add(new SystemObjectJsonConverter()); + options.Converters.Add(new MapToModelJsonConverter()); + options.Converters.Add(new MapToModelJsonConverter()); + options.Converters.Add(new MapToModelJsonConverter()); + options.Converters.Add(new MapToModelJsonConverter()); + options.Converters.Add(new MapToModelJsonConverter()); + options.Converters.Add(new ProjectRuntimeJsonConverter()); + options.Converters.Add(new ResourceChangesJsonConverter()); + options.Converters.Add(new StackSettingsConfigValueJsonConverter()); + + return options; + } + + public static IDeserializer BuildYamlDeserializer() + => new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .IgnoreUnmatchedProperties() + .WithTypeConverter(new ProjectRuntimeYamlConverter()) + .WithTypeConverter(new StackSettingsConfigValueYamlConverter()) + .Build(); + + public static ISerializer BuildYamlSerializer() + => new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull) + .WithTypeConverter(new ProjectRuntimeYamlConverter()) + .WithTypeConverter(new StackSettingsConfigValueYamlConverter()) + .Build(); + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/PluginInfoModel.cs b/sdk/dotnet/Pulumi.Automation/Serialization/PluginInfoModel.cs new file mode 100644 index 000000000..2d45432a7 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/PluginInfoModel.cs @@ -0,0 +1,44 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using Pulumi.Automation.Serialization.Json; + +namespace Pulumi.Automation.Serialization +{ + // necessary for constructor deserialization + internal class PluginInfoModel : IJsonModel + { + public string Name { get; set; } = null!; + + public string? Path { get; set; } + + public string Kind { get; set; } = null!; + + public string? Version { get; set; } + + public long Size { get; set; } + + public DateTimeOffset InstallTime { get; set; } + + public DateTimeOffset LastUsedTime { get; set; } + + public string? ServerUrl { get; set; } + + private PluginKind GetKind() + => string.Equals(this.Kind, "analyzer", StringComparison.OrdinalIgnoreCase) ? PluginKind.Analyzer + : string.Equals(this.Kind, "language", StringComparison.OrdinalIgnoreCase) ? PluginKind.Language + : string.Equals(this.Kind, "resource", StringComparison.OrdinalIgnoreCase) ? PluginKind.Resource + : throw new InvalidOperationException($"Invalid plugin kind: {this.Kind}"); + + public PluginInfo Convert() + => new PluginInfo( + this.Name, + this.Path, + this.GetKind(), + this.Version, + this.Size, + this.InstallTime, + this.LastUsedTime, + this.ServerUrl); + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/ProjectSettingsModel.cs b/sdk/dotnet/Pulumi.Automation/Serialization/ProjectSettingsModel.cs new file mode 100644 index 000000000..72e11cd4e --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/ProjectSettingsModel.cs @@ -0,0 +1,43 @@ +// Copyright 2016-2021, Pulumi Corporation + +using Pulumi.Automation.Serialization.Json; + +namespace Pulumi.Automation.Serialization +{ + // necessary for constructor deserialization + internal class ProjectSettingsModel : IJsonModel + { + public string? Name { get; set; } + + public ProjectRuntime? Runtime { get; set; } + + public string? Main { get; set; } + + public string? Description { get; set; } + + public string? Author { get; set; } + + public string? Website { get; set; } + + public string? License { get; set; } + + public string? Config { get; set; } + + public ProjectTemplate? Template { get; set; } + + public ProjectBackend? Backend { get; set; } + + public ProjectSettings Convert() + => new ProjectSettings(this.Name!, this.Runtime ?? new ProjectRuntime(ProjectRuntimeName.NodeJS)) + { + Main = this.Main, + Description = this.Description, + Author = this.Author, + Website = this.Website, + License = this.License, + Config = this.Config, + Template = this.Template, + Backend = this.Backend, + }; + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/StackSummaryModel.cs b/sdk/dotnet/Pulumi.Automation/Serialization/StackSummaryModel.cs new file mode 100644 index 000000000..a7d80d625 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/StackSummaryModel.cs @@ -0,0 +1,32 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using Pulumi.Automation.Serialization.Json; + +namespace Pulumi.Automation.Serialization +{ + // necessary for constructor deserialization + internal class StackSummaryModel : IJsonModel + { + public string Name { get; set; } = null!; + + public bool Current { get; set; } + + public DateTimeOffset? LastUpdate { get; set; } + + public bool UpdateInProgress { get; set; } + + public int? ResourceCount { get; set; } + + public string? Url { get; set; } + + public StackSummary Convert() + => new StackSummary( + this.Name, + this.Current, + this.LastUpdate, + this.UpdateInProgress, + this.ResourceCount, + this.Url); + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/UpdateSummaryModel.cs b/sdk/dotnet/Pulumi.Automation/Serialization/UpdateSummaryModel.cs new file mode 100644 index 000000000..f8c75aa5c --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/UpdateSummaryModel.cs @@ -0,0 +1,56 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using Pulumi.Automation.Serialization.Json; + +namespace Pulumi.Automation.Serialization +{ + // necessary for constructor deserialization + internal class UpdateSummaryModel : IJsonModel + { + // pre-update information + public UpdateKind Kind { get; set; } + + public DateTimeOffset StartTime { get; set; } + + public string? Message { get; set; } + + public Dictionary? Environment { get; set; } + + public Dictionary? Config { get; set; } + + // post-update information + public string? Result { get; set; } + + public DateTimeOffset EndTime { get; set; } + + public int? Version { get; set; } + + public string? Deployment { get; set; } + + public Dictionary? ResourceChanges { get; set; } + + private UpdateState GetResult() + => string.Equals(this.Result, "not-started", StringComparison.OrdinalIgnoreCase) ? UpdateState.NotStarted + : string.Equals(this.Result, "in-progress", StringComparison.OrdinalIgnoreCase) ? UpdateState.InProgress + : string.Equals(this.Result, "succeeded", StringComparison.OrdinalIgnoreCase) ? UpdateState.Succeeded + : string.Equals(this.Result, "failed", StringComparison.OrdinalIgnoreCase) ? UpdateState.Failed + : throw new InvalidOperationException($"Invalid update result: {this.Result}"); + + public UpdateSummary Convert() + { + return new UpdateSummary( + this.Kind, + this.StartTime, + this.Message ?? string.Empty, + this.Environment ?? new Dictionary(), + this.Config ?? new Dictionary(), + this.GetResult(), + this.EndTime, + this.Version, + this.Deployment, + this.ResourceChanges); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/Yaml/ProjectRuntimeOptionsYamlConverter.cs b/sdk/dotnet/Pulumi.Automation/Serialization/Yaml/ProjectRuntimeOptionsYamlConverter.cs new file mode 100644 index 000000000..21afd135c --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/Yaml/ProjectRuntimeOptionsYamlConverter.cs @@ -0,0 +1,91 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Linq; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Pulumi.Automation.Serialization.Yaml +{ + internal class ProjectRuntimeOptionsYamlConverter : IYamlTypeConverter + { + private static readonly Type Type = typeof(ProjectRuntimeOptions); + private static readonly List PropertyNames = typeof(ProjectRuntimeOptions).GetProperties().Select(x => x.Name).ToList(); + private static readonly Dictionary> Readers = + new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + [nameof(ProjectRuntimeOptions.Binary)] = (x, p, t) => x.Value, + [nameof(ProjectRuntimeOptions.TypeScript)] = (x, p, t) => x.ReadBoolean(p, t), + [nameof(ProjectRuntimeOptions.VirtualEnv)] = (x, p, t) => x.Value, + }; + + public bool Accepts(Type type) + => type == Type; + + public object? ReadYaml(IParser parser, Type type) + { + if (!parser.TryConsume(out _)) + throw new YamlException($"Unable to deserialize [{type.FullName}]. Expecting object."); + + var values = PropertyNames.ToDictionary(x => x, x => (object?)null, StringComparer.OrdinalIgnoreCase); + + do + { + if (!parser.TryConsume(out var propertyNameScalar)) + throw new YamlException($"Unable to deserialize [{type.FullName}]. Expecting a property name."); + + if (!Readers.TryGetValue(propertyNameScalar.Value, out var readerFunc)) + throw new YamlException($"Unable to deserialize [{type.FullName}]. Invalid property [{propertyNameScalar.Value}]."); + + if (!parser.TryConsume(out var propertyValueScalar)) + throw new YamlException($"Unable to deserialize [{type.FullName}]. Expecting a scalar value for [{propertyNameScalar.Value}]."); + + values[propertyNameScalar.Value] = readerFunc(propertyValueScalar, propertyNameScalar.Value, type); + } + while (!parser.Accept(out _)); + + parser.MoveNext(); // read final MappingEnd event + return new ProjectRuntimeOptions + { + Binary = (string?)values[nameof(ProjectRuntimeOptions.Binary)], + TypeScript = (bool?)values[nameof(ProjectRuntimeOptions.TypeScript)], + VirtualEnv = (string?)values[nameof(ProjectRuntimeOptions.VirtualEnv)], + }; + } + + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + if (!(value is ProjectRuntimeOptions options)) + return; + + if (string.IsNullOrWhiteSpace(options.Binary) + && options.TypeScript is null + && string.IsNullOrWhiteSpace(options.VirtualEnv)) + return; + + emitter.Emit(new MappingStart(null, null, false, MappingStyle.Block)); + + if (!string.IsNullOrWhiteSpace(options.Binary)) + { + emitter.Emit(new Scalar("binary")); + emitter.Emit(new Scalar(options.Binary)); + } + + if (options.TypeScript != null) + { + emitter.Emit(new Scalar("typescript")); + emitter.Emit(new Scalar(options.TypeScript.ToString()!.ToLower())); + } + + if (!string.IsNullOrWhiteSpace(options.VirtualEnv)) + { + emitter.Emit(new Scalar("virtualenv")); + emitter.Emit(new Scalar(options.VirtualEnv)); + } + + emitter.Emit(new MappingEnd()); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/Yaml/ProjectRuntimeYamlConverter.cs b/sdk/dotnet/Pulumi.Automation/Serialization/Yaml/ProjectRuntimeYamlConverter.cs new file mode 100644 index 000000000..9394826e6 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/Yaml/ProjectRuntimeYamlConverter.cs @@ -0,0 +1,96 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Pulumi.Automation.Serialization.Yaml +{ + internal class ProjectRuntimeYamlConverter : IYamlTypeConverter + { + private static readonly Type Type = typeof(ProjectRuntime); + private static readonly Type OptionsType = typeof(ProjectRuntimeOptions); + + private readonly IYamlTypeConverter _optionsConverter = new ProjectRuntimeOptionsYamlConverter(); + + public bool Accepts(Type type) + => type == Type; + + public object? ReadYaml(IParser parser, Type type) + { + if (parser.TryConsume(out var nameValueScalar)) + { + var runtimeName = DeserializeName(nameValueScalar, type); + return new ProjectRuntime(runtimeName); + } + + if (!parser.TryConsume(out _)) + throw new YamlException($"Unable to deserialize [{type.FullName}]. Expecting string or object."); + + if (!parser.TryConsume(out var namePropertyScalar) + || !string.Equals(nameof(ProjectRuntime.Name), namePropertyScalar.Value, StringComparison.OrdinalIgnoreCase)) + throw new YamlException($"Unable to deserialize [{type.FullName}]. Expecting runtime name property."); + + if (!parser.TryConsume(out var nameValueScalar2)) + throw new YamlException($"Unable to deserialize [{type.FullName}]. Runtime name property should be a string."); + + var name = DeserializeName(nameValueScalar2, type); + + // early mapping end is ok + if (parser.Accept(out _)) + { + parser.MoveNext(); // read final MappingEnd since Accept doesn't call MoveNext + return new ProjectRuntime(name); + } + + if (!parser.TryConsume(out var optionsPropertyScalar) + || !string.Equals(nameof(ProjectRuntime.Options), optionsPropertyScalar.Value, StringComparison.OrdinalIgnoreCase)) + throw new YamlException($"Unable to deserialize [{type.FullName}]. Expecting runtime options property."); + + if (!parser.Accept(out _)) + throw new YamlException($"Unable to deserialize [{type.FullName}]. Runtime options property should be an object."); + + var runtimeOptionsObj = this._optionsConverter.ReadYaml(parser, OptionsType); + if (!(runtimeOptionsObj is ProjectRuntimeOptions runtimeOptions)) + throw new YamlException("There was an issue deserializing the runtime options object."); + + parser.MoveNext(); // read final MappingEnd event + return new ProjectRuntime(name) { Options = runtimeOptions }; + + static ProjectRuntimeName DeserializeName(Scalar nameValueScalar, Type type) + { + if (string.IsNullOrWhiteSpace(nameValueScalar.Value)) + throw new YamlException($"A valid runtime name was not provided when deserializing [{type.FullName}]."); + + if (Enum.TryParse(nameValueScalar.Value, true, out var runtimeName)) + return runtimeName; + + throw new YamlException($"Unexpected runtime name of \"{nameValueScalar.Value}\" provided when deserializing [{type.FullName}]."); + } + } + + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + if (!(value is ProjectRuntime runtime)) + return; + + if (runtime.Options is null) + { + emitter.Emit(new Scalar(runtime.Name.ToString().ToLower())); + } + else + { + emitter.Emit(new MappingStart(null, null, false, MappingStyle.Block)); + + emitter.Emit(new Scalar("name")); + emitter.Emit(new Scalar(runtime.Name.ToString().ToLower())); + + emitter.Emit(new Scalar("options")); + this._optionsConverter.WriteYaml(emitter, runtime.Options, OptionsType); + + emitter.Emit(new MappingEnd()); + } + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/Yaml/StackSettingsConfigValueYamlConverter.cs b/sdk/dotnet/Pulumi.Automation/Serialization/Yaml/StackSettingsConfigValueYamlConverter.cs new file mode 100644 index 000000000..d760d83ca --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/Yaml/StackSettingsConfigValueYamlConverter.cs @@ -0,0 +1,72 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Pulumi.Automation.Serialization.Yaml +{ + internal class StackSettingsConfigValueYamlConverter : IYamlTypeConverter + { + private static readonly Type Type = typeof(StackSettingsConfigValue); + + public bool Accepts(Type type) + => type == Type; + + public object? ReadYaml(IParser parser, Type type) + { + // check if plain string + if (parser.Accept(out var stringValue)) + { + parser.MoveNext(); + return new StackSettingsConfigValue(stringValue.Value, false); + } + + // confirm it is an object + if (!parser.TryConsume(out _)) + throw new YamlException($"Unable to deserialize [{type.FullName}]. Expecting object if not plain string."); + + // get first property name + if (!parser.TryConsume(out var firstPropertyName)) + throw new YamlException($"Unable to deserialize [{type.FullName}]. Expecting first property name inside object."); + + // check if secure string + if (string.Equals("Secure", firstPropertyName.Value, StringComparison.OrdinalIgnoreCase)) + { + // secure string + if (!parser.TryConsume(out var securePropertyValue)) + throw new YamlException($"Unable to deserialize [{type.FullName}] as a secure string. Expecting a string secret."); + + // needs to be 1 mapping end and then return + parser.Require(); + parser.MoveNext(); + return new StackSettingsConfigValue(securePropertyValue.Value, true); + } + else + { + throw new NotSupportedException("Automation API does not currently support deserializing complex objects from stack settings."); + } + } + + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + if (!(value is StackSettingsConfigValue configValue)) + return; + + // secure string + if (configValue.IsSecure) + { + emitter.Emit(new MappingStart(null, null, false, MappingStyle.Block)); + emitter.Emit(new Scalar("secure")); + emitter.Emit(new Scalar(configValue.Value)); + emitter.Emit(new MappingEnd()); + } + // plain string + else + { + emitter.Emit(new Scalar(configValue.Value)); + } + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/Yaml/YamlScalarExtensions.cs b/sdk/dotnet/Pulumi.Automation/Serialization/Yaml/YamlScalarExtensions.cs new file mode 100644 index 000000000..d53611b63 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Serialization/Yaml/YamlScalarExtensions.cs @@ -0,0 +1,19 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; + +namespace Pulumi.Automation.Serialization.Yaml +{ + internal static class YamlScalarExtensions + { + public static bool ReadBoolean(this Scalar scalar, string propertyName, Type type) + { + if (bool.TryParse(scalar.Value, out var boolean)) + return boolean; + + throw new YamlException($"Unable to deserialize [{type.FullName}]. Exepecting a boolean for [{propertyName}]."); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/StackSettings.cs b/sdk/dotnet/Pulumi.Automation/StackSettings.cs new file mode 100644 index 000000000..43cf9853e --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/StackSettings.cs @@ -0,0 +1,31 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System.Collections.Generic; + +namespace Pulumi.Automation +{ + public class StackSettings + { + /// + /// This stack's secrets provider. + /// + public string? SecretsProvider { get; set; } + + /// + /// This is the KMS-encrypted ciphertext for the data key used for secrets + /// encryption. Only used for cloud-based secrets providers. + /// + public string? EncryptedKey { get; set; } + + /// + /// This is this stack's base64 encoded encryption salt. Only used for + /// passphrase-based secrets providers. + /// + public string? EncryptionSalt { get; set; } + + /// + /// This is an optional configuration bag. + /// + public IDictionary? Config { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/StackSettingsConfigValue.cs b/sdk/dotnet/Pulumi.Automation/StackSettingsConfigValue.cs new file mode 100644 index 000000000..77569e2f8 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/StackSettingsConfigValue.cs @@ -0,0 +1,19 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + public class StackSettingsConfigValue + { + public string Value { get; } + + public bool IsSecure { get; } + + public StackSettingsConfigValue( + string value, + bool isSecure) + { + this.Value = value; + this.IsSecure = isSecure; + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/StackSummary.cs b/sdk/dotnet/Pulumi.Automation/StackSummary.cs new file mode 100644 index 000000000..e3bf3c23c --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/StackSummary.cs @@ -0,0 +1,37 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; + +namespace Pulumi.Automation +{ + public class StackSummary + { + public string Name { get; } + + public bool IsCurrent { get; } + + public DateTimeOffset? LastUpdate { get; } + + public bool IsUpdateInProgress { get; } + + public int? ResourceCount { get; } + + public string? Url { get; } + + internal StackSummary( + string name, + bool isCurrent, + DateTimeOffset? lastUpdate, + bool isUpdateInProgress, + int? resourceCount, + string? url) + { + this.Name = name; + this.IsCurrent = isCurrent; + this.LastUpdate = lastUpdate; + this.IsUpdateInProgress = isUpdateInProgress; + this.ResourceCount = resourceCount; + this.Url = url; + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/UpOptions.cs b/sdk/dotnet/Pulumi.Automation/UpOptions.cs new file mode 100644 index 000000000..1b3ddddd1 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/UpOptions.cs @@ -0,0 +1,23 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; + +namespace Pulumi.Automation +{ + /// + /// Options controlling the behavior of an operation. + /// + public sealed class UpOptions : UpdateOptions + { + public bool? ExpectNoChanges { get; set; } + + public List? Replace { get; set; } + + public bool? TargetDependents { get; set; } + + public Action? OnOutput { get; set; } + + public PulumiFn? Program { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/UpResult.cs b/sdk/dotnet/Pulumi.Automation/UpResult.cs new file mode 100644 index 000000000..7ee67aff7 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/UpResult.cs @@ -0,0 +1,21 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System.Collections.Immutable; + +namespace Pulumi.Automation +{ + public sealed class UpResult : UpdateResult + { + public IImmutableDictionary Outputs { get; } + + internal UpResult( + string standardOutput, + string standardError, + UpdateSummary summary, + IImmutableDictionary outputs) + : base(standardOutput, standardError, summary) + { + this.Outputs = outputs; + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/UpdateKind.cs b/sdk/dotnet/Pulumi.Automation/UpdateKind.cs new file mode 100644 index 000000000..3f5a3e995 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/UpdateKind.cs @@ -0,0 +1,14 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + public enum UpdateKind + { + Update, + Preview, + Refresh, + Rename, + Destroy, + Import, + } +} diff --git a/sdk/dotnet/Pulumi.Automation/UpdateOptions.cs b/sdk/dotnet/Pulumi.Automation/UpdateOptions.cs new file mode 100644 index 000000000..217b06734 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/UpdateOptions.cs @@ -0,0 +1,19 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System.Collections.Generic; + +namespace Pulumi.Automation +{ + /// + /// Common options controlling the behavior of update actions taken + /// against an instance of . + /// + public class UpdateOptions + { + public int? Parallel { get; set; } + + public string? Message { get; set; } + + public List? Target { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/UpdateResult.cs b/sdk/dotnet/Pulumi.Automation/UpdateResult.cs new file mode 100644 index 000000000..208997c9a --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/UpdateResult.cs @@ -0,0 +1,23 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + public class UpdateResult + { + public string StandardOutput { get; } + + public string StandardError { get; } + + public UpdateSummary Summary { get; } + + internal UpdateResult( + string standardOutput, + string standardError, + UpdateSummary summary) + { + this.StandardOutput = standardOutput; + this.StandardError = standardError; + this.Summary = summary; + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/UpdateState.cs b/sdk/dotnet/Pulumi.Automation/UpdateState.cs new file mode 100644 index 000000000..69bf8c432 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/UpdateState.cs @@ -0,0 +1,12 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + public enum UpdateState + { + NotStarted, + InProgress, + Succeeded, + Failed, + } +} diff --git a/sdk/dotnet/Pulumi.Automation/UpdateSummary.cs b/sdk/dotnet/Pulumi.Automation/UpdateSummary.cs new file mode 100644 index 000000000..973cdd49a --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/UpdateSummary.cs @@ -0,0 +1,57 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Pulumi.Automation +{ + public sealed class UpdateSummary + { + // pre-update information + public UpdateKind Kind { get; } + + public DateTimeOffset StartTime { get; } + + public string Message { get; } + + public IImmutableDictionary Environment { get; } + + public IImmutableDictionary Config { get; } + + // post-update information + public UpdateState Result { get; } + + public DateTimeOffset EndTime { get; } + + public int? Version { get; } + + public string? Deployment { get; } + + public IImmutableDictionary? ResourceChanges { get; } + + internal UpdateSummary( + UpdateKind kind, + DateTimeOffset startTime, + string message, + IDictionary environment, + IDictionary config, + UpdateState result, + DateTimeOffset endTime, + int? version, + string? deployment, + IDictionary? resourceChanges) + { + this.Kind = kind; + this.StartTime = startTime; + this.Message = message; + this.Environment = environment.ToImmutableDictionary(); + this.Config = config.ToImmutableDictionary(); + this.Result = result; + this.EndTime = endTime; + this.Version = version; + this.Deployment = deployment; + this.ResourceChanges = resourceChanges?.ToImmutableDictionary(); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/WhoAmIResult.cs b/sdk/dotnet/Pulumi.Automation/WhoAmIResult.cs new file mode 100644 index 000000000..2a4df8224 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/WhoAmIResult.cs @@ -0,0 +1,14 @@ +// Copyright 2016-2021, Pulumi Corporation + +namespace Pulumi.Automation +{ + public class WhoAmIResult + { + public string User { get; } + + public WhoAmIResult(string user) + { + this.User = user; + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Workspace.cs b/sdk/dotnet/Pulumi.Automation/Workspace.cs new file mode 100644 index 000000000..eeee3a147 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/Workspace.cs @@ -0,0 +1,289 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Pulumi.Automation.Commands; +using Pulumi.Automation.Commands.Exceptions; + +namespace Pulumi.Automation +{ + /// + /// Workspace is the execution context containing a single Pulumi project, a program, and multiple stacks. + /// + /// Workspaces are used to manage the execution environment, providing various utilities such as plugin + /// installation, environment configuration ($PULUMI_HOME), and creation, deletion, and listing of Stacks. + /// + public abstract class Workspace : IDisposable + { + private readonly IPulumiCmd _cmd; + + internal Workspace(IPulumiCmd cmd) + { + this._cmd = cmd; + } + + /// + /// The working directory to run Pulumi CLI commands. + /// + public abstract string WorkDir { get; } + + /// + /// The directory override for CLI metadata if set. + /// + /// This customizes the location of $PULUMI_HOME where metadata is stored and plugins are installed. + /// + public abstract string? PulumiHome { get; } + + /// + /// The secrets provider to use for encryption and decryption of stack secrets. + /// + /// See: https://www.pulumi.com/docs/intro/concepts/config/#available-encryption-providers + /// + public abstract string? SecretsProvider { get; } + + /// + /// The inline program to be used for Preview/Update operations if any. + /// + /// If none is specified, the stack will refer to for this information. + /// + public abstract PulumiFn? Program { get; set; } + + /// + /// Environment values scoped to the current workspace. These will be supplied to every Pulumi command. + /// + public abstract IDictionary? EnvironmentVariables { get; set; } + + /// + /// Returns project settings for the current project if any. + /// + public abstract Task GetProjectSettingsAsync(CancellationToken cancellationToken = default); + + /// + /// Overwrites the settings for the current project. + /// + /// There can only be a single project per workspace. Fails if new project name does not match old. + /// + /// The settings object to save. + /// A cancellation token. + public abstract Task SaveProjectSettingsAsync(ProjectSettings settings, CancellationToken cancellationToken = default); + + /// + /// Returns stack settings for the stack matching the specified stack name if any. + /// + /// The name of the stack. + /// A cancellation token. + public abstract Task GetStackSettingsAsync(string stackName, CancellationToken cancellationToken = default); + + /// + /// Overwrite the settings for the stack matching the specified stack name. + /// + /// The name of the stack to operate on. + /// The settings object to save. + /// A cancellation token. + public abstract Task SaveStackSettingsAsync(string stackName, StackSettings settings, CancellationToken cancellationToken = default); + + /// + /// Hook to provide additional args to every CLI command before they are executed. + /// + /// Provided with a stack name, returns an array of args to append to an invoked command ["--config=...", ]. + /// + /// does not utilize this extensibility point. + /// + /// The name of the stack. + /// A cancellation token. + public abstract Task> SerializeArgsForOpAsync(string stackName, CancellationToken cancellationToken = default); + + /// + /// Hook executed after every command. Called with the stack name. + /// + /// An extensibility point to perform workspace cleanup (CLI operations may create/modify a Pulumi.stack.yaml). + /// + /// does not utilize this extensibility point. + /// + /// The name of the stack. + /// A cancellation token. + public abstract Task PostCommandCallbackAsync(string stackName, CancellationToken cancellationToken = default); + + /// + /// Returns the value associated with the specified stack name and key, scoped + /// to the Workspace. + /// + /// The name of the stack to read config from. + /// The key to use for the config lookup. + /// A cancellation token. + public abstract Task GetConfigValueAsync(string stackName, string key, CancellationToken cancellationToken = default); + + /// + /// Returns the config map for the specified stack name, scoped to the current Workspace. + /// + /// The name of the stack to read config from. + /// A cancellation token. + public abstract Task> GetConfigAsync(string stackName, CancellationToken cancellationToken = default); + + /// + /// Sets the specified key-value pair in the provided stack's config. + /// + /// The name of the stack to operate on. + /// The config key to set. + /// The config value to set. + /// A cancellation token. + public abstract Task SetConfigValueAsync(string stackName, string key, ConfigValue value, CancellationToken cancellationToken = default); + + /// + /// Sets all values in the provided config map for the specified stack name. + /// + /// The name of the stack to operate on. + /// The config map to upsert against the existing config. + /// A cancellation token. + public abstract Task SetConfigAsync(string stackName, IDictionary configMap, CancellationToken cancellationToken = default); + + /// + /// Removes the specified key-value pair from the provided stack's config. + /// + /// The name of the stack to operate on. + /// The config key to remove. + /// A cancellation token. + public abstract Task RemoveConfigValueAsync(string stackName, string key, CancellationToken cancellationToken = default); + + /// + /// Removes all values in the provided key collection from the config map for the specified stack name. + /// + /// The name of the stack to operate on. + /// The collection of keys to remove from the underlying config map. + /// A cancellation token. + public abstract Task RemoveConfigAsync(string stackName, IEnumerable keys, CancellationToken cancellationToken = default); + + /// + /// Gets and sets the config map used with the last update for the stack matching the specified stack name. + /// + /// The name of the stack to operate on. + /// A cancellation token. + public abstract Task> RefreshConfigAsync(string stackName, CancellationToken cancellationToken = default); + + /// + /// Returns the currently authenticated user. + /// + public abstract Task WhoAmIAsync(CancellationToken cancellationToken = default); + + /// + /// Returns a summary of the currently selected stack, if any. + /// + public virtual async Task GetStackAsync(CancellationToken cancellationToken = default) + { + var stacks = await this.ListStacksAsync(cancellationToken).ConfigureAwait(false); + return stacks.FirstOrDefault(x => x.IsCurrent); + } + + /// + /// Creates and sets a new stack with the specified stack name, failing if one already exists. + /// + /// The stack to create. + public Task CreateStackAsync(string stackName) + => this.CreateStackAsync(stackName, default); + + /// + /// Creates and sets a new stack with the specified stack name, failing if one already exists. + /// + /// The stack to create. + /// A cancellation token. + /// If a stack already exists by the provided name. + public abstract Task CreateStackAsync(string stackName, CancellationToken cancellationToken); + + /// + /// Selects and sets an existing stack matching the stack name, failing if none exists. + /// + /// The stack to select. + /// If no stack was found by the provided name. + public Task SelectStackAsync(string stackName) + => this.SelectStackAsync(stackName, default); + + /// + /// Selects and sets an existing stack matching the stack name, failing if none exists. + /// + /// The stack to select. + /// A cancellation token. + public abstract Task SelectStackAsync(string stackName, CancellationToken cancellationToken); + + /// + /// Deletes the stack and all associated configuration and history. + /// + /// The stack to remove. + /// A cancellation token. + public abstract Task RemoveStackAsync(string stackName, CancellationToken cancellationToken = default); + + /// + /// Returns all stacks created under the current project. + /// + /// This queries underlying backend and may return stacks not present in the Workspace (as Pulumi.{stack}.yaml files). + /// + public abstract Task> ListStacksAsync(CancellationToken cancellationToken = default); + + /// + /// Installs a plugin in the Workspace, for example to use cloud providers like AWS or GCP. + /// + /// The name of the plugin. + /// The version of the plugin e.g. "v1.0.0". + /// The kind of plugin e.g. "resource". + /// A cancellation token. + public abstract Task InstallPluginAsync(string name, string version, PluginKind kind = PluginKind.Resource, CancellationToken cancellationToken = default); + + /// + /// Removes a plugin from the Workspace matching the specified name and version. + /// + /// The optional name of the plugin. + /// The optional semver range to check when removing plugins matching the given name e.g. "1.0.0", ">1.0.0". + /// The kind of plugin e.g. "resource". + /// A cancellation token. + public abstract Task RemovePluginAsync(string? name = null, string? versionRange = null, PluginKind kind = PluginKind.Resource, CancellationToken cancellationToken = default); + + /// + /// Returns a list of all plugins installed in the Workspace. + /// + public abstract Task> ListPluginsAsync(CancellationToken cancellationToken = default); + + internal async Task RunStackCommandAsync( + string stackName, + IEnumerable args, + Action? onOutput, + CancellationToken cancellationToken) + { + var additionalArgs = await this.SerializeArgsForOpAsync(stackName, cancellationToken).ConfigureAwait(false); + var completeArgs = args.Concat(additionalArgs).ToList(); + + var result = await this.RunCommandAsync(completeArgs, onOutput, cancellationToken).ConfigureAwait(false); + await this.PostCommandCallbackAsync(stackName, cancellationToken).ConfigureAwait(false); + return result; + } + + internal Task RunCommandAsync( + IEnumerable args, + CancellationToken cancellationToken) + => this.RunCommandAsync(args, null, cancellationToken); + + internal Task RunCommandAsync( + IEnumerable args, + Action? onOutput, + CancellationToken cancellationToken) + { + var env = new Dictionary(); + if (!string.IsNullOrWhiteSpace(this.PulumiHome)) + env["PULUMI_HOME"] = this.PulumiHome; + + if (this.EnvironmentVariables != null) + { + foreach (var pair in this.EnvironmentVariables) + env[pair.Key] = pair.Value; + } + + return this._cmd.RunAsync(args, this.WorkDir, env, onOutput, cancellationToken); + } + + public virtual void Dispose() + { + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/WorkspaceStack.cs b/sdk/dotnet/Pulumi.Automation/WorkspaceStack.cs new file mode 100644 index 000000000..754ad6d0d --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/WorkspaceStack.cs @@ -0,0 +1,681 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net; +using System.Runtime.ExceptionServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Pulumi.Automation.Commands; +using Pulumi.Automation.Commands.Exceptions; +using Pulumi.Automation.Serialization; + +namespace Pulumi.Automation +{ + /// + /// is an isolated, independently configurable instance of a + /// Pulumi program. exposes methods for the full pulumi lifecycle + /// (up/preview/refresh/destroy), as well as managing configuration. + /// + /// Multiple stacks are commonly used to denote different phases of development + /// (such as development, staging, and production) or feature branches (such as + /// feature-x-dev, jane-feature-x-dev). + /// + /// Will dispose the on . + /// + public sealed class WorkspaceStack : IDisposable + { + private readonly Task _readyTask; + + /// + /// The name identifying the Stack. + /// + public string Name { get; } + + /// + /// The Workspace the Stack was created from. + /// + public Workspace Workspace { get; } + + /// + /// Creates a new stack using the given workspace, and stack name. + /// It fails if a stack with that name already exists. + /// + /// The name identifying the stack. + /// The Workspace the Stack was created from. + /// A cancellation token. + /// If a stack with the provided name already exists. + public static async Task CreateAsync( + string name, + Workspace workspace, + CancellationToken cancellationToken = default) + { + var stack = new WorkspaceStack(name, workspace, WorkspaceStackInitMode.Create, cancellationToken); + await stack._readyTask.ConfigureAwait(false); + return stack; + } + + /// + /// Selects stack using the given workspace, and stack name. + /// It returns an error if the given Stack does not exist. + /// + /// The name identifying the stack. + /// The Workspace the Stack was created from. + /// A cancellation token. + /// If a stack with the provided name does not exists. + public static async Task SelectAsync( + string name, + Workspace workspace, + CancellationToken cancellationToken = default) + { + var stack = new WorkspaceStack(name, workspace, WorkspaceStackInitMode.Select, cancellationToken); + await stack._readyTask.ConfigureAwait(false); + return stack; + } + + /// + /// Tries to create a new Stack using the given workspace, and stack name + /// if the stack does not already exist, or falls back to selecting an + /// existing stack. If the stack does not exist, it will be created and + /// selected. + /// + /// The name of the identifying stack. + /// The Workspace the Stack was created from. + /// A cancellation token. + public static async Task CreateOrSelectAsync( + string name, + Workspace workspace, + CancellationToken cancellationToken = default) + { + var stack = new WorkspaceStack(name, workspace, WorkspaceStackInitMode.CreateOrSelect, cancellationToken); + await stack._readyTask.ConfigureAwait(false); + return stack; + } + + private WorkspaceStack( + string name, + Workspace workspace, + WorkspaceStackInitMode mode, + CancellationToken cancellationToken) + { + this.Name = name; + this.Workspace = workspace; + + switch (mode) + { + case WorkspaceStackInitMode.Create: + this._readyTask = workspace.CreateStackAsync(name, cancellationToken); + break; + case WorkspaceStackInitMode.Select: + this._readyTask = workspace.SelectStackAsync(name, cancellationToken); + break; + case WorkspaceStackInitMode.CreateOrSelect: + this._readyTask = Task.Run(async () => + { + try + { + await workspace.CreateStackAsync(name, cancellationToken).ConfigureAwait(false); + } + catch (StackAlreadyExistsException) + { + await workspace.SelectStackAsync(name, cancellationToken).ConfigureAwait(false); + } + }); + break; + default: + throw new InvalidOperationException($"Unexpected Stack creation mode: {mode}"); + } + } + + /// + /// Returns the config value associated with the specified key. + /// + /// The key to use for the config lookup. + /// A cancellation token. + public Task GetConfigValueAsync(string key, CancellationToken cancellationToken = default) + => this.Workspace.GetConfigValueAsync(this.Name, key, cancellationToken); + + /// + /// Returns the full config map associated with the stack in the Workspace. + /// + /// A cancellation token. + public Task> GetConfigAsync(CancellationToken cancellationToken = default) + => this.Workspace.GetConfigAsync(this.Name, cancellationToken); + + /// + /// Sets the config key-value pair on the Stack in the associated Workspace. + /// + /// The key to set. + /// The config value to set. + /// A cancellation token. + public Task SetConfigValueAsync(string key, ConfigValue value, CancellationToken cancellationToken = default) + => this.Workspace.SetConfigValueAsync(this.Name, key, value, cancellationToken); + + /// + /// Sets all specified config values on the stack in the associated Workspace. + /// + /// The map of config key-value pairs to set. + /// A cancellation token. + public Task SetConfigAsync(IDictionary configMap, CancellationToken cancellationToken = default) + => this.Workspace.SetConfigAsync(this.Name, configMap, cancellationToken); + + /// + /// Removes the specified config key from the Stack in the associated Workspace. + /// + /// The config key to remove. + /// A cancellation token. + public Task RemoveConfigValueAsync(string key, CancellationToken cancellationToken = default) + => this.Workspace.RemoveConfigValueAsync(this.Name, key, cancellationToken); + + /// + /// Removes the specified config keys from the Stack in the associated Workspace. + /// + /// The config keys to remove. + /// A cancellation token. + public Task RemoveConfigAsync(IEnumerable keys, CancellationToken cancellationToken = default) + => this.Workspace.RemoveConfigAsync(this.Name, keys, cancellationToken); + + /// + /// Gets and sets the config map used with the last update. + /// + /// A cancellation token. + public Task> RefreshConfigAsync(CancellationToken cancellationToken = default) + => this.Workspace.RefreshConfigAsync(this.Name, cancellationToken); + + /// + /// Creates or updates the resources in a stack by executing the program in the Workspace. + /// + /// https://www.pulumi.com/docs/reference/cli/pulumi_up/ + /// + /// Options to customize the behavior of the update. + /// A cancellation token. + public async Task UpAsync( + UpOptions? options = null, + CancellationToken cancellationToken = default) + { + await this.Workspace.SelectStackAsync(this.Name, cancellationToken).ConfigureAwait(false); + var execKind = ExecKind.Local; + var program = this.Workspace.Program; + var args = new List() + { + "up", + "--yes", + "--skip-preview", + }; + + if (options != null) + { + if (options.Program != null) + program = options.Program; + + if (!string.IsNullOrWhiteSpace(options.Message)) + { + args.Add("--message"); + args.Add(options.Message); + } + + if (options.ExpectNoChanges is true) + args.Add("--expect-no-changes"); + + if (options.Replace?.Any() == true) + { + foreach (var item in options.Replace) + { + args.Add("--replace"); + args.Add(item); + } + } + + if (options.Target?.Any() == true) + { + foreach (var item in options.Target) + { + args.Add("--target"); + args.Add(item); + } + } + + if (options.TargetDependents is true) + args.Add("--target-dependents"); + + if (options.Parallel.HasValue) + { + args.Add("--parallel"); + args.Add(options.Parallel.Value.ToString()); + } + } + + InlineLanguageHost? inlineHost = null; + try + { + if (program != null) + { + execKind = ExecKind.Inline; + inlineHost = new InlineLanguageHost(program, cancellationToken); + await inlineHost.StartAsync().ConfigureAwait(false); + var port = await inlineHost.GetPortAsync().ConfigureAwait(false); + args.Add($"--client=127.0.0.1:{port}"); + } + + args.Add("--exec-kind"); + args.Add(execKind); + + var upResult = await this.RunCommandAsync(args, options?.OnOutput, cancellationToken).ConfigureAwait(false); + if (inlineHost != null && inlineHost.TryGetExceptionInfo(out var exceptionInfo)) + exceptionInfo.Throw(); + + var output = await this.GetOutputAsync(cancellationToken).ConfigureAwait(false); + var summary = await this.GetInfoAsync(cancellationToken).ConfigureAwait(false); + return new UpResult( + upResult.StandardOutput, + upResult.StandardError, + summary!, + output); + } + finally + { + if (inlineHost != null) + { + await inlineHost.DisposeAsync().ConfigureAwait(false); + } + } + } + + /// + /// Performs a dry-run update to a stack, returning pending changes. + /// + /// https://www.pulumi.com/docs/reference/cli/pulumi_preview/ + /// + /// Options to customize the behavior of the update. + /// A cancellation token. + public async Task PreviewAsync( + PreviewOptions? options = null, + CancellationToken cancellationToken = default) + { + await this.Workspace.SelectStackAsync(this.Name, cancellationToken).ConfigureAwait(false); + var execKind = ExecKind.Local; + var program = this.Workspace.Program; + var args = new List() { "preview" }; + + if (options != null) + { + if (options.Program != null) + program = options.Program; + + if (!string.IsNullOrWhiteSpace(options.Message)) + { + args.Add("--message"); + args.Add(options.Message); + } + + if (options.ExpectNoChanges is true) + args.Add("--expect-no-changes"); + + if (options.Replace?.Any() == true) + { + foreach (var item in options.Replace) + { + args.Add("--replace"); + args.Add(item); + } + } + + if (options.Target?.Any() == true) + { + foreach (var item in options.Target) + { + args.Add("--target"); + args.Add(item); + } + } + + if (options.TargetDependents is true) + args.Add("--target-dependents"); + + if (options.Parallel.HasValue) + { + args.Add("--parallel"); + args.Add(options.Parallel.Value.ToString()); + } + } + + InlineLanguageHost? inlineHost = null; + try + { + if (program != null) + { + execKind = ExecKind.Inline; + inlineHost = new InlineLanguageHost(program, cancellationToken); + await inlineHost.StartAsync().ConfigureAwait(false); + var port = await inlineHost.GetPortAsync().ConfigureAwait(false); + args.Add($"--client=127.0.0.1:{port}"); + } + + args.Add("--exec-kind"); + args.Add(execKind); + + var upResult = await this.RunCommandAsync(args, null, cancellationToken).ConfigureAwait(false); + if (inlineHost != null && inlineHost.TryGetExceptionInfo(out var exceptionInfo)) + exceptionInfo.Throw(); + + var summary = await this.GetInfoAsync(cancellationToken).ConfigureAwait(false); + return new UpdateResult( + upResult.StandardOutput, + upResult.StandardError, + summary!); + } + finally + { + if (inlineHost != null) + { + await inlineHost.DisposeAsync().ConfigureAwait(false); + } + } + } + + /// + /// Compares the current stack’s resource state with the state known to exist in the actual + /// cloud provider. Any such changes are adopted into the current stack. + /// + /// Options to customize the behavior of the refresh. + /// A cancellation token. + public async Task RefreshAsync( + RefreshOptions? options = null, + CancellationToken cancellationToken = default) + { + await this.Workspace.SelectStackAsync(this.Name, cancellationToken).ConfigureAwait(false); + var args = new List() + { + "refresh", + "--yes", + "--skip-preview", + }; + + if (options != null) + { + if (!string.IsNullOrWhiteSpace(options.Message)) + { + args.Add("--message"); + args.Add(options.Message); + } + + if (options.ExpectNoChanges is true) + args.Add("--expect-no-changes"); + + if (options.Target?.Any() == true) + { + foreach (var item in options.Target) + { + args.Add("--target"); + args.Add(item); + } + } + + if (options.Parallel.HasValue) + { + args.Add("--parallel"); + args.Add(options.Parallel.Value.ToString()); + } + } + + var result = await this.RunCommandAsync(args, options?.OnOutput, cancellationToken).ConfigureAwait(false); + var summary = await this.GetInfoAsync(cancellationToken).ConfigureAwait(false); + return new UpdateResult( + result.StandardOutput, + result.StandardError, + summary!); + } + + /// + /// Destroy deletes all resources in a stack, leaving all history and configuration intact. + /// + /// Options to customize the behavior of the destroy. + /// A cancellation token. + public async Task DestroyAsync( + DestroyOptions? options = null, + CancellationToken cancellationToken = default) + { + await this.Workspace.SelectStackAsync(this.Name, cancellationToken).ConfigureAwait(false); + var args = new List() + { + "destroy", + "--yes", + "--skip-preview", + }; + + if (options != null) + { + if (!string.IsNullOrWhiteSpace(options.Message)) + { + args.Add("--message"); + args.Add(options.Message); + } + + if (options.Target?.Any() == true) + { + foreach (var item in options.Target) + { + args.Add("--target"); + args.Add(item); + } + } + + if (options.TargetDependents is true) + args.Add("--target-dependents"); + + if (options.Parallel.HasValue) + { + args.Add("--parallel"); + args.Add(options.Parallel.Value.ToString()); + } + } + + var result = await this.RunCommandAsync(args, options?.OnOutput, cancellationToken).ConfigureAwait(false); + var summary = await this.GetInfoAsync(cancellationToken).ConfigureAwait(false); + return new UpdateResult( + result.StandardOutput, + result.StandardError, + summary!); + } + + /// + /// Gets the current set of Stack outputs from the last . + /// + private async Task> GetOutputAsync(CancellationToken cancellationToken) + { + await this.Workspace.SelectStackAsync(this.Name).ConfigureAwait(false); + + // TODO: do this in parallel after this is fixed https://github.com/pulumi/pulumi/issues/6050 + var maskedResult = await this.RunCommandAsync(new[] { "stack", "output", "--json" }, null, cancellationToken).ConfigureAwait(false); + var plaintextResult = await this.RunCommandAsync(new[] { "stack", "output", "--json", "--show-secrets" }, null, cancellationToken).ConfigureAwait(false); + var jsonOptions = LocalSerializer.BuildJsonSerializerOptions(); + var maskedOutput = JsonSerializer.Deserialize>(maskedResult.StandardOutput, jsonOptions); + var plaintextOutput = JsonSerializer.Deserialize>(plaintextResult.StandardOutput, jsonOptions); + + var output = new Dictionary(); + foreach (var (key, value) in plaintextOutput) + { + var secret = maskedOutput[key] is string maskedValue && maskedValue == "[secret]"; + output[key] = new OutputValue(value, secret); + } + + return output.ToImmutableDictionary(); + } + + /// + /// Returns a list summarizing all previews and current results from Stack lifecycle operations (up/preview/refresh/destroy). + /// + /// Options to customize the behavior of the fetch history action. + /// A cancellation token. + public async Task> GetHistoryAsync( + HistoryOptions? options = null, + CancellationToken cancellationToken = default) + { + var args = new List() + { + "history", + "--json", + "--show-secrets", + }; + + if (options?.PageSize.HasValue == true) + { + if (options.PageSize!.Value < 1) + throw new ArgumentException($"{nameof(options.PageSize)} must be greater than or equal to 1.", nameof(options.PageSize)); + + var page = !options.Page.HasValue ? 1 + : options.Page.Value < 1 ? 1 + : options.Page.Value; + + args.Add("--page-size"); + args.Add(options.PageSize.Value.ToString()); + args.Add("--page"); + args.Add(page.ToString()); + } + + var result = await this.RunCommandAsync(args, null, cancellationToken).ConfigureAwait(false); + var jsonOptions = LocalSerializer.BuildJsonSerializerOptions(); + var list = JsonSerializer.Deserialize>(result.StandardOutput, jsonOptions); + return list.ToImmutableList(); + } + + public async Task GetInfoAsync(CancellationToken cancellationToken = default) + { + var history = await this.GetHistoryAsync( + new HistoryOptions + { + PageSize = 1, + }, + cancellationToken).ConfigureAwait(false); + + return history.FirstOrDefault(); + } + + private Task RunCommandAsync( + IEnumerable args, + Action? onOutput, + CancellationToken cancellationToken) + => this.Workspace.RunStackCommandAsync(this.Name, args, onOutput, cancellationToken); + + public void Dispose() + => this.Workspace.Dispose(); + + private static class ExecKind + { + public const string Local = "auto.local"; + public const string Inline = "auto.inline"; + } + + private enum WorkspaceStackInitMode + { + Create, + Select, + CreateOrSelect + } + + private class InlineLanguageHost : IAsyncDisposable + { + private readonly TaskCompletionSource _portTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly CancellationToken _cancelToken; + private readonly IHost _host; + private readonly CancellationTokenRegistration _portRegistration; + + public InlineLanguageHost( + PulumiFn program, + CancellationToken cancellationToken) + { + this._cancelToken = cancellationToken; + this._host = Host.CreateDefaultBuilder() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder + .ConfigureKestrel(kestrelOptions => + { + kestrelOptions.Listen(IPAddress.Any, 0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); + }) + .ConfigureServices(services => + { + services.AddLogging(); + + // to be injected into LanguageRuntimeService + var callerContext = new LanguageRuntimeService.CallerContext(program, cancellationToken); + services.AddSingleton(callerContext); + + services.AddGrpc(grpcOptions => + { + grpcOptions.MaxReceiveMessageSize = LanguageRuntimeService.MaxRpcMesageSize; + grpcOptions.MaxSendMessageSize = LanguageRuntimeService.MaxRpcMesageSize; + }); + }) + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + }); + }); + }) + .Build(); + + // before starting the host, set up this callback to tell us what port was selected + this._portRegistration = this._host.Services.GetRequiredService().ApplicationStarted.Register(() => + { + try + { + var serverFeatures = this._host.Services.GetRequiredService().Features; + var addresses = serverFeatures.Get().Addresses.ToList(); + Debug.Assert(addresses.Count == 1, "Server should only be listening on one address"); + var uri = new Uri(addresses[0]); + this._portTcs.TrySetResult(uri.Port); + } + catch (Exception ex) + { + this._portTcs.TrySetException(ex); + } + }); + } + + public Task StartAsync() + => this._host.StartAsync(this._cancelToken); + + public Task GetPortAsync() + => this._portTcs.Task; + + public bool TryGetExceptionInfo([NotNullWhen(true)] out ExceptionDispatchInfo? info) + { + var callerContext = this._host.Services.GetRequiredService(); + if (callerContext.ExceptionDispatchInfo is null) + { + info = null; + return false; + } + + info = callerContext.ExceptionDispatchInfo; + return true; + } + + public async ValueTask DisposeAsync() + { + this._portRegistration.Unregister(); + await this._host.StopAsync(this._cancelToken).ConfigureAwait(false); + this._host.Dispose(); + } + } + } +} diff --git a/sdk/dotnet/Pulumi.Tests/AssemblyAttributes.cs b/sdk/dotnet/Pulumi.Tests/AssemblyAttributes.cs index 93085d07d..d8917540d 100644 --- a/sdk/dotnet/Pulumi.Tests/AssemblyAttributes.cs +++ b/sdk/dotnet/Pulumi.Tests/AssemblyAttributes.cs @@ -1,4 +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 +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/sdk/dotnet/Pulumi.Tests/Pulumi.Tests.csproj b/sdk/dotnet/Pulumi.Tests/Pulumi.Tests.csproj index ad9c1a68e..bfa75a827 100644 --- a/sdk/dotnet/Pulumi.Tests/Pulumi.Tests.csproj +++ b/sdk/dotnet/Pulumi.Tests/Pulumi.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1 @@ -19,7 +19,9 @@ - + + PreserveNewest + \ No newline at end of file diff --git a/sdk/dotnet/Pulumi/AssemblyAttributes.cs b/sdk/dotnet/Pulumi/AssemblyAttributes.cs index 8fc446652..4266e7d1b 100644 --- a/sdk/dotnet/Pulumi/AssemblyAttributes.cs +++ b/sdk/dotnet/Pulumi/AssemblyAttributes.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Pulumi.Automation")] [assembly: InternalsVisibleTo("Pulumi.Tests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq tests diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment.Runner.cs b/sdk/dotnet/Pulumi/Deployment/Deployment.Runner.cs index 68aec1be3..6f14468bf 100644 --- a/sdk/dotnet/Pulumi/Deployment/Deployment.Runner.cs +++ b/sdk/dotnet/Pulumi/Deployment/Deployment.Runner.cs @@ -43,7 +43,7 @@ namespace Pulumi public Task RunAsync() where TStack : Stack, new() => RunAsync(() => new TStack()); - private Task RunAsync(Func stackFactory) where TStack : Stack + public Task RunAsync(Func stackFactory) where TStack : Stack { try { diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_Config.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_Config.cs index 355e573f0..9373f6e96 100644 --- a/sdk/dotnet/Pulumi/Deployment/Deployment_Config.cs +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_Config.cs @@ -1,6 +1,7 @@ // Copyright 2016-2019, Pulumi Corporation using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Text.Json; @@ -24,6 +25,12 @@ namespace Pulumi internal void SetConfig(string key, string value) => AllConfig = AllConfig.Add(key, value); + /// + /// Appends all provided configuration. + /// + internal void SetAllConfig(IDictionary config) + => AllConfig = AllConfig.AddRange(config); + /// /// Returns a configuration variable's value or if it is unset. /// diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_Inline.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_Inline.cs new file mode 100644 index 000000000..8c6aa6342 --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_Inline.cs @@ -0,0 +1,44 @@ +// Copyright 2016-2021, Pulumi Corporation + +using System; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; + +namespace Pulumi +{ + public partial class Deployment + { + private Deployment(InlineDeploymentSettings settings) + { + if (settings is null) + throw new ArgumentNullException(nameof(settings)); + + _projectName = settings.Project; + _stackName = settings.Stack; + _isDryRun = settings.IsDryRun; + SetAllConfig(settings.Config); + + if (string.IsNullOrEmpty(settings.MonitorAddr) + || string.IsNullOrEmpty(settings.EngineAddr) + || string.IsNullOrEmpty(_projectName) + || string.IsNullOrEmpty(_stackName)) + { + throw new InvalidOperationException("Inline execution was not provided the necessary parameters to run the Pulumi engine."); + } + + Serilog.Log.Debug("Creating Deployment Engine."); + Engine = new GrpcEngine(settings.EngineAddr); + Serilog.Log.Debug("Created Deployment Engine."); + + Serilog.Log.Debug("Creating Deployment Monitor."); + Monitor = new GrpcMonitor(settings.MonitorAddr); + Serilog.Log.Debug("Created Deployment Monitor."); + + _runner = new Runner(this); + _logger = new Logger(this, Engine); + } + + internal static Task RunInlineAsync(InlineDeploymentSettings settings, Func> func) + => func(CreateRunner(() => new Deployment(settings))); + } +} diff --git a/sdk/dotnet/Pulumi/Deployment/Deployment_Run.cs b/sdk/dotnet/Pulumi/Deployment/Deployment_Run.cs index 263c5c153..50084eaea 100644 --- a/sdk/dotnet/Pulumi/Deployment/Deployment_Run.cs +++ b/sdk/dotnet/Pulumi/Deployment/Deployment_Run.cs @@ -72,7 +72,7 @@ namespace Pulumi /// Callback that creates stack resources. /// Stack options. public static Task RunAsync(Func>> func, StackOptions? options = null) - => CreateRunner().RunAsync(func, options); + => CreateRunner(() => new Deployment()).RunAsync(func, options); /// /// is an entry-point to a Pulumi @@ -101,7 +101,7 @@ namespace Pulumi /// /// public static Task RunAsync() where TStack : Stack, new() - => CreateRunner().RunAsync(); + => CreateRunner(() => new Deployment()).RunAsync(); /// /// is an entry-point to a Pulumi @@ -131,7 +131,7 @@ namespace Pulumi /// /// public static Task RunAsync(IServiceProvider serviceProvider) where TStack : Stack - => CreateRunner().RunAsync(serviceProvider); + => CreateRunner(() => new Deployment()).RunAsync(serviceProvider); /// /// Entry point to test a Pulumi application. Deployment will @@ -203,7 +203,7 @@ namespace Pulumi } } - private static IRunner CreateRunner() + private static IRunner CreateRunner(Func deploymentFactory) { // Serilog.Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().CreateLogger(); @@ -214,7 +214,7 @@ namespace Pulumi throw new NotSupportedException("Deployment.Run can only be called a single time."); Serilog.Log.Debug("Creating new Deployment."); - var deployment = new Deployment(); + var deployment = deploymentFactory(); Instance = new DeploymentInstance(deployment); return deployment._runner; } diff --git a/sdk/dotnet/Pulumi/Deployment/IRunner.cs b/sdk/dotnet/Pulumi/Deployment/IRunner.cs index 78575f391..6137b0aa4 100644 --- a/sdk/dotnet/Pulumi/Deployment/IRunner.cs +++ b/sdk/dotnet/Pulumi/Deployment/IRunner.cs @@ -11,6 +11,7 @@ namespace Pulumi void RegisterTask(string description, Task task); Task RunAsync(Func>> func, StackOptions? options); Task RunAsync() where TStack : Stack, new(); + Task RunAsync(Func stackFactory) where TStack : Stack; Task RunAsync(IServiceProvider serviceProvider) where TStack : Stack; } } diff --git a/sdk/dotnet/Pulumi/Deployment/InlineDeploymentSettings.cs b/sdk/dotnet/Pulumi/Deployment/InlineDeploymentSettings.cs new file mode 100644 index 000000000..dd50131ce --- /dev/null +++ b/sdk/dotnet/Pulumi/Deployment/InlineDeploymentSettings.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace Pulumi +{ + internal class InlineDeploymentSettings + { + public string EngineAddr { get; } + + public string MonitorAddr { get; } + + public IDictionary Config { get; } + + public string Project { get; } + + public string Stack { get; } + + public int Parallel { get; } + + public bool IsDryRun { get; } + + public InlineDeploymentSettings( + string engineAddr, + string monitorAddr, + IDictionary config, + string project, + string stack, + int parallel, + bool isDryRun) + { + EngineAddr = engineAddr; + MonitorAddr = monitorAddr; + Config = config; + Project = project; + Stack = stack; + Parallel = parallel; + IsDryRun = isDryRun; + } + } +} diff --git a/sdk/dotnet/Pulumi/Serialization/ResourcePackages.cs b/sdk/dotnet/Pulumi/Serialization/ResourcePackages.cs index 59c4fc6de..6a911c458 100644 --- a/sdk/dotnet/Pulumi/Serialization/ResourcePackages.cs +++ b/sdk/dotnet/Pulumi/Serialization/ResourcePackages.cs @@ -113,7 +113,10 @@ namespace Pulumi } } - static bool PossibleMatch(AssemblyName? assembly) => assembly != null && !assembly.FullName.StartsWith("System", StringComparison.Ordinal); + static bool PossibleMatch(AssemblyName? assembly) + => assembly != null + && !assembly.FullName.StartsWith("System", StringComparison.Ordinal) + && assembly.ContentType != AssemblyContentType.WindowsRuntime; } } } diff --git a/sdk/dotnet/dotnet.sln b/sdk/dotnet/dotnet.sln index c07a41fae..771e2e1d5 100644 --- a/sdk/dotnet/dotnet.sln +++ b/sdk/dotnet/dotnet.sln @@ -16,6 +16,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pulumi.Automation", "Pulumi.Automation\Pulumi.Automation.csproj", "{74A15689-FB60-4760-99C8-FC0D89883F3D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pulumi.Automation.Tests", "Pulumi.Automation.Tests\Pulumi.Automation.Tests.csproj", "{A1E69FAC-B6C2-4EA2-8A2D-397536CE35B2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -62,6 +66,30 @@ Global {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 + {74A15689-FB60-4760-99C8-FC0D89883F3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74A15689-FB60-4760-99C8-FC0D89883F3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74A15689-FB60-4760-99C8-FC0D89883F3D}.Debug|x64.ActiveCfg = Debug|Any CPU + {74A15689-FB60-4760-99C8-FC0D89883F3D}.Debug|x64.Build.0 = Debug|Any CPU + {74A15689-FB60-4760-99C8-FC0D89883F3D}.Debug|x86.ActiveCfg = Debug|Any CPU + {74A15689-FB60-4760-99C8-FC0D89883F3D}.Debug|x86.Build.0 = Debug|Any CPU + {74A15689-FB60-4760-99C8-FC0D89883F3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74A15689-FB60-4760-99C8-FC0D89883F3D}.Release|Any CPU.Build.0 = Release|Any CPU + {74A15689-FB60-4760-99C8-FC0D89883F3D}.Release|x64.ActiveCfg = Release|Any CPU + {74A15689-FB60-4760-99C8-FC0D89883F3D}.Release|x64.Build.0 = Release|Any CPU + {74A15689-FB60-4760-99C8-FC0D89883F3D}.Release|x86.ActiveCfg = Release|Any CPU + {74A15689-FB60-4760-99C8-FC0D89883F3D}.Release|x86.Build.0 = Release|Any CPU + {A1E69FAC-B6C2-4EA2-8A2D-397536CE35B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1E69FAC-B6C2-4EA2-8A2D-397536CE35B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1E69FAC-B6C2-4EA2-8A2D-397536CE35B2}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1E69FAC-B6C2-4EA2-8A2D-397536CE35B2}.Debug|x64.Build.0 = Debug|Any CPU + {A1E69FAC-B6C2-4EA2-8A2D-397536CE35B2}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1E69FAC-B6C2-4EA2-8A2D-397536CE35B2}.Debug|x86.Build.0 = Debug|Any CPU + {A1E69FAC-B6C2-4EA2-8A2D-397536CE35B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1E69FAC-B6C2-4EA2-8A2D-397536CE35B2}.Release|Any CPU.Build.0 = Release|Any CPU + {A1E69FAC-B6C2-4EA2-8A2D-397536CE35B2}.Release|x64.ActiveCfg = Release|Any CPU + {A1E69FAC-B6C2-4EA2-8A2D-397536CE35B2}.Release|x64.Build.0 = Release|Any CPU + {A1E69FAC-B6C2-4EA2-8A2D-397536CE35B2}.Release|x86.ActiveCfg = Release|Any CPU + {A1E69FAC-B6C2-4EA2-8A2D-397536CE35B2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE