diff --git a/README.md b/README.md index b3929999..ae9ead01 100644 --- a/README.md +++ b/README.md @@ -265,21 +265,46 @@ curl \ There are cases where it's important to run some cleanup operation before destroying a resource such as when destroying the resource does not properly handle orderly cleanup. For example, destroying an EKS Cluster will not ensure that all Kubernetes object finalizers are run, which may lead to leaking external resources managed by those Kubernetes resources. This example shows how we can use a `delete`-only `Command` to ensure some cleanup is run within a cluster before destroying it. +```yaml +resources: + cluster: + type: eks:Cluster + + cleanupKubernetesNamespaces: + # We could also use `RemoteCommand` to run this from + # within a node in the cluster. + type: command:local:Command + properties: + # This will run before the cluster is destroyed. + # Everything else will need to depend on this resource + # to ensure this cleanup doesn't happen too early. + delete: | + kubectl --kubeconfig <(echo "$KUBECONFIG_DATA") delete namespace nginx + # Process substitution "<()" doesn't work in the default interpreter sh. + interpreter: ["/bin/bash", "-c"] + environment: + KUBECONFIG_DATA: "${cluster.kubeconfigJson}" +``` + ```ts -import { local } from "@pulumi/command"; +import * as pulumi from "@pulumi/pulumi"; +import * as command from "@pulumi/command"; import * as eks from "@pulumi/eks"; -import * as random from "@pulumi/random"; -import { interpolate } from "@pulumi/pulumi"; const cluster = new eks.Cluster("cluster", {}); // We could also use `RemoteCommand` to run this from within a node in the cluster -const cleanupKubernetesNamespaces = new local.Command("cleanupKubernetesNamespaces", { +const cleanupKubernetesNamespaces = new command.local.Command("cleanupKubernetesNamespaces", { // This will run before the cluster is destroyed. Everything else will need to // depend on this resource to ensure this cleanup doesn't happen too early. - delete: "kubectl delete --all namespaces", + "delete": "kubectl --kubeconfig <(echo \"$KUBECONFIG_DATA\") delete namespace nginx\n", + // Process substitution "<()" doesn't work in the default interpreter sh. + interpreter: [ + "/bin/bash", + "-c", + ], environment: { - KUBECONFIG: cluster.kubeconfig, + KUBECONFIG_DATA: cluster.kubeconfigJson, }, }); ``` diff --git a/provider/cmd/pulumi-resource-command/schema.json b/provider/cmd/pulumi-resource-command/schema.json index eb833fcd..d7941de4 100644 --- a/provider/cmd/pulumi-resource-command/schema.json +++ b/provider/cmd/pulumi-resource-command/schema.json @@ -205,7 +205,7 @@ }, "resources": { "command:local:Command": { - "description": "A local command to be executed.\n\nThis command can be inserted into the life cycles of other resources using the `dependsOn` or `parent` resource options. A command is considered to have failed when it finished with a non-zero exit code. This will fail the CRUD step of the `Command` resource.\n\n{{% examples %}}\n## Example Usage\n\n{{% example %}}\n### Triggers\n\nThis example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run.\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as command from \"@pulumi/command\";\nimport * as random from \"@pulumi/random\";\n\nconst str = \"foo\";\nconst fileAsset = new pulumi.asset.FileAsset(\"Pulumi.yaml\");\nconst rand = new random.RandomString(\"rand\", {length: 5});\nconst localFile = new command.local.Command(\"localFile\", {\n create: \"touch foo.txt\",\n archivePaths: [\"*.txt\"],\n});\n\nconst cmd = new command.local.Command(\"cmd\", {\n create: \"echo create > op.txt\",\n delete: \"echo delete >> op.txt\",\n triggers: [\n str,\n rand.result,\n fileAsset,\n localFile.archive,\n ],\n});\n```\n\n```python\nimport pulumi\nimport pulumi_command as command\nimport pulumi_random as random\n\nfoo = \"foo\"\nfile_asset_var = pulumi.FileAsset(\"Pulumi.yaml\")\nrand = random.RandomString(\"rand\", length=5)\nlocal_file = command.local.Command(\"localFile\",\n create=\"touch foo.txt\",\n archive_paths=[\"*.txt\"])\n\ncmd = command.local.Command(\"cmd\",\n create=\"echo create > op.txt\",\n delete=\"echo delete >> op.txt\",\n triggers=[\n foo,\n rand.result,\n file_asset_var,\n local_file.archive,\n ])\n```\n\n```go\npackage main\n\nimport (\n\t\"github.com/pulumi/pulumi-command/sdk/go/command/local\"\n\t\"github.com/pulumi/pulumi-random/sdk/v4/go/random\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\tstr := pulumi.String(\"foo\")\n\n\t\tfileAsset := pulumi.NewFileAsset(\"Pulumi.yaml\")\n\n\t\trand, err := random.NewRandomString(ctx, \"rand\", &random.RandomStringArgs{\n\t\t\tLength: pulumi.Int(5),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlocalFile, err := local.NewCommand(ctx, \"localFile\", &local.CommandArgs{\n\t\t\tCreate: pulumi.String(\"touch foo.txt\"),\n\t\t\tArchivePaths: pulumi.StringArray{\n\t\t\t\tpulumi.String(\"*.txt\"),\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t_, err = local.NewCommand(ctx, \"cmd\", &local.CommandArgs{\n\t\t\tCreate: pulumi.String(\"echo create > op.txt\"),\n\t\t\tDelete: pulumi.String(\"echo delete >> op.txt\"),\n\t\t\tTriggers: pulumi.Array{\n\t\t\t\tstr,\n\t\t\t\trand.Result,\n\t\t\t\tfileAsset,\n\t\t\t\tlocalFile.Archive,\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n```\n\n```csharp\nusing Pulumi;\nusing Command = Pulumi.Command;\nusing Random = Pulumi.Random;\n\nreturn await Deployment.RunAsync(() =>\n{\n var str = \"foo\";\n\n var fileAssetVar = new FileAsset(\"Pulumi.yaml\");\n\n var rand = new Random.RandomString(\"rand\", new()\n {\n Length = 5,\n });\n\n var localFile = new Command.Local.Command(\"localFile\", new()\n {\n Create = \"touch foo.txt\",\n ArchivePaths = new[]\n {\n \"*.txt\",\n },\n });\n\n var cmd = new Command.Local.Command(\"cmd\", new()\n {\n Create = \"echo create > op.txt\",\n Delete = \"echo delete >> op.txt\",\n Triggers = new object[]\n {\n str,\n rand.Result,\n fileAssetVar,\n localFile.Archive,\n },\n });\n\n});\n```\n\n```java\npublic class App {\n public static void main(String[] args) {\n Pulumi.run(App::stack);\n }\n\n public static void stack(Context ctx) {\n final var fileAssetVar = new FileAsset(\"Pulumi.yaml\");\n\n var rand = new RandomString(\"rand\", RandomStringArgs.builder()\n .length(5)\n .build());\n\n var localFile = new Command(\"localFile\", CommandArgs.builder()\n .create(\"touch foo.txt\")\n .archivePaths(\"*.txt\")\n .build());\n\n var cmd = new Command(\"cmd\", CommandArgs.builder()\n .create(\"echo create > op.txt\")\n .delete(\"echo delete >> op.txt\")\n .triggers(\n rand.result(),\n fileAssetVar,\n localFile.archive())\n .build());\n\n }\n}\n```\n\n```yaml\nconfig: {}\noutputs: {}\nresources:\n rand:\n type: random:index/randomString:RandomString\n properties:\n length: 5\n\n localFile:\n type: command:local:Command\n properties:\n create: touch foo.txt\n archivePaths:\n - \"*.txt\"\n\n cmd:\n type: command:local:Command\n properties:\n create: echo create > op.txt\n delete: echo delete >> op.txt\n triggers:\n - ${rand.result}\n - ${fileAsset}\n - ${localFile.archive}\n\nvariables:\n fileAsset:\n fn::fileAsset: \"Pulumi.yaml\"\n```\n{{% /example %}}\n\n{{% /examples %}}", + "description": "A local command to be executed.\n\nThis command can be inserted into the life cycles of other resources using the `dependsOn` or `parent` resource options. A command is considered to have failed when it finished with a non-zero exit code. This will fail the CRUD step of the `Command` resource.\n\n{{% examples %}}\n\n## Example Usage\n\n{{% example %}}\n\n### Basic Example\n\nThis example shows the simplest use case, simply running a command on `create` in the Pulumi lifecycle.\n\n```typescript\nimport { local } from \"@pulumi/command\";\n\nconst random = new local.Command(\"random\", {\n create: \"openssl rand -hex 16\",\n});\n\nexport const output = random.stdout;\n```\n\n```csharp\nusing System.Collections.Generic;\nusing Pulumi;\nusing Pulumi.Command.Local;\n\nawait Deployment.RunAsync(() =>\n{\n var command = new Command(\"random\", new CommandArgs\n {\n Create = \"openssl rand -hex 16\"\n });\n\n return new Dictionary\n {\n [\"stdOut\"] = command.Stdout\n };\n});\n```\n\n```python\nimport pulumi\nfrom pulumi_command import local\n\nrandom = local.Command(\"random\",\n create=\"openssl rand -hex 16\"\n)\n\npulumi.export(\"random\", random.stdout)\n```\n\n```go\npackage main\n\nimport (\n\t\"github.com/pulumi/pulumi-command/sdk/go/command/local\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\trandom, err := local.NewCommand(ctx, \"my-bucket\", &local.CommandArgs{\n\t\t\tCreate: pulumi.String(\"openssl rand -hex 16\"),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tctx.Export(\"output\", random.Stdout)\n\t\treturn nil\n\t})\n}\n```\n\n```java\npackage generated_program;\n\nimport com.pulumi.Context;\nimport com.pulumi.Pulumi;\nimport com.pulumi.command.local.Command;\nimport com.pulumi.command.local.CommandArgs;\n\npublic class App {\n public static void main(String[] args) {\n Pulumi.run(App::stack);\n }\n\n public static void stack(Context ctx) {\n var random = new Command(\"random\", CommandArgs.builder()\n .create(\"openssl rand -hex 16\")\n .build());\n\n ctx.export(\"rand\", random.stdout());\n }\n}\n```\n\n```yaml\noutputs:\n rand: \"${random.stdout}\"\nresources:\n random:\n type: command:local:Command\n properties:\n create: \"openssl rand -hex 16\"\n```\n\n{{% /example %}}\n\n{{% example %}}\n\n### Invoking a Lambda during Pulumi Deployment\n\nThis example show using a local command to invoke an AWS Lambda once it's deployed. The Lambda invocation could also depend on other resources.\n\n```typescript\nimport * as aws from \"@pulumi/aws\";\nimport { local } from \"@pulumi/command\";\nimport { getStack } from \"@pulumi/pulumi\";\n\nconst f = new aws.lambda.CallbackFunction(\"f\", {\n publish: true,\n callback: async (ev: any) => {\n return `Stack ${ev.stackName} is deployed!`;\n }\n});\n\nconst invoke = new local.Command(\"execf\", {\n create: `aws lambda invoke --function-name \"$FN\" --payload '{\"stackName\": \"${getStack()}\"}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '\"' && rm out.txt`,\n environment: {\n FN: f.qualifiedArn,\n AWS_REGION: aws.config.region!,\n AWS_PAGER: \"\",\n },\n}, { dependsOn: f })\n\nexport const output = invoke.stdout;\n```\n\n```python\nimport pulumi\nimport json\nimport pulumi_aws as aws\nimport pulumi_command as command\n\nlambda_role = aws.iam.Role(\"lambdaRole\", assume_role_policy=json.dumps({\n \"Version\": \"2012-10-17\",\n \"Statement\": [{\n \"Action\": \"sts:AssumeRole\",\n \"Effect\": \"Allow\",\n \"Principal\": {\n \"Service\": \"lambda.amazonaws.com\",\n },\n }],\n}))\n\nlambda_function = aws.lambda_.Function(\"lambdaFunction\",\n name=\"f\",\n publish=True,\n role=lambda_role.arn,\n handler=\"index.handler\",\n runtime=aws.lambda_.Runtime.NODE_JS20D_X,\n code=pulumi.FileArchive(\"./handler\"))\n\naws_config = pulumi.Config(\"aws\")\naws_region = aws_config.require(\"region\")\n\ninvoke_command = command.local.Command(\"invokeCommand\",\n create=f\"aws lambda invoke --function-name \\\"$FN\\\" --payload '{{\\\"stackName\\\": \\\"{pulumi.get_stack()}\\\"}}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '\\\"' && rm out.txt\",\n environment={\n \"FN\": lambda_function.arn,\n \"AWS_REGION\": aws_region,\n \"AWS_PAGER\": \"\",\n },\n opts = pulumi.ResourceOptions(depends_on=[lambda_function]))\n\npulumi.export(\"output\", invoke_command.stdout)\n```\n\n```go\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/iam\"\n\t\"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/lambda\"\n\t\"github.com/pulumi/pulumi-command/sdk/go/command/local\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\tawsConfig := config.New(ctx, \"aws\")\n\t\tawsRegion := awsConfig.Require(\"region\")\n\n\t\ttmpJSON0, err := json.Marshal(map[string]interface{}{\n\t\t\t\"Version\": \"2012-10-17\",\n\t\t\t\"Statement\": []map[string]interface{}{\n\t\t\t\t{\n\t\t\t\t\t\"Action\": \"sts:AssumeRole\",\n\t\t\t\t\t\"Effect\": \"Allow\",\n\t\t\t\t\t\"Principal\": map[string]interface{}{\n\t\t\t\t\t\t\"Service\": \"lambda.amazonaws.com\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tjson0 := string(tmpJSON0)\n\t\tlambdaRole, err := iam.NewRole(ctx, \"lambdaRole\", &iam.RoleArgs{\n\t\t\tAssumeRolePolicy: pulumi.String(json0),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlambdaFunction, err := lambda.NewFunction(ctx, \"lambdaFunction\", &lambda.FunctionArgs{\n\t\t\tName: pulumi.String(\"f\"),\n\t\t\tPublish: pulumi.Bool(true),\n\t\t\tRole: lambdaRole.Arn,\n\t\t\tHandler: pulumi.String(\"index.handler\"),\n\t\t\tRuntime: pulumi.String(lambda.RuntimeNodeJS20dX),\n\t\t\tCode: pulumi.NewFileArchive(\"./handler\"),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tinvokeCommand, err := local.NewCommand(ctx, \"invokeCommand\", &local.CommandArgs{\n\t\t\tCreate: pulumi.String(fmt.Sprintf(\"aws lambda invoke --function-name \\\"$FN\\\" --payload '{\\\"stackName\\\": \\\"%v\\\"}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '\\\"' && rm out.txt\", ctx.Stack())),\n\t\t\tEnvironment: pulumi.StringMap{\n\t\t\t\t\"FN\": lambdaFunction.Arn,\n\t\t\t\t\"AWS_REGION\": pulumi.String(awsRegion),\n\t\t\t\t\"AWS_PAGER\": pulumi.String(\"\"),\n\t\t\t},\n\t\t}, pulumi.DependsOn([]pulumi.Resource{\n\t\t\tlambdaFunction,\n\t\t}))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tctx.Export(\"output\", invokeCommand.Stdout)\n\t\treturn nil\n\t})\n}\n```\n\n```csharp\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing Pulumi;\nusing Aws = Pulumi.Aws;\nusing Command = Pulumi.Command;\n\nreturn await Deployment.RunAsync(() => \n{\n var awsConfig = new Config(\"aws\");\n\n var lambdaRole = new Aws.Iam.Role(\"lambdaRole\", new()\n {\n AssumeRolePolicy = JsonSerializer.Serialize(new Dictionary\n {\n [\"Version\"] = \"2012-10-17\",\n [\"Statement\"] = new[]\n {\n new Dictionary\n {\n [\"Action\"] = \"sts:AssumeRole\",\n [\"Effect\"] = \"Allow\",\n [\"Principal\"] = new Dictionary\n {\n [\"Service\"] = \"lambda.amazonaws.com\",\n },\n },\n },\n }),\n });\n\n var lambdaFunction = new Aws.Lambda.Function(\"lambdaFunction\", new()\n {\n Name = \"f\",\n Publish = true,\n Role = lambdaRole.Arn,\n Handler = \"index.handler\",\n Runtime = Aws.Lambda.Runtime.NodeJS20dX,\n Code = new FileArchive(\"./handler\"),\n });\n\n var invokeCommand = new Command.Local.Command(\"invokeCommand\", new()\n {\n Create = $\"aws lambda invoke --function-name \\\"$FN\\\" --payload '{{\\\"stackName\\\": \\\"{Deployment.Instance.StackName}\\\"}}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '\\\"' && rm out.txt\",\n Environment = \n {\n { \"FN\", lambdaFunction.Arn },\n { \"AWS_REGION\", awsConfig.Require(\"region\") },\n { \"AWS_PAGER\", \"\" },\n },\n }, new CustomResourceOptions\n {\n DependsOn =\n {\n lambdaFunction,\n },\n });\n\n return new Dictionary\n {\n [\"output\"] = invokeCommand.Stdout,\n };\n});\n```\n\n```java\npackage generated_program;\n\nimport com.pulumi.Context;\nimport com.pulumi.Pulumi;\nimport com.pulumi.aws.iam.Role;\nimport com.pulumi.aws.iam.RoleArgs;\nimport com.pulumi.aws.lambda.Function;\nimport com.pulumi.aws.lambda.FunctionArgs;\nimport com.pulumi.command.local.Command;\nimport com.pulumi.command.local.CommandArgs;\nimport static com.pulumi.codegen.internal.Serialization.*;\nimport com.pulumi.resources.CustomResourceOptions;\nimport com.pulumi.asset.FileArchive;\nimport java.util.Map;\n\npublic class App {\n public static void main(String[] args) {\n Pulumi.run(App::stack);\n }\n\n public static void stack(Context ctx) {\n var awsConfig = ctx.config(\"aws\");\n var awsRegion = awsConfig.require(\"region\");\n\n var lambdaRole = new Role(\"lambdaRole\", RoleArgs.builder()\n .assumeRolePolicy(serializeJson(\n jsonObject(\n jsonProperty(\"Version\", \"2012-10-17\"),\n jsonProperty(\"Statement\", jsonArray(jsonObject(\n jsonProperty(\"Action\", \"sts:AssumeRole\"),\n jsonProperty(\"Effect\", \"Allow\"),\n jsonProperty(\"Principal\", jsonObject(\n jsonProperty(\"Service\", \"lambda.amazonaws.com\")))))))))\n .build());\n\n var lambdaFunction = new Function(\"lambdaFunction\", FunctionArgs.builder()\n .name(\"f\")\n .publish(true)\n .role(lambdaRole.arn())\n .handler(\"index.handler\")\n .runtime(\"nodejs20.x\")\n .code(new FileArchive(\"./handler\"))\n .build());\n\n // Work around the lack of Output.all for Maps in Java. We cannot use a plain Map because\n // `lambdaFunction.arn()` is an Output.\n var invokeEnv = Output.tuple(\n Output.of(\"FN\"), lambdaFunction.arn(),\n Output.of(\"AWS_REGION\"), Output.of(awsRegion),\n Output.of(\"AWS_PAGER\"), Output.of(\"\")\n ).applyValue(t -> Map.of(t.t1, t.t2, t.t3, t.t4, t.t5, t.t6));\n\n var invokeCommand = new Command(\"invokeCommand\", CommandArgs.builder()\n .create(String.format(\n \"aws lambda invoke --function-name \\\"$FN\\\" --payload '{\\\"stackName\\\": \\\"%s\\\"}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '\\\"' && rm out.txt\",\n ctx.stackName()))\n .environment(invokeEnv)\n .build(),\n CustomResourceOptions.builder()\n .dependsOn(lambdaFunction)\n .build());\n\n ctx.export(\"output\", invokeCommand.stdout());\n }\n}\n```\n\n```yaml\nresources:\n lambdaRole:\n type: aws:iam:Role\n properties:\n assumeRolePolicy:\n fn::toJSON:\n Version: \"2012-10-17\"\n Statement:\n - Action: sts:AssumeRole\n Effect: Allow\n Principal:\n Service: lambda.amazonaws.com\n\n lambdaFunction:\n type: aws:lambda:Function\n properties:\n name: f\n publish: true\n role: ${lambdaRole.arn}\n handler: index.handler\n runtime: \"nodejs20.x\"\n code:\n fn::fileArchive: ./handler\n\n invokeCommand:\n type: command:local:Command\n properties:\n create: 'aws lambda invoke --function-name \"$FN\" --payload ''{\"stackName\": \"${pulumi.stack}\"}'' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d ''\"'' && rm out.txt'\n environment:\n FN: ${lambdaFunction.arn}\n AWS_REGION: ${aws:region}\n AWS_PAGER: \"\"\n options:\n dependsOn:\n - ${lambdaFunction}\n\noutputs:\n output: ${invokeCommand.stdout}\n```\n\n{{% /example %}}\n\n{{% example %}}\n\n### Using Triggers\n\nThis example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run.\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as command from \"@pulumi/command\";\nimport * as random from \"@pulumi/random\";\n\nconst str = \"foo\";\nconst fileAsset = new pulumi.asset.FileAsset(\"Pulumi.yaml\");\nconst rand = new random.RandomString(\"rand\", {length: 5});\nconst localFile = new command.local.Command(\"localFile\", {\n create: \"touch foo.txt\",\n archivePaths: [\"*.txt\"],\n});\n\nconst cmd = new command.local.Command(\"cmd\", {\n create: \"echo create > op.txt\",\n delete: \"echo delete >> op.txt\",\n triggers: [\n str,\n rand.result,\n fileAsset,\n localFile.archive,\n ],\n});\n```\n\n```python\nimport pulumi\nimport pulumi_command as command\nimport pulumi_random as random\n\nfoo = \"foo\"\nfile_asset_var = pulumi.FileAsset(\"Pulumi.yaml\")\nrand = random.RandomString(\"rand\", length=5)\nlocal_file = command.local.Command(\"localFile\",\n create=\"touch foo.txt\",\n archive_paths=[\"*.txt\"])\n\ncmd = command.local.Command(\"cmd\",\n create=\"echo create > op.txt\",\n delete=\"echo delete >> op.txt\",\n triggers=[\n foo,\n rand.result,\n file_asset_var,\n local_file.archive,\n ])\n```\n\n```go\npackage main\n\nimport (\n\t\"github.com/pulumi/pulumi-command/sdk/go/command/local\"\n\t\"github.com/pulumi/pulumi-random/sdk/v4/go/random\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\tstr := pulumi.String(\"foo\")\n\n\t\tfileAsset := pulumi.NewFileAsset(\"Pulumi.yaml\")\n\n\t\trand, err := random.NewRandomString(ctx, \"rand\", &random.RandomStringArgs{\n\t\t\tLength: pulumi.Int(5),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlocalFile, err := local.NewCommand(ctx, \"localFile\", &local.CommandArgs{\n\t\t\tCreate: pulumi.String(\"touch foo.txt\"),\n\t\t\tArchivePaths: pulumi.StringArray{\n\t\t\t\tpulumi.String(\"*.txt\"),\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t_, err = local.NewCommand(ctx, \"cmd\", &local.CommandArgs{\n\t\t\tCreate: pulumi.String(\"echo create > op.txt\"),\n\t\t\tDelete: pulumi.String(\"echo delete >> op.txt\"),\n\t\t\tTriggers: pulumi.Array{\n\t\t\t\tstr,\n\t\t\t\trand.Result,\n\t\t\t\tfileAsset,\n\t\t\t\tlocalFile.Archive,\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n```\n\n```csharp\nusing Pulumi;\nusing Command = Pulumi.Command;\nusing Random = Pulumi.Random;\n\nreturn await Deployment.RunAsync(() =>\n{\n var str = \"foo\";\n\n var fileAssetVar = new FileAsset(\"Pulumi.yaml\");\n\n var rand = new Random.RandomString(\"rand\", new()\n {\n Length = 5,\n });\n\n var localFile = new Command.Local.Command(\"localFile\", new()\n {\n Create = \"touch foo.txt\",\n ArchivePaths = new[]\n {\n \"*.txt\",\n },\n });\n\n var cmd = new Command.Local.Command(\"cmd\", new()\n {\n Create = \"echo create > op.txt\",\n Delete = \"echo delete >> op.txt\",\n Triggers = new object[]\n {\n str,\n rand.Result,\n fileAssetVar,\n localFile.Archive,\n },\n });\n\n});\n```\n\n```java\npublic class App {\n public static void main(String[] args) {\n Pulumi.run(App::stack);\n }\n\n public static void stack(Context ctx) {\n final var fileAssetVar = new FileAsset(\"Pulumi.yaml\");\n\n var rand = new RandomString(\"rand\", RandomStringArgs.builder()\n .length(5)\n .build());\n\n var localFile = new Command(\"localFile\", CommandArgs.builder()\n .create(\"touch foo.txt\")\n .archivePaths(\"*.txt\")\n .build());\n\n var cmd = new Command(\"cmd\", CommandArgs.builder()\n .create(\"echo create > op.txt\")\n .delete(\"echo delete >> op.txt\")\n .triggers(\n rand.result(),\n fileAssetVar,\n localFile.archive())\n .build());\n\n }\n}\n```\n\n```yaml\nconfig: {}\noutputs: {}\nresources:\n rand:\n type: random:index/randomString:RandomString\n properties:\n length: 5\n\n localFile:\n type: command:local:Command\n properties:\n create: touch foo.txt\n archivePaths:\n - \"*.txt\"\n\n cmd:\n type: command:local:Command\n properties:\n create: echo create > op.txt\n delete: echo delete >> op.txt\n triggers:\n - ${rand.result}\n - ${fileAsset}\n - ${localFile.archive}\n\nvariables:\n fileAsset:\n fn::fileAsset: \"Pulumi.yaml\"\n```\n\n{{% /example %}}\n\n{{% /examples %}}", "properties": { "addPreviousOutputInEnv": { "type": "boolean", @@ -366,7 +366,7 @@ } }, "command:remote:Command": { - "description": "A command to run on a remote host. The connection is established via ssh.\n\n{{% examples %}}\n## Example Usage\n\n{{% example %}}\n### Triggers\n\nThis example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run.\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as command from \"@pulumi/command\";\nimport * as random from \"@pulumi/random\";\n\nconst str = \"foo\";\nconst fileAsset = new pulumi.asset.FileAsset(\"Pulumi.yaml\");\nconst rand = new random.RandomString(\"rand\", {length: 5});\nconst localFile = new command.local.Command(\"localFile\", {\n create: \"touch foo.txt\",\n archivePaths: [\"*.txt\"],\n});\nconst cmd = new command.remote.Command(\"cmd\", {\n connection: {\n host: \"insert host here\",\n },\n create: \"echo create > op.txt\",\n delete: \"echo delete >> op.txt\",\n triggers: [\n str,\n rand.result,\n fileAsset,\n localFile.archive,\n ],\n});\n\n```\n\n```python\nimport pulumi\nimport pulumi_command as command\nimport pulumi_random as random\n\nfoo = \"foo\"\nfile_asset_var = pulumi.FileAsset(\"Pulumi.yaml\")\nrand = random.RandomString(\"rand\", length=5)\nlocal_file = command.local.Command(\"localFile\",\n create=\"touch foo.txt\",\n archive_paths=[\"*.txt\"])\n\ncmd = command.remote.Command(\"cmd\",\n connection=command.remote.ConnectionArgs(\n host=\"insert host here\",\n ),\n create=\"echo create > op.txt\",\n delete=\"echo delete >> op.txt\",\n triggers=[\n foo,\n rand.result,\n file_asset_var,\n local_file.archive,\n ])\n```\n\n```go\npackage main\n\nimport (\n\t\"github.com/pulumi/pulumi-command/sdk/go/command/local\"\n\t\"github.com/pulumi/pulumi-command/sdk/go/command/remote\"\n\t\"github.com/pulumi/pulumi-random/sdk/v4/go/random\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\tstr := pulumi.String(\"foo\")\n\n\t\tfileAsset := pulumi.NewFileAsset(\"Pulumi.yaml\")\n\n\t\trand, err := random.NewRandomString(ctx, \"rand\", &random.RandomStringArgs{\n\t\t\tLength: pulumi.Int(5),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlocalFile, err := local.NewCommand(ctx, \"localFile\", &local.CommandArgs{\n\t\t\tCreate: pulumi.String(\"touch foo.txt\"),\n\t\t\tArchivePaths: pulumi.StringArray{\n\t\t\t\tpulumi.String(\"*.txt\"),\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t_, err = remote.NewCommand(ctx, \"cmd\", &remote.CommandArgs{\n\t\t\tConnection: &remote.ConnectionArgs{\n\t\t\t\tHost: pulumi.String(\"insert host here\"),\n\t\t\t},\n\t\t\tCreate: pulumi.String(\"echo create > op.txt\"),\n\t\t\tDelete: pulumi.String(\"echo delete >> op.txt\"),\n\t\t\tTriggers: pulumi.Array{\n\t\t\t\tstr,\n\t\t\t\trand.Result,\n\t\t\t\tfileAsset,\n\t\t\t\tlocalFile.Archive,\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n```\n\n```csharp\nusing Pulumi;\nusing Command = Pulumi.Command;\nusing Random = Pulumi.Random;\n\nreturn await Deployment.RunAsync(() => \n{\n var str = \"foo\";\n\n var fileAssetVar = new FileAsset(\"Pulumi.yaml\");\n\n var rand = new Random.RandomString(\"rand\", new()\n {\n Length = 5,\n });\n\n var localFile = new Command.Local.Command(\"localFile\", new()\n {\n Create = \"touch foo.txt\",\n ArchivePaths = new[]\n {\n \"*.txt\",\n },\n });\n\n var cmd = new Command.Remote.Command(\"cmd\", new()\n {\n Connection = new Command.Remote.Inputs.ConnectionArgs\n {\n Host = \"insert host here\",\n },\n Create = \"echo create > op.txt\",\n Delete = \"echo delete >> op.txt\",\n Triggers = new object[]\n {\n str,\n rand.Result,\n fileAssetVar,\n localFile.Archive,\n },\n });\n\n});\n```\n\n```java\npublic class App {\n public static void main(String[] args) {\n Pulumi.run(App::stack);\n }\n\n public static void stack(Context ctx) {\n final var fileAssetVar = new FileAsset(\"Pulumi.yaml\");\n\n var rand = new RandomString(\"rand\", RandomStringArgs.builder()\n .length(5)\n .build());\n\n var localFile = new Command(\"localFile\", CommandArgs.builder()\n .create(\"touch foo.txt\")\n .archivePaths(\"*.txt\")\n .build());\n\n var cmd = new Command(\"cmd\", CommandArgs.builder()\n .connection(ConnectionArgs.builder()\n .host(\"insert host here\")\n .build())\n .create(\"echo create > op.txt\")\n .delete(\"echo delete >> op.txt\")\n .triggers( \n rand.result(),\n fileAssetVar,\n localFile.archive())\n .build());\n\n }\n}\n```\n\n```yaml\nconfig: {}\noutputs: {}\n\nresources:\n rand:\n type: random:index/randomString:RandomString\n properties:\n length: 5\n\n localFile:\n type: command:local:Command\n properties:\n create: touch foo.txt\n archivePaths:\n - \"*.txt\"\n\n cmd:\n type: command:remote:Command\n properties:\n connection:\n host: \"insert host here\"\n create: echo create > op.txt\n delete: echo delete >> op.txt\n triggers:\n - ${rand.result}\n - ${fileAsset}\n - ${localFile.archive}\n\nvariables:\n fileAsset:\n fn::fileAsset: \"Pulumi.yaml\"\n```\n\n{{% /example %}}\n\n{{% /examples %}}", + "description": "A command to run on a remote host. The connection is established via ssh.\n\n{{% examples %}}\n\n## Example Usage\n\n{{% example %}}\n\n### A Basic Example\nThis program connects to a server and runs the `hostname` command. The output is then available via the `stdout` property.\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as command from \"@pulumi/command\";\n\nconst config = new pulumi.Config();\nconst server = config.require(\"server\");\nconst userName = config.require(\"userName\");\nconst privateKey = config.require(\"privateKey\");\n\nconst hostnameCmd = new command.remote.Command(\"hostnameCmd\", {\n create: \"hostname\",\n connection: {\n host: server,\n user: userName,\n privateKey: privateKey,\n },\n});\nexport const hostname = hostnameCmd.stdout;\n```\n\n```python\nimport pulumi\nimport pulumi_command as command\n\nconfig = pulumi.Config()\nserver = config.require(\"server\")\nuser_name = config.require(\"userName\")\nprivate_key = config.require(\"privateKey\")\nhostname_cmd = command.remote.Command(\"hostnameCmd\",\n create=\"hostname\",\n connection=command.remote.ConnectionArgs(\n host=server,\n user=user_name,\n private_key=private_key,\n ))\npulumi.export(\"hostname\", hostname_cmd.stdout)\n```\n\n```go\npackage main\n\nimport (\n\t\"github.com/pulumi/pulumi-command/sdk/go/command/remote\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\tcfg := config.New(ctx, \"\")\n\t\tserver := cfg.Require(\"server\")\n\t\tuserName := cfg.Require(\"userName\")\n\t\tprivateKey := cfg.Require(\"privateKey\")\n\t\thostnameCmd, err := remote.NewCommand(ctx, \"hostnameCmd\", &remote.CommandArgs{\n\t\t\tCreate: pulumi.String(\"hostname\"),\n\t\t\tConnection: &remote.ConnectionArgs{\n\t\t\t\tHost: pulumi.String(server),\n\t\t\t\tUser: pulumi.String(userName),\n\t\t\t\tPrivateKey: pulumi.String(privateKey),\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tctx.Export(\"hostname\", hostnameCmd.Stdout)\n\t\treturn nil\n\t})\n}\n```\n\n```csharp\nusing System.Collections.Generic;\nusing System.Linq;\nusing Pulumi;\nusing Command = Pulumi.Command;\n\nreturn await Deployment.RunAsync(() =>\n{\n var config = new Config();\n var server = config.Require(\"server\");\n var userName = config.Require(\"userName\");\n var privateKey = config.Require(\"privateKey\");\n var hostnameCmd = new Command.Remote.Command(\"hostnameCmd\", new()\n {\n Create = \"hostname\",\n Connection = new Command.Remote.Inputs.ConnectionArgs\n {\n Host = server,\n User = userName,\n PrivateKey = privateKey,\n },\n });\n\n return new Dictionary\n {\n [\"hostname\"] = hostnameCmd.Stdout,\n };\n});\n```\n\n```java\npackage generated_program;\n\nimport com.pulumi.Context;\nimport com.pulumi.Pulumi;\nimport com.pulumi.core.Output;\nimport com.pulumi.command.remote.Command;\nimport com.pulumi.command.remote.CommandArgs;\nimport com.pulumi.command.remote.inputs.ConnectionArgs;\n\npublic class App {\n public static void main(String[] args) {\n Pulumi.run(App::stack);\n }\n\n public static void stack(Context ctx) {\n final var config = ctx.config();\n final var server = config.require(\"server\");\n final var userName = config.require(\"userName\");\n final var privateKey = config.require(\"privateKey\");\n var hostnameCmd = new Command(\"hostnameCmd\", CommandArgs.builder()\n .create(\"hostname\")\n .connection(ConnectionArgs.builder()\n .host(server)\n .user(userName)\n .privateKey(privateKey)\n .build())\n .build());\n\n ctx.export(\"hostname\", hostnameCmd.stdout());\n }\n}\n```\n\n```yaml\noutputs:\n hostname: ${hostnameCmd.stdout}\n\nconfig:\n server:\n type: string\n userName:\n type: string\n privateKey:\n type: string\n\nresources:\n hostnameCmd:\n type: command:remote:Command\n properties:\n create: \"hostname\"\n # The configuration of our SSH connection to the server.\n connection:\n host: ${server}\n user: ${userName}\n privateKey: ${privateKey}\n```\n\n{{% /example %}}\n\n{{% example %}}\n\n### Triggers\nThis example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run.\n\n{{% example %}}\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as command from \"@pulumi/command\";\nimport * as random from \"@pulumi/random\";\n\nconst str = \"foo\";\nconst fileAsset = new pulumi.asset.FileAsset(\"Pulumi.yaml\");\nconst rand = new random.RandomString(\"rand\", {length: 5});\nconst localFile = new command.local.Command(\"localFile\", {\n create: \"touch foo.txt\",\n archivePaths: [\"*.txt\"],\n});\nconst cmd = new command.remote.Command(\"cmd\", {\n connection: {\n host: \"insert host here\",\n },\n create: \"echo create > op.txt\",\n delete: \"echo delete >> op.txt\",\n triggers: [\n str,\n rand.result,\n fileAsset,\n localFile.archive,\n ],\n});\n\n```\n\n```python\nimport pulumi\nimport pulumi_command as command\nimport pulumi_random as random\n\nfoo = \"foo\"\nfile_asset_var = pulumi.FileAsset(\"Pulumi.yaml\")\nrand = random.RandomString(\"rand\", length=5)\nlocal_file = command.local.Command(\"localFile\",\n create=\"touch foo.txt\",\n archive_paths=[\"*.txt\"])\n\ncmd = command.remote.Command(\"cmd\",\n connection=command.remote.ConnectionArgs(\n host=\"insert host here\",\n ),\n create=\"echo create > op.txt\",\n delete=\"echo delete >> op.txt\",\n triggers=[\n foo,\n rand.result,\n file_asset_var,\n local_file.archive,\n ])\n```\n\n```go\npackage main\n\nimport (\n\t\"github.com/pulumi/pulumi-command/sdk/go/command/local\"\n\t\"github.com/pulumi/pulumi-command/sdk/go/command/remote\"\n\t\"github.com/pulumi/pulumi-random/sdk/v4/go/random\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\tstr := pulumi.String(\"foo\")\n\n\t\tfileAsset := pulumi.NewFileAsset(\"Pulumi.yaml\")\n\n\t\trand, err := random.NewRandomString(ctx, \"rand\", &random.RandomStringArgs{\n\t\t\tLength: pulumi.Int(5),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlocalFile, err := local.NewCommand(ctx, \"localFile\", &local.CommandArgs{\n\t\t\tCreate: pulumi.String(\"touch foo.txt\"),\n\t\t\tArchivePaths: pulumi.StringArray{\n\t\t\t\tpulumi.String(\"*.txt\"),\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t_, err = remote.NewCommand(ctx, \"cmd\", &remote.CommandArgs{\n\t\t\tConnection: &remote.ConnectionArgs{\n\t\t\t\tHost: pulumi.String(\"insert host here\"),\n\t\t\t},\n\t\t\tCreate: pulumi.String(\"echo create > op.txt\"),\n\t\t\tDelete: pulumi.String(\"echo delete >> op.txt\"),\n\t\t\tTriggers: pulumi.Array{\n\t\t\t\tstr,\n\t\t\t\trand.Result,\n\t\t\t\tfileAsset,\n\t\t\t\tlocalFile.Archive,\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n```\n\n```csharp\nusing Pulumi;\nusing Command = Pulumi.Command;\nusing Random = Pulumi.Random;\n\nreturn await Deployment.RunAsync(() =>\n{\n var str = \"foo\";\n\n var fileAssetVar = new FileAsset(\"Pulumi.yaml\");\n\n var rand = new Random.RandomString(\"rand\", new()\n {\n Length = 5,\n });\n\n var localFile = new Command.Local.Command(\"localFile\", new()\n {\n Create = \"touch foo.txt\",\n ArchivePaths = new[]\n {\n \"*.txt\",\n },\n });\n\n var cmd = new Command.Remote.Command(\"cmd\", new()\n {\n Connection = new Command.Remote.Inputs.ConnectionArgs\n {\n Host = \"insert host here\",\n },\n Create = \"echo create > op.txt\",\n Delete = \"echo delete >> op.txt\",\n Triggers = new object[]\n {\n str,\n rand.Result,\n fileAssetVar,\n localFile.Archive,\n },\n });\n\n});\n```\n\n```java\npublic class App {\n public static void main(String[] args) {\n Pulumi.run(App::stack);\n }\n\n public static void stack(Context ctx) {\n final var fileAssetVar = new FileAsset(\"Pulumi.yaml\");\n\n var rand = new RandomString(\"rand\", RandomStringArgs.builder()\n .length(5)\n .build());\n\n var localFile = new Command(\"localFile\", CommandArgs.builder()\n .create(\"touch foo.txt\")\n .archivePaths(\"*.txt\")\n .build());\n\n var cmd = new Command(\"cmd\", CommandArgs.builder()\n .connection(ConnectionArgs.builder()\n .host(\"insert host here\")\n .build())\n .create(\"echo create > op.txt\")\n .delete(\"echo delete >> op.txt\")\n .triggers( \n rand.result(),\n fileAssetVar,\n localFile.archive())\n .build());\n\n }\n}\n```\n\n```yaml\nconfig: {}\noutputs: {}\n\nresources:\n rand:\n type: random:index/randomString:RandomString\n properties:\n length: 5\n\n localFile:\n type: command:local:Command\n properties:\n create: touch foo.txt\n archivePaths:\n - \"*.txt\"\n\n cmd:\n type: command:remote:Command\n properties:\n connection:\n host: \"insert host here\"\n create: echo create > op.txt\n delete: echo delete >> op.txt\n triggers:\n - ${rand.result}\n - ${fileAsset}\n - ${localFile.archive}\n\nvariables:\n fileAsset:\n fn::fileAsset: \"Pulumi.yaml\"\n```\n\n{{% /example %}}\n\n{{% /examples %}}", "properties": { "addPreviousOutputInEnv": { "type": "boolean", @@ -539,7 +539,7 @@ "deprecationMessage": "This resource is deprecated and will be removed in a future release. Please use the `CopyToRemote` resource instead." }, "command:remote:CopyToRemote": { - "description": "Copy an Asset or Archive to a remote host.", + "description": "Copy an Asset or Archive to a remote host.\n\n{{% examples %}}\n\n## Example usage\n\nThis example copies a local directory to a remote host via SSH. For brevity, the remote server is assumed to exist, but it could also be provisioned in the same Pulumi program. \n\n{{% example %}}\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { remote, types } from \"@pulumi/command\";\nimport * as fs from \"fs\";\nimport * as os from \"os\";\nimport * as path from \"path\";\n\nexport = async () => {\n const config = new pulumi.Config();\n\n // Get the private key to connect to the server. If a key is\n // provided, use it, otherwise default to the standard id_rsa SSH key.\n const privateKeyBase64 = config.get(\"privateKeyBase64\");\n const privateKey = privateKeyBase64 ?\n Buffer.from(privateKeyBase64, 'base64').toString('ascii') :\n fs.readFileSync(path.join(os.homedir(), \".ssh\", \"id_rsa\")).toString(\"utf8\");\n\n const serverPublicIp = config.require(\"serverPublicIp\");\n const userName = config.require(\"userName\");\n\n // The configuration of our SSH connection to the instance.\n const connection: types.input.remote.ConnectionArgs = {\n host: serverPublicIp,\n user: userName,\n privateKey: privateKey,\n };\n\n // Set up source and target of the remote copy.\n const from = config.require(\"payload\")!;\n const archive = new pulumi.asset.FileArchive(from);\n const to = config.require(\"destDir\")!;\n\n // Copy the files to the remote.\n const copy = new remote.CopyToRemote(\"copy\", {\n connection,\n source: archive,\n remotePath: to,\n });\n\n // Verify that the expected files were copied to the remote.\n // We want to run this after each copy, i.e., when something changed,\n // so we use the asset to be copied as a trigger.\n const find = new remote.Command(\"ls\", {\n connection,\n create: `find ${to}/${from} | sort`,\n triggers: [archive],\n }, { dependsOn: copy });\n\n return {\n remoteContents: find.stdout\n }\n}\n```\n\n```python\nimport pulumi\nimport pulumi_command as command\n\nconfig = pulumi.Config()\n\nserver_public_ip = config.require(\"serverPublicIp\")\nuser_name = config.require(\"userName\")\nprivate_key = config.require(\"privateKey\")\npayload = config.require(\"payload\")\ndest_dir = config.require(\"destDir\")\n\narchive = pulumi.FileArchive(payload)\n\n# The configuration of our SSH connection to the instance.\nconn = command.remote.ConnectionArgs(\n host = server_public_ip,\n user = user_name,\n privateKey = private_key,\n)\n\n# Copy the files to the remote.\ncopy = command.remote.CopyToRemote(\"copy\",\n connection=conn,\n source=archive,\n destination=dest_dir)\n\n# Verify that the expected files were copied to the remote.\n# We want to run this after each copy, i.e., when something changed,\n# so we use the asset to be copied as a trigger.\nfind = command.remote.Command(\"find\",\n connection=conn,\n create=f\"find {dest_dir}/{payload} | sort\",\n triggers=[archive],\n opts = pulumi.ResourceOptions(depends_on=[copy]))\n\npulumi.export(\"remoteContents\", find.stdout)\n```\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pulumi/pulumi-command/sdk/go/command/remote\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\tcfg := config.New(ctx, \"\")\n\t\tserverPublicIp := cfg.Require(\"serverPublicIp\")\n\t\tuserName := cfg.Require(\"userName\")\n\t\tprivateKey := cfg.Require(\"privateKey\")\n\t\tpayload := cfg.Require(\"payload\")\n\t\tdestDir := cfg.Require(\"destDir\")\n\n\t\tarchive := pulumi.NewFileArchive(payload)\n\n\t\tconn := remote.ConnectionArgs{\n\t\t\tHost: pulumi.String(serverPublicIp),\n\t\t\tUser: pulumi.String(userName),\n\t\t\tPrivateKey: pulumi.String(privateKey),\n\t\t}\n\n\t\tcopy, err := remote.NewCopyToRemote(ctx, \"copy\", &remote.CopyToRemoteArgs{\n\t\t\tConnection: conn,\n\t\t\tSource: archive,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfind, err := remote.NewCommand(ctx, \"find\", &remote.CommandArgs{\n\t\t\tConnection: conn,\n\t\t\tCreate: pulumi.String(fmt.Sprintf(\"find %v/%v | sort\", destDir, payload)),\n\t\t\tTriggers: pulumi.Array{\n\t\t\t\tarchive,\n\t\t\t},\n\t\t}, pulumi.DependsOn([]pulumi.Resource{\n\t\t\tcopy,\n\t\t}))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tctx.Export(\"remoteContents\", find.Stdout)\n\t\treturn nil\n\t})\n}\n```\n\n```csharp\nusing System.Collections.Generic;\nusing Pulumi;\nusing Command = Pulumi.Command;\n\nreturn await Deployment.RunAsync(() => \n{\n var config = new Config();\n var serverPublicIp = config.Require(\"serverPublicIp\");\n var userName = config.Require(\"userName\");\n var privateKey = config.Require(\"privateKey\");\n var payload = config.Require(\"payload\");\n var destDir = config.Require(\"destDir\");\n \n var archive = new FileArchive(payload);\n\n // The configuration of our SSH connection to the instance.\n var conn = new Command.Remote.Inputs.ConnectionArgs\n {\n Host = serverPublicIp,\n User = userName,\n PrivateKey = privateKey,\n };\n\n // Copy the files to the remote.\n var copy = new Command.Remote.CopyToRemote(\"copy\", new()\n {\n Connection = conn,\n Source = archive,\n });\n\n // Verify that the expected files were copied to the remote.\n // We want to run this after each copy, i.e., when something changed,\n // so we use the asset to be copied as a trigger.\n var find = new Command.Remote.Command(\"find\", new()\n {\n Connection = conn,\n Create = $\"find {destDir}/{payload} | sort\",\n Triggers = new[]\n {\n archive,\n },\n }, new CustomResourceOptions\n {\n DependsOn =\n {\n copy,\n },\n });\n\n return new Dictionary\n {\n [\"remoteContents\"] = find.Stdout,\n };\n});\n```\n\n```java\npackage generated_program;\n\nimport com.pulumi.Context;\nimport com.pulumi.Pulumi;\nimport com.pulumi.core.Output;\nimport com.pulumi.command.remote.Command;\nimport com.pulumi.command.remote.CommandArgs;\nimport com.pulumi.command.remote.CopyToRemote;\nimport com.pulumi.command.remote.inputs.*;\nimport com.pulumi.resources.CustomResourceOptions;\nimport com.pulumi.asset.FileArchive;\nimport java.util.List;\nimport java.util.ArrayList;\nimport java.util.Map;\nimport java.io.File;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\n\npublic class App {\n public static void main(String[] args) {\n Pulumi.run(App::stack);\n }\n\n public static void stack(Context ctx) {\n final var config = ctx.config();\n final var serverPublicIp = config.require(\"serverPublicIp\");\n final var userName = config.require(\"userName\");\n final var privateKey = config.require(\"privateKey\");\n final var payload = config.require(\"payload\");\n final var destDir = config.require(\"destDir\");\n\n final var archive = new FileArchive(payload);\n\n // The configuration of our SSH connection to the instance.\n final var conn = ConnectionArgs.builder()\n .host(serverPublicIp)\n .user(userName)\n .privateKey(privateKey)\n .build();\n\n // Copy the files to the remote.\n var copy = new CopyToRemote(\"copy\", CopyToRemoteArgs.builder()\n .connection(conn)\n .source(archive)\n .destination(destDir)\n .build());\n\n // Verify that the expected files were copied to the remote.\n // We want to run this after each copy, i.e., when something changed,\n // so we use the asset to be copied as a trigger.\n var find = new Command(\"find\", CommandArgs.builder()\n .connection(conn)\n .create(String.format(\"find %s/%s | sort\", destDir,payload))\n .triggers(archive)\n .build(), CustomResourceOptions.builder()\n .dependsOn(copy)\n .build());\n\n ctx.export(\"remoteContents\", find.stdout());\n }\n}\n```\n\n```yaml\nresources:\n # Copy the files to the remote.\n copy:\n type: command:remote:CopyToRemote\n properties:\n connection: ${conn}\n source: ${archive}\n remotePath: ${destDir}\n\n # Verify that the expected files were copied to the remote.\n # We want to run this after each copy, i.e., when something changed,\n # so we use the asset to be copied as a trigger.\n find:\n type: command:remote:Command\n properties:\n connection: ${conn}\n create: find ${destDir}/${payload} | sort\n triggers:\n - ${archive}\n options:\n dependsOn:\n - ${copy}\n\nconfig:\n serverPublicIp:\n type: string\n userName:\n type: string\n privateKey:\n type: string\n payload:\n type: string\n destDir:\n type: string\n\nvariables:\n # The source directory or archive to copy.\n archive:\n fn::fileArchive: ${payload}\n # The configuration of our SSH connection to the instance.\n conn:\n host: ${serverPublicIp}\n user: ${userName}\n privateKey: ${privateKey}\n\noutputs:\n remoteContents: ${find.stdout}\n```\n\n{{% /example %}}\n\n{{% /examples %}}", "properties": { "connection": { "$ref": "#/types/command:remote:Connection", diff --git a/provider/pkg/provider/local/command.md b/provider/pkg/provider/local/command.md index 2d633453..5de19233 100644 --- a/provider/pkg/provider/local/command.md +++ b/provider/pkg/provider/local/command.md @@ -3,10 +3,433 @@ A local command to be executed. This command can be inserted into the life cycles of other resources using the `dependsOn` or `parent` resource options. A command is considered to have failed when it finished with a non-zero exit code. This will fail the CRUD step of the `Command` resource. {{% examples %}} + ## Example Usage {{% example %}} -### Triggers + +### Basic Example + +This example shows the simplest use case, simply running a command on `create` in the Pulumi lifecycle. + +```typescript +import { local } from "@pulumi/command"; + +const random = new local.Command("random", { + create: "openssl rand -hex 16", +}); + +export const output = random.stdout; +``` + +```csharp +using System.Collections.Generic; +using Pulumi; +using Pulumi.Command.Local; + +await Deployment.RunAsync(() => +{ + var command = new Command("random", new CommandArgs + { + Create = "openssl rand -hex 16" + }); + + return new Dictionary + { + ["stdOut"] = command.Stdout + }; +}); +``` + +```python +import pulumi +from pulumi_command import local + +random = local.Command("random", + create="openssl rand -hex 16" +) + +pulumi.export("random", random.stdout) +``` + +```go +package main + +import ( + "github.com/pulumi/pulumi-command/sdk/go/command/local" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + random, err := local.NewCommand(ctx, "my-bucket", &local.CommandArgs{ + Create: pulumi.String("openssl rand -hex 16"), + }) + if err != nil { + return err + } + + ctx.Export("output", random.Stdout) + return nil + }) +} +``` + +```java +package generated_program; + +import com.pulumi.Context; +import com.pulumi.Pulumi; +import com.pulumi.command.local.Command; +import com.pulumi.command.local.CommandArgs; + +public class App { + public static void main(String[] args) { + Pulumi.run(App::stack); + } + + public static void stack(Context ctx) { + var random = new Command("random", CommandArgs.builder() + .create("openssl rand -hex 16") + .build()); + + ctx.export("rand", random.stdout()); + } +} +``` + +```yaml +outputs: + rand: "${random.stdout}" +resources: + random: + type: command:local:Command + properties: + create: "openssl rand -hex 16" +``` + +{{% /example %}} + +{{% example %}} + +### Invoking a Lambda during Pulumi Deployment + +This example show using a local command to invoke an AWS Lambda once it's deployed. The Lambda invocation could also depend on other resources. + +```typescript +import * as aws from "@pulumi/aws"; +import { local } from "@pulumi/command"; +import { getStack } from "@pulumi/pulumi"; + +const f = new aws.lambda.CallbackFunction("f", { + publish: true, + callback: async (ev: any) => { + return `Stack ${ev.stackName} is deployed!`; + } +}); + +const invoke = new local.Command("execf", { + create: `aws lambda invoke --function-name "$FN" --payload '{"stackName": "${getStack()}"}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '"' && rm out.txt`, + environment: { + FN: f.qualifiedArn, + AWS_REGION: aws.config.region!, + AWS_PAGER: "", + }, +}, { dependsOn: f }) + +export const output = invoke.stdout; +``` + +```python +import pulumi +import json +import pulumi_aws as aws +import pulumi_command as command + +lambda_role = aws.iam.Role("lambdaRole", assume_role_policy=json.dumps({ + "Version": "2012-10-17", + "Statement": [{ + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }], +})) + +lambda_function = aws.lambda_.Function("lambdaFunction", + name="f", + publish=True, + role=lambda_role.arn, + handler="index.handler", + runtime=aws.lambda_.Runtime.NODE_JS20D_X, + code=pulumi.FileArchive("./handler")) + +aws_config = pulumi.Config("aws") +aws_region = aws_config.require("region") + +invoke_command = command.local.Command("invokeCommand", + create=f"aws lambda invoke --function-name \"$FN\" --payload '{{\"stackName\": \"{pulumi.get_stack()}\"}}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '\"' && rm out.txt", + environment={ + "FN": lambda_function.arn, + "AWS_REGION": aws_region, + "AWS_PAGER": "", + }, + opts = pulumi.ResourceOptions(depends_on=[lambda_function])) + +pulumi.export("output", invoke_command.stdout) +``` + +```go +package main + +import ( + "encoding/json" + "fmt" + + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/iam" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/lambda" + "github.com/pulumi/pulumi-command/sdk/go/command/local" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + awsConfig := config.New(ctx, "aws") + awsRegion := awsConfig.Require("region") + + tmpJSON0, err := json.Marshal(map[string]interface{}{ + "Version": "2012-10-17", + "Statement": []map[string]interface{}{ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": map[string]interface{}{ + "Service": "lambda.amazonaws.com", + }, + }, + }, + }) + if err != nil { + return err + } + json0 := string(tmpJSON0) + lambdaRole, err := iam.NewRole(ctx, "lambdaRole", &iam.RoleArgs{ + AssumeRolePolicy: pulumi.String(json0), + }) + if err != nil { + return err + } + lambdaFunction, err := lambda.NewFunction(ctx, "lambdaFunction", &lambda.FunctionArgs{ + Name: pulumi.String("f"), + Publish: pulumi.Bool(true), + Role: lambdaRole.Arn, + Handler: pulumi.String("index.handler"), + Runtime: pulumi.String(lambda.RuntimeNodeJS20dX), + Code: pulumi.NewFileArchive("./handler"), + }) + if err != nil { + return err + } + invokeCommand, err := local.NewCommand(ctx, "invokeCommand", &local.CommandArgs{ + Create: pulumi.String(fmt.Sprintf("aws lambda invoke --function-name \"$FN\" --payload '{\"stackName\": \"%v\"}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '\"' && rm out.txt", ctx.Stack())), + Environment: pulumi.StringMap{ + "FN": lambdaFunction.Arn, + "AWS_REGION": pulumi.String(awsRegion), + "AWS_PAGER": pulumi.String(""), + }, + }, pulumi.DependsOn([]pulumi.Resource{ + lambdaFunction, + })) + if err != nil { + return err + } + ctx.Export("output", invokeCommand.Stdout) + return nil + }) +} +``` + +```csharp +using System.Collections.Generic; +using System.Text.Json; +using Pulumi; +using Aws = Pulumi.Aws; +using Command = Pulumi.Command; + +return await Deployment.RunAsync(() => +{ + var awsConfig = new Config("aws"); + + var lambdaRole = new Aws.Iam.Role("lambdaRole", new() + { + AssumeRolePolicy = JsonSerializer.Serialize(new Dictionary + { + ["Version"] = "2012-10-17", + ["Statement"] = new[] + { + new Dictionary + { + ["Action"] = "sts:AssumeRole", + ["Effect"] = "Allow", + ["Principal"] = new Dictionary + { + ["Service"] = "lambda.amazonaws.com", + }, + }, + }, + }), + }); + + var lambdaFunction = new Aws.Lambda.Function("lambdaFunction", new() + { + Name = "f", + Publish = true, + Role = lambdaRole.Arn, + Handler = "index.handler", + Runtime = Aws.Lambda.Runtime.NodeJS20dX, + Code = new FileArchive("./handler"), + }); + + var invokeCommand = new Command.Local.Command("invokeCommand", new() + { + Create = $"aws lambda invoke --function-name \"$FN\" --payload '{{\"stackName\": \"{Deployment.Instance.StackName}\"}}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '\"' && rm out.txt", + Environment = + { + { "FN", lambdaFunction.Arn }, + { "AWS_REGION", awsConfig.Require("region") }, + { "AWS_PAGER", "" }, + }, + }, new CustomResourceOptions + { + DependsOn = + { + lambdaFunction, + }, + }); + + return new Dictionary + { + ["output"] = invokeCommand.Stdout, + }; +}); +``` + +```java +package generated_program; + +import com.pulumi.Context; +import com.pulumi.Pulumi; +import com.pulumi.aws.iam.Role; +import com.pulumi.aws.iam.RoleArgs; +import com.pulumi.aws.lambda.Function; +import com.pulumi.aws.lambda.FunctionArgs; +import com.pulumi.command.local.Command; +import com.pulumi.command.local.CommandArgs; +import static com.pulumi.codegen.internal.Serialization.*; +import com.pulumi.resources.CustomResourceOptions; +import com.pulumi.asset.FileArchive; +import java.util.Map; + +public class App { + public static void main(String[] args) { + Pulumi.run(App::stack); + } + + public static void stack(Context ctx) { + var awsConfig = ctx.config("aws"); + var awsRegion = awsConfig.require("region"); + + var lambdaRole = new Role("lambdaRole", RoleArgs.builder() + .assumeRolePolicy(serializeJson( + jsonObject( + jsonProperty("Version", "2012-10-17"), + jsonProperty("Statement", jsonArray(jsonObject( + jsonProperty("Action", "sts:AssumeRole"), + jsonProperty("Effect", "Allow"), + jsonProperty("Principal", jsonObject( + jsonProperty("Service", "lambda.amazonaws.com"))))))))) + .build()); + + var lambdaFunction = new Function("lambdaFunction", FunctionArgs.builder() + .name("f") + .publish(true) + .role(lambdaRole.arn()) + .handler("index.handler") + .runtime("nodejs20.x") + .code(new FileArchive("./handler")) + .build()); + + // Work around the lack of Output.all for Maps in Java. We cannot use a plain Map because + // `lambdaFunction.arn()` is an Output. + var invokeEnv = Output.tuple( + Output.of("FN"), lambdaFunction.arn(), + Output.of("AWS_REGION"), Output.of(awsRegion), + Output.of("AWS_PAGER"), Output.of("") + ).applyValue(t -> Map.of(t.t1, t.t2, t.t3, t.t4, t.t5, t.t6)); + + var invokeCommand = new Command("invokeCommand", CommandArgs.builder() + .create(String.format( + "aws lambda invoke --function-name \"$FN\" --payload '{\"stackName\": \"%s\"}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '\"' && rm out.txt", + ctx.stackName())) + .environment(invokeEnv) + .build(), + CustomResourceOptions.builder() + .dependsOn(lambdaFunction) + .build()); + + ctx.export("output", invokeCommand.stdout()); + } +} +``` + +```yaml +resources: + lambdaRole: + type: aws:iam:Role + properties: + assumeRolePolicy: + fn::toJSON: + Version: "2012-10-17" + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: lambda.amazonaws.com + + lambdaFunction: + type: aws:lambda:Function + properties: + name: f + publish: true + role: ${lambdaRole.arn} + handler: index.handler + runtime: "nodejs20.x" + code: + fn::fileArchive: ./handler + + invokeCommand: + type: command:local:Command + properties: + create: 'aws lambda invoke --function-name "$FN" --payload ''{"stackName": "${pulumi.stack}"}'' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d ''"'' && rm out.txt' + environment: + FN: ${lambdaFunction.arn} + AWS_REGION: ${aws:region} + AWS_PAGER: "" + options: + dependsOn: + - ${lambdaFunction} + +outputs: + output: ${invokeCommand.stdout} +``` + +{{% /example %}} + +{{% example %}} + +### Using Triggers This example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run. @@ -210,6 +633,7 @@ variables: fileAsset: fn::fileAsset: "Pulumi.yaml" ``` + {{% /example %}} {{% /examples %}} \ No newline at end of file diff --git a/provider/pkg/provider/remote/command.md b/provider/pkg/provider/remote/command.md index 9002f4f7..3495ed08 100644 --- a/provider/pkg/provider/remote/command.md +++ b/provider/pkg/provider/remote/command.md @@ -1,13 +1,181 @@ A command to run on a remote host. The connection is established via ssh. {{% examples %}} + ## Example Usage {{% example %}} -### Triggers +### A Basic Example +This program connects to a server and runs the `hostname` command. The output is then available via the `stdout` property. + +```typescript +import * as pulumi from "@pulumi/pulumi"; +import * as command from "@pulumi/command"; + +const config = new pulumi.Config(); +const server = config.require("server"); +const userName = config.require("userName"); +const privateKey = config.require("privateKey"); + +const hostnameCmd = new command.remote.Command("hostnameCmd", { + create: "hostname", + connection: { + host: server, + user: userName, + privateKey: privateKey, + }, +}); +export const hostname = hostnameCmd.stdout; +``` + +```python +import pulumi +import pulumi_command as command + +config = pulumi.Config() +server = config.require("server") +user_name = config.require("userName") +private_key = config.require("privateKey") +hostname_cmd = command.remote.Command("hostnameCmd", + create="hostname", + connection=command.remote.ConnectionArgs( + host=server, + user=user_name, + private_key=private_key, + )) +pulumi.export("hostname", hostname_cmd.stdout) +``` + +```go +package main + +import ( + "github.com/pulumi/pulumi-command/sdk/go/command/remote" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + cfg := config.New(ctx, "") + server := cfg.Require("server") + userName := cfg.Require("userName") + privateKey := cfg.Require("privateKey") + hostnameCmd, err := remote.NewCommand(ctx, "hostnameCmd", &remote.CommandArgs{ + Create: pulumi.String("hostname"), + Connection: &remote.ConnectionArgs{ + Host: pulumi.String(server), + User: pulumi.String(userName), + PrivateKey: pulumi.String(privateKey), + }, + }) + if err != nil { + return err + } + ctx.Export("hostname", hostnameCmd.Stdout) + return nil + }) +} +``` + +```csharp +using System.Collections.Generic; +using System.Linq; +using Pulumi; +using Command = Pulumi.Command; + +return await Deployment.RunAsync(() => +{ + var config = new Config(); + var server = config.Require("server"); + var userName = config.Require("userName"); + var privateKey = config.Require("privateKey"); + var hostnameCmd = new Command.Remote.Command("hostnameCmd", new() + { + Create = "hostname", + Connection = new Command.Remote.Inputs.ConnectionArgs + { + Host = server, + User = userName, + PrivateKey = privateKey, + }, + }); + + return new Dictionary + { + ["hostname"] = hostnameCmd.Stdout, + }; +}); +``` + +```java +package generated_program; + +import com.pulumi.Context; +import com.pulumi.Pulumi; +import com.pulumi.core.Output; +import com.pulumi.command.remote.Command; +import com.pulumi.command.remote.CommandArgs; +import com.pulumi.command.remote.inputs.ConnectionArgs; + +public class App { + public static void main(String[] args) { + Pulumi.run(App::stack); + } + + public static void stack(Context ctx) { + final var config = ctx.config(); + final var server = config.require("server"); + final var userName = config.require("userName"); + final var privateKey = config.require("privateKey"); + var hostnameCmd = new Command("hostnameCmd", CommandArgs.builder() + .create("hostname") + .connection(ConnectionArgs.builder() + .host(server) + .user(userName) + .privateKey(privateKey) + .build()) + .build()); + + ctx.export("hostname", hostnameCmd.stdout()); + } +} +``` + +```yaml +outputs: + hostname: ${hostnameCmd.stdout} + +config: + server: + type: string + userName: + type: string + privateKey: + type: string + +resources: + hostnameCmd: + type: command:remote:Command + properties: + create: "hostname" + # The configuration of our SSH connection to the server. + connection: + host: ${server} + user: ${userName} + privateKey: ${privateKey} +``` + +{{% /example %}} + +{{% example %}} + +### Triggers This example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run. +{{% example %}} + ```typescript import * as pulumi from "@pulumi/pulumi"; import * as command from "@pulumi/command"; @@ -121,7 +289,7 @@ using Pulumi; using Command = Pulumi.Command; using Random = Pulumi.Random; -return await Deployment.RunAsync(() => +return await Deployment.RunAsync(() => { var str = "foo"; diff --git a/provider/pkg/provider/remote/copy.go b/provider/pkg/provider/remote/copy.go index e5cd4057..e37cef48 100644 --- a/provider/pkg/provider/remote/copy.go +++ b/provider/pkg/provider/remote/copy.go @@ -15,17 +15,22 @@ package remote import ( + _ "embed" + "github.com/pulumi/pulumi-go-provider/infer" "github.com/pulumi/pulumi-go-provider/infer/types" ) +//go:embed copyToRemote.md +var copyResourceDoc string + type CopyToRemote struct{} var _ = (infer.Annotated)((*CopyToRemote)(nil)) // Copy implements Annotate which allows you to attach descriptions to the Copy resource. func (c *CopyToRemote) Annotate(a infer.Annotator) { - a.Describe(&c, "Copy an Asset or Archive to a remote host.") + a.Describe(&c, copyResourceDoc) } type CopyToRemoteInputs struct { diff --git a/provider/pkg/provider/remote/copyToRemote.md b/provider/pkg/provider/remote/copyToRemote.md new file mode 100644 index 00000000..8499a336 --- /dev/null +++ b/provider/pkg/provider/remote/copyToRemote.md @@ -0,0 +1,332 @@ +Copy an Asset or Archive to a remote host. + +{{% examples %}} + +## Example usage + +This example copies a local directory to a remote host via SSH. For brevity, the remote server is assumed to exist, but it could also be provisioned in the same Pulumi program. + +{{% example %}} + +```typescript +import * as pulumi from "@pulumi/pulumi"; +import { remote, types } from "@pulumi/command"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; + +export = async () => { + const config = new pulumi.Config(); + + // Get the private key to connect to the server. If a key is + // provided, use it, otherwise default to the standard id_rsa SSH key. + const privateKeyBase64 = config.get("privateKeyBase64"); + const privateKey = privateKeyBase64 ? + Buffer.from(privateKeyBase64, 'base64').toString('ascii') : + fs.readFileSync(path.join(os.homedir(), ".ssh", "id_rsa")).toString("utf8"); + + const serverPublicIp = config.require("serverPublicIp"); + const userName = config.require("userName"); + + // The configuration of our SSH connection to the instance. + const connection: types.input.remote.ConnectionArgs = { + host: serverPublicIp, + user: userName, + privateKey: privateKey, + }; + + // Set up source and target of the remote copy. + const from = config.require("payload")!; + const archive = new pulumi.asset.FileArchive(from); + const to = config.require("destDir")!; + + // Copy the files to the remote. + const copy = new remote.CopyToRemote("copy", { + connection, + source: archive, + remotePath: to, + }); + + // Verify that the expected files were copied to the remote. + // We want to run this after each copy, i.e., when something changed, + // so we use the asset to be copied as a trigger. + const find = new remote.Command("ls", { + connection, + create: `find ${to}/${from} | sort`, + triggers: [archive], + }, { dependsOn: copy }); + + return { + remoteContents: find.stdout + } +} +``` + +```python +import pulumi +import pulumi_command as command + +config = pulumi.Config() + +server_public_ip = config.require("serverPublicIp") +user_name = config.require("userName") +private_key = config.require("privateKey") +payload = config.require("payload") +dest_dir = config.require("destDir") + +archive = pulumi.FileArchive(payload) + +# The configuration of our SSH connection to the instance. +conn = command.remote.ConnectionArgs( + host = server_public_ip, + user = user_name, + privateKey = private_key, +) + +# Copy the files to the remote. +copy = command.remote.CopyToRemote("copy", + connection=conn, + source=archive, + destination=dest_dir) + +# Verify that the expected files were copied to the remote. +# We want to run this after each copy, i.e., when something changed, +# so we use the asset to be copied as a trigger. +find = command.remote.Command("find", + connection=conn, + create=f"find {dest_dir}/{payload} | sort", + triggers=[archive], + opts = pulumi.ResourceOptions(depends_on=[copy])) + +pulumi.export("remoteContents", find.stdout) +``` + +```go +package main + +import ( + "fmt" + + "github.com/pulumi/pulumi-command/sdk/go/command/remote" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + cfg := config.New(ctx, "") + serverPublicIp := cfg.Require("serverPublicIp") + userName := cfg.Require("userName") + privateKey := cfg.Require("privateKey") + payload := cfg.Require("payload") + destDir := cfg.Require("destDir") + + archive := pulumi.NewFileArchive(payload) + + conn := remote.ConnectionArgs{ + Host: pulumi.String(serverPublicIp), + User: pulumi.String(userName), + PrivateKey: pulumi.String(privateKey), + } + + copy, err := remote.NewCopyToRemote(ctx, "copy", &remote.CopyToRemoteArgs{ + Connection: conn, + Source: archive, + }) + if err != nil { + return err + } + + find, err := remote.NewCommand(ctx, "find", &remote.CommandArgs{ + Connection: conn, + Create: pulumi.String(fmt.Sprintf("find %v/%v | sort", destDir, payload)), + Triggers: pulumi.Array{ + archive, + }, + }, pulumi.DependsOn([]pulumi.Resource{ + copy, + })) + if err != nil { + return err + } + + ctx.Export("remoteContents", find.Stdout) + return nil + }) +} +``` + +```csharp +using System.Collections.Generic; +using Pulumi; +using Command = Pulumi.Command; + +return await Deployment.RunAsync(() => +{ + var config = new Config(); + var serverPublicIp = config.Require("serverPublicIp"); + var userName = config.Require("userName"); + var privateKey = config.Require("privateKey"); + var payload = config.Require("payload"); + var destDir = config.Require("destDir"); + + var archive = new FileArchive(payload); + + // The configuration of our SSH connection to the instance. + var conn = new Command.Remote.Inputs.ConnectionArgs + { + Host = serverPublicIp, + User = userName, + PrivateKey = privateKey, + }; + + // Copy the files to the remote. + var copy = new Command.Remote.CopyToRemote("copy", new() + { + Connection = conn, + Source = archive, + }); + + // Verify that the expected files were copied to the remote. + // We want to run this after each copy, i.e., when something changed, + // so we use the asset to be copied as a trigger. + var find = new Command.Remote.Command("find", new() + { + Connection = conn, + Create = $"find {destDir}/{payload} | sort", + Triggers = new[] + { + archive, + }, + }, new CustomResourceOptions + { + DependsOn = + { + copy, + }, + }); + + return new Dictionary + { + ["remoteContents"] = find.Stdout, + }; +}); +``` + +```java +package generated_program; + +import com.pulumi.Context; +import com.pulumi.Pulumi; +import com.pulumi.core.Output; +import com.pulumi.command.remote.Command; +import com.pulumi.command.remote.CommandArgs; +import com.pulumi.command.remote.CopyToRemote; +import com.pulumi.command.remote.inputs.*; +import com.pulumi.resources.CustomResourceOptions; +import com.pulumi.asset.FileArchive; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class App { + public static void main(String[] args) { + Pulumi.run(App::stack); + } + + public static void stack(Context ctx) { + final var config = ctx.config(); + final var serverPublicIp = config.require("serverPublicIp"); + final var userName = config.require("userName"); + final var privateKey = config.require("privateKey"); + final var payload = config.require("payload"); + final var destDir = config.require("destDir"); + + final var archive = new FileArchive(payload); + + // The configuration of our SSH connection to the instance. + final var conn = ConnectionArgs.builder() + .host(serverPublicIp) + .user(userName) + .privateKey(privateKey) + .build(); + + // Copy the files to the remote. + var copy = new CopyToRemote("copy", CopyToRemoteArgs.builder() + .connection(conn) + .source(archive) + .destination(destDir) + .build()); + + // Verify that the expected files were copied to the remote. + // We want to run this after each copy, i.e., when something changed, + // so we use the asset to be copied as a trigger. + var find = new Command("find", CommandArgs.builder() + .connection(conn) + .create(String.format("find %s/%s | sort", destDir,payload)) + .triggers(archive) + .build(), CustomResourceOptions.builder() + .dependsOn(copy) + .build()); + + ctx.export("remoteContents", find.stdout()); + } +} +``` + +```yaml +resources: + # Copy the files to the remote. + copy: + type: command:remote:CopyToRemote + properties: + connection: ${conn} + source: ${archive} + remotePath: ${destDir} + + # Verify that the expected files were copied to the remote. + # We want to run this after each copy, i.e., when something changed, + # so we use the asset to be copied as a trigger. + find: + type: command:remote:Command + properties: + connection: ${conn} + create: find ${destDir}/${payload} | sort + triggers: + - ${archive} + options: + dependsOn: + - ${copy} + +config: + serverPublicIp: + type: string + userName: + type: string + privateKey: + type: string + payload: + type: string + destDir: + type: string + +variables: + # The source directory or archive to copy. + archive: + fn::fileArchive: ${payload} + # The configuration of our SSH connection to the instance. + conn: + host: ${serverPublicIp} + user: ${userName} + privateKey: ${privateKey} + +outputs: + remoteContents: ${find.stdout} +``` + +{{% /example %}} + +{{% /examples %}} \ No newline at end of file diff --git a/sdk/dotnet/Local/Command.cs b/sdk/dotnet/Local/Command.cs index aca5604b..b5d95b57 100644 --- a/sdk/dotnet/Local/Command.cs +++ b/sdk/dotnet/Local/Command.cs @@ -15,7 +15,100 @@ namespace Pulumi.Command.Local /// This command can be inserted into the life cycles of other resources using the `dependsOn` or `parent` resource options. A command is considered to have failed when it finished with a non-zero exit code. This will fail the CRUD step of the `Command` resource. /// /// ## Example Usage - /// ### Triggers + /// + /// ### Basic Example + /// + /// This example shows the simplest use case, simply running a command on `create` in the Pulumi lifecycle. + /// + /// ```csharp + /// using System.Collections.Generic; + /// using Pulumi; + /// using Pulumi.Command.Local; + /// + /// await Deployment.RunAsync(() => + /// { + /// var command = new Command("random", new CommandArgs + /// { + /// Create = "openssl rand -hex 16" + /// }); + /// + /// return new Dictionary<string, object?> + /// { + /// ["stdOut"] = command.Stdout + /// }; + /// }); + /// ``` + /// + /// ### Invoking a Lambda during Pulumi Deployment + /// + /// This example show using a local command to invoke an AWS Lambda once it's deployed. The Lambda invocation could also depend on other resources. + /// + /// ```csharp + /// using System.Collections.Generic; + /// using System.Text.Json; + /// using Pulumi; + /// using Aws = Pulumi.Aws; + /// using Command = Pulumi.Command; + /// + /// return await Deployment.RunAsync(() => + /// { + /// var awsConfig = new Config("aws"); + /// + /// var lambdaRole = new Aws.Iam.Role("lambdaRole", new() + /// { + /// AssumeRolePolicy = JsonSerializer.Serialize(new Dictionary<string, object?> + /// { + /// ["Version"] = "2012-10-17", + /// ["Statement"] = new[] + /// { + /// new Dictionary<string, object?> + /// { + /// ["Action"] = "sts:AssumeRole", + /// ["Effect"] = "Allow", + /// ["Principal"] = new Dictionary<string, object?> + /// { + /// ["Service"] = "lambda.amazonaws.com", + /// }, + /// }, + /// }, + /// }), + /// }); + /// + /// var lambdaFunction = new Aws.Lambda.Function("lambdaFunction", new() + /// { + /// Name = "f", + /// Publish = true, + /// Role = lambdaRole.Arn, + /// Handler = "index.handler", + /// Runtime = Aws.Lambda.Runtime.NodeJS20dX, + /// Code = new FileArchive("./handler"), + /// }); + /// + /// var invokeCommand = new Command.Local.Command("invokeCommand", new() + /// { + /// Create = $"aws lambda invoke --function-name \"$FN\" --payload '{{\"stackName\": \"{Deployment.Instance.StackName}\"}}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '\"' && rm out.txt", + /// Environment = + /// { + /// { "FN", lambdaFunction.Arn }, + /// { "AWS_REGION", awsConfig.Require("region") }, + /// { "AWS_PAGER", "" }, + /// }, + /// }, new CustomResourceOptions + /// { + /// DependsOn = + /// { + /// lambdaFunction, + /// }, + /// }); + /// + /// return new Dictionary<string, object?> + /// { + /// ["output"] = invokeCommand.Stdout, + /// }; + /// }); + /// ``` + /// + /// ### Using Triggers /// /// This example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run. /// diff --git a/sdk/dotnet/Remote/Command.cs b/sdk/dotnet/Remote/Command.cs index 32274e2e..1acc009d 100644 --- a/sdk/dotnet/Remote/Command.cs +++ b/sdk/dotnet/Remote/Command.cs @@ -13,8 +13,41 @@ namespace Pulumi.Command.Remote /// A command to run on a remote host. The connection is established via ssh. /// /// ## Example Usage - /// ### Triggers /// + /// ### A Basic Example + /// This program connects to a server and runs the `hostname` command. The output is then available via the `stdout` property. + /// + /// ```csharp + /// using System.Collections.Generic; + /// using System.Linq; + /// using Pulumi; + /// using Command = Pulumi.Command; + /// + /// return await Deployment.RunAsync(() => + /// { + /// var config = new Config(); + /// var server = config.Require("server"); + /// var userName = config.Require("userName"); + /// var privateKey = config.Require("privateKey"); + /// var hostnameCmd = new Command.Remote.Command("hostnameCmd", new() + /// { + /// Create = "hostname", + /// Connection = new Command.Remote.Inputs.ConnectionArgs + /// { + /// Host = server, + /// User = userName, + /// PrivateKey = privateKey, + /// }, + /// }); + /// + /// return new Dictionary<string, object?> + /// { + /// ["hostname"] = hostnameCmd.Stdout, + /// }; + /// }); + /// ``` + /// + /// ### Triggers /// This example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run. /// /// ```csharp @@ -22,7 +55,7 @@ namespace Pulumi.Command.Remote /// using Command = Pulumi.Command; /// using Random = Pulumi.Random; /// - /// return await Deployment.RunAsync(() => + /// return await Deployment.RunAsync(() => /// { /// var str = "foo"; /// diff --git a/sdk/dotnet/Remote/CopyToRemote.cs b/sdk/dotnet/Remote/CopyToRemote.cs index b8ab02ec..d65ad8a3 100644 --- a/sdk/dotnet/Remote/CopyToRemote.cs +++ b/sdk/dotnet/Remote/CopyToRemote.cs @@ -11,6 +11,67 @@ namespace Pulumi.Command.Remote { /// /// Copy an Asset or Archive to a remote host. + /// + /// ## Example usage + /// + /// This example copies a local directory to a remote host via SSH. For brevity, the remote server is assumed to exist, but it could also be provisioned in the same Pulumi program. + /// + /// ```csharp + /// using System.Collections.Generic; + /// using Pulumi; + /// using Command = Pulumi.Command; + /// + /// return await Deployment.RunAsync(() => + /// { + /// var config = new Config(); + /// var serverPublicIp = config.Require("serverPublicIp"); + /// var userName = config.Require("userName"); + /// var privateKey = config.Require("privateKey"); + /// var payload = config.Require("payload"); + /// var destDir = config.Require("destDir"); + /// + /// var archive = new FileArchive(payload); + /// + /// // The configuration of our SSH connection to the instance. + /// var conn = new Command.Remote.Inputs.ConnectionArgs + /// { + /// Host = serverPublicIp, + /// User = userName, + /// PrivateKey = privateKey, + /// }; + /// + /// // Copy the files to the remote. + /// var copy = new Command.Remote.CopyToRemote("copy", new() + /// { + /// Connection = conn, + /// Source = archive, + /// }); + /// + /// // Verify that the expected files were copied to the remote. + /// // We want to run this after each copy, i.e., when something changed, + /// // so we use the asset to be copied as a trigger. + /// var find = new Command.Remote.Command("find", new() + /// { + /// Connection = conn, + /// Create = $"find {destDir}/{payload} | sort", + /// Triggers = new[] + /// { + /// archive, + /// }, + /// }, new CustomResourceOptions + /// { + /// DependsOn = + /// { + /// copy, + /// }, + /// }); + /// + /// return new Dictionary<string, object?> + /// { + /// ["remoteContents"] = find.Stdout, + /// }; + /// }); + /// ``` /// [CommandResourceType("command:remote:CopyToRemote")] public partial class CopyToRemote : global::Pulumi.CustomResource diff --git a/sdk/go/command/local/command.go b/sdk/go/command/local/command.go index 0318988a..e809b56e 100644 --- a/sdk/go/command/local/command.go +++ b/sdk/go/command/local/command.go @@ -16,7 +16,116 @@ import ( // This command can be inserted into the life cycles of other resources using the `dependsOn` or `parent` resource options. A command is considered to have failed when it finished with a non-zero exit code. This will fail the CRUD step of the `Command` resource. // // ## Example Usage -// ### Triggers +// +// ### Basic Example +// +// This example shows the simplest use case, simply running a command on `create` in the Pulumi lifecycle. +// +// ```go +// package main +// +// import ( +// +// "github.com/pulumi/pulumi-command/sdk/go/command/local" +// "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +// +// ) +// +// func main() { +// pulumi.Run(func(ctx *pulumi.Context) error { +// random, err := local.NewCommand(ctx, "my-bucket", &local.CommandArgs{ +// Create: pulumi.String("openssl rand -hex 16"), +// }) +// if err != nil { +// return err +// } +// +// ctx.Export("output", random.Stdout) +// return nil +// }) +// } +// +// ``` +// +// ### Invoking a Lambda during Pulumi Deployment +// +// This example show using a local command to invoke an AWS Lambda once it's deployed. The Lambda invocation could also depend on other resources. +// +// ```go +// package main +// +// import ( +// +// "encoding/json" +// "fmt" +// +// "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/iam" +// "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/lambda" +// "github.com/pulumi/pulumi-command/sdk/go/command/local" +// "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +// "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config" +// +// ) +// +// func main() { +// pulumi.Run(func(ctx *pulumi.Context) error { +// awsConfig := config.New(ctx, "aws") +// awsRegion := awsConfig.Require("region") +// +// tmpJSON0, err := json.Marshal(map[string]interface{}{ +// "Version": "2012-10-17", +// "Statement": []map[string]interface{}{ +// { +// "Action": "sts:AssumeRole", +// "Effect": "Allow", +// "Principal": map[string]interface{}{ +// "Service": "lambda.amazonaws.com", +// }, +// }, +// }, +// }) +// if err != nil { +// return err +// } +// json0 := string(tmpJSON0) +// lambdaRole, err := iam.NewRole(ctx, "lambdaRole", &iam.RoleArgs{ +// AssumeRolePolicy: pulumi.String(json0), +// }) +// if err != nil { +// return err +// } +// lambdaFunction, err := lambda.NewFunction(ctx, "lambdaFunction", &lambda.FunctionArgs{ +// Name: pulumi.String("f"), +// Publish: pulumi.Bool(true), +// Role: lambdaRole.Arn, +// Handler: pulumi.String("index.handler"), +// Runtime: pulumi.String(lambda.RuntimeNodeJS20dX), +// Code: pulumi.NewFileArchive("./handler"), +// }) +// if err != nil { +// return err +// } +// invokeCommand, err := local.NewCommand(ctx, "invokeCommand", &local.CommandArgs{ +// Create: pulumi.String(fmt.Sprintf("aws lambda invoke --function-name \"$FN\" --payload '{\"stackName\": \"%v\"}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '\"' && rm out.txt", ctx.Stack())), +// Environment: pulumi.StringMap{ +// "FN": lambdaFunction.Arn, +// "AWS_REGION": pulumi.String(awsRegion), +// "AWS_PAGER": pulumi.String(""), +// }, +// }, pulumi.DependsOn([]pulumi.Resource{ +// lambdaFunction, +// })) +// if err != nil { +// return err +// } +// ctx.Export("output", invokeCommand.Stdout) +// return nil +// }) +// } +// +// ``` +// +// ### Using Triggers // // This example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run. // diff --git a/sdk/go/command/remote/command.go b/sdk/go/command/remote/command.go index b784ee6e..df7bfaee 100644 --- a/sdk/go/command/remote/command.go +++ b/sdk/go/command/remote/command.go @@ -15,8 +15,46 @@ import ( // A command to run on a remote host. The connection is established via ssh. // // ## Example Usage -// ### Triggers // +// ### A Basic Example +// This program connects to a server and runs the `hostname` command. The output is then available via the `stdout` property. +// +// ```go +// package main +// +// import ( +// +// "github.com/pulumi/pulumi-command/sdk/go/command/remote" +// "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +// "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config" +// +// ) +// +// func main() { +// pulumi.Run(func(ctx *pulumi.Context) error { +// cfg := config.New(ctx, "") +// server := cfg.Require("server") +// userName := cfg.Require("userName") +// privateKey := cfg.Require("privateKey") +// hostnameCmd, err := remote.NewCommand(ctx, "hostnameCmd", &remote.CommandArgs{ +// Create: pulumi.String("hostname"), +// Connection: &remote.ConnectionArgs{ +// Host: pulumi.String(server), +// User: pulumi.String(userName), +// PrivateKey: pulumi.String(privateKey), +// }, +// }) +// if err != nil { +// return err +// } +// ctx.Export("hostname", hostnameCmd.Stdout) +// return nil +// }) +// } +// +// ``` +// +// ### Triggers // This example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run. // // ```go diff --git a/sdk/go/command/remote/copyToRemote.go b/sdk/go/command/remote/copyToRemote.go index b360dcc4..f0b0c5ea 100644 --- a/sdk/go/command/remote/copyToRemote.go +++ b/sdk/go/command/remote/copyToRemote.go @@ -13,6 +13,68 @@ import ( ) // Copy an Asset or Archive to a remote host. +// +// ## Example usage +// +// This example copies a local directory to a remote host via SSH. For brevity, the remote server is assumed to exist, but it could also be provisioned in the same Pulumi program. +// +// ```go +// package main +// +// import ( +// +// "fmt" +// +// "github.com/pulumi/pulumi-command/sdk/go/command/remote" +// "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +// "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config" +// +// ) +// +// func main() { +// pulumi.Run(func(ctx *pulumi.Context) error { +// cfg := config.New(ctx, "") +// serverPublicIp := cfg.Require("serverPublicIp") +// userName := cfg.Require("userName") +// privateKey := cfg.Require("privateKey") +// payload := cfg.Require("payload") +// destDir := cfg.Require("destDir") +// +// archive := pulumi.NewFileArchive(payload) +// +// conn := remote.ConnectionArgs{ +// Host: pulumi.String(serverPublicIp), +// User: pulumi.String(userName), +// PrivateKey: pulumi.String(privateKey), +// } +// +// copy, err := remote.NewCopyToRemote(ctx, "copy", &remote.CopyToRemoteArgs{ +// Connection: conn, +// Source: archive, +// }) +// if err != nil { +// return err +// } +// +// find, err := remote.NewCommand(ctx, "find", &remote.CommandArgs{ +// Connection: conn, +// Create: pulumi.String(fmt.Sprintf("find %v/%v | sort", destDir, payload)), +// Triggers: pulumi.Array{ +// archive, +// }, +// }, pulumi.DependsOn([]pulumi.Resource{ +// copy, +// })) +// if err != nil { +// return err +// } +// +// ctx.Export("remoteContents", find.Stdout) +// return nil +// }) +// } +// +// ``` type CopyToRemote struct { pulumi.CustomResourceState diff --git a/sdk/java/src/main/java/com/pulumi/command/local/Command.java b/sdk/java/src/main/java/com/pulumi/command/local/Command.java index 792cf165..32ba6722 100644 --- a/sdk/java/src/main/java/com/pulumi/command/local/Command.java +++ b/sdk/java/src/main/java/com/pulumi/command/local/Command.java @@ -26,7 +26,111 @@ * This command can be inserted into the life cycles of other resources using the `dependsOn` or `parent` resource options. A command is considered to have failed when it finished with a non-zero exit code. This will fail the CRUD step of the `Command` resource. * * ## Example Usage - * ### Triggers + * + * ### Basic Example + * + * This example shows the simplest use case, simply running a command on `create` in the Pulumi lifecycle. + * + *
+ * {@code
+ * package generated_program;
+ * 
+ * import com.pulumi.Context;
+ * import com.pulumi.Pulumi;
+ * import com.pulumi.command.local.Command;
+ * import com.pulumi.command.local.CommandArgs;
+ * 
+ * public class App {
+ *     public static void main(String[] args) {
+ *         Pulumi.run(App::stack);
+ *     }
+ * 
+ *     public static void stack(Context ctx) {
+ *         var random = new Command("random", CommandArgs.builder()
+ *             .create("openssl rand -hex 16")
+ *             .build());
+ * 
+ *         ctx.export("rand", random.stdout());
+ *     }
+ * }
+ * }
+ * 
+ * + * ### Invoking a Lambda during Pulumi Deployment + * + * This example show using a local command to invoke an AWS Lambda once it's deployed. The Lambda invocation could also depend on other resources. + * + *
+ * {@code
+ * package generated_program;
+ * 
+ * import com.pulumi.Context;
+ * import com.pulumi.Pulumi;
+ * import com.pulumi.aws.iam.Role;
+ * import com.pulumi.aws.iam.RoleArgs;
+ * import com.pulumi.aws.lambda.Function;
+ * import com.pulumi.aws.lambda.FunctionArgs;
+ * import com.pulumi.command.local.Command;
+ * import com.pulumi.command.local.CommandArgs;
+ * import static com.pulumi.codegen.internal.Serialization.*;
+ * import com.pulumi.resources.CustomResourceOptions;
+ * import com.pulumi.asset.FileArchive;
+ * import java.util.Map;
+ * 
+ * public class App {
+ *     public static void main(String[] args) {
+ *         Pulumi.run(App::stack);
+ *     }
+ * 
+ *     public static void stack(Context ctx) {
+ *         var awsConfig = ctx.config("aws");
+ *         var awsRegion = awsConfig.require("region");
+ * 
+ *         var lambdaRole = new Role("lambdaRole", RoleArgs.builder()
+ *                 .assumeRolePolicy(serializeJson(
+ *                         jsonObject(
+ *                                 jsonProperty("Version", "2012-10-17"),
+ *                                 jsonProperty("Statement", jsonArray(jsonObject(
+ *                                         jsonProperty("Action", "sts:AssumeRole"),
+ *                                         jsonProperty("Effect", "Allow"),
+ *                                         jsonProperty("Principal", jsonObject(
+ *                                                 jsonProperty("Service", "lambda.amazonaws.com")))))))))
+ *                 .build());
+ * 
+ *         var lambdaFunction = new Function("lambdaFunction", FunctionArgs.builder()
+ *                 .name("f")
+ *                 .publish(true)
+ *                 .role(lambdaRole.arn())
+ *                 .handler("index.handler")
+ *                 .runtime("nodejs20.x")
+ *                 .code(new FileArchive("./handler"))
+ *                 .build());
+ * 
+ *         // Work around the lack of Output.all for Maps in Java. We cannot use a plain Map because
+ *         // `lambdaFunction.arn()` is an Output.
+ *         var invokeEnv = Output.tuple(
+ *                 Output.of("FN"), lambdaFunction.arn(),
+ *                 Output.of("AWS_REGION"), Output.of(awsRegion),
+ *                 Output.of("AWS_PAGER"), Output.of("")
+ *         ).applyValue(t -> Map.of(t.t1, t.t2, t.t3, t.t4, t.t5, t.t6));
+ * 
+ *         var invokeCommand = new Command("invokeCommand", CommandArgs.builder()
+ *                 .create(String.format(
+ *                         "aws lambda invoke --function-name \"$FN\" --payload '{\"stackName\": \"%s\"}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '\"'  && rm out.txt",
+ *                         ctx.stackName()))
+ *                 .environment(invokeEnv)
+ *                 .build(),
+ *                 CustomResourceOptions.builder()
+ *                         .dependsOn(lambdaFunction)
+ *                         .build());
+ * 
+ *         ctx.export("output", invokeCommand.stdout());
+ *     }
+ * }
+ * }
+ * 
+ * + * ### Using Triggers * * This example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run. * diff --git a/sdk/java/src/main/java/com/pulumi/command/remote/Command.java b/sdk/java/src/main/java/com/pulumi/command/remote/Command.java index 3c58d4dd..3a541e86 100644 --- a/sdk/java/src/main/java/com/pulumi/command/remote/Command.java +++ b/sdk/java/src/main/java/com/pulumi/command/remote/Command.java @@ -23,8 +23,47 @@ * A command to run on a remote host. The connection is established via ssh. * * ## Example Usage - * ### Triggers * + * ### A Basic Example + * This program connects to a server and runs the `hostname` command. The output is then available via the `stdout` property. + * + *
+ * {@code
+ * package generated_program;
+ * 
+ * import com.pulumi.Context;
+ * import com.pulumi.Pulumi;
+ * import com.pulumi.core.Output;
+ * import com.pulumi.command.remote.Command;
+ * import com.pulumi.command.remote.CommandArgs;
+ * import com.pulumi.command.remote.inputs.ConnectionArgs;
+ * 
+ * public class App {
+ *     public static void main(String[] args) {
+ *         Pulumi.run(App::stack);
+ *     }
+ * 
+ *     public static void stack(Context ctx) {
+ *         final var config = ctx.config();
+ *         final var server = config.require("server");
+ *         final var userName = config.require("userName");
+ *         final var privateKey = config.require("privateKey");
+ *         var hostnameCmd = new Command("hostnameCmd", CommandArgs.builder()
+ *             .create("hostname")
+ *             .connection(ConnectionArgs.builder()
+ *                 .host(server)
+ *                 .user(userName)
+ *                 .privateKey(privateKey)
+ *                 .build())
+ *             .build());
+ * 
+ *         ctx.export("hostname", hostnameCmd.stdout());
+ *     }
+ * }
+ * }
+ * 
+ * + * ### Triggers * This example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run. * *
diff --git a/sdk/java/src/main/java/com/pulumi/command/remote/CopyToRemote.java b/sdk/java/src/main/java/com/pulumi/command/remote/CopyToRemote.java
index 09064288..ba1cac91 100644
--- a/sdk/java/src/main/java/com/pulumi/command/remote/CopyToRemote.java
+++ b/sdk/java/src/main/java/com/pulumi/command/remote/CopyToRemote.java
@@ -20,6 +20,76 @@
 /**
  * Copy an Asset or Archive to a remote host.
  * 
+ * ## Example usage
+ * 
+ * This example copies a local directory to a remote host via SSH. For brevity, the remote server is assumed to exist, but it could also be provisioned in the same Pulumi program.
+ * 
+ * 
+ * {@code
+ * package generated_program;
+ * 
+ * import com.pulumi.Context;
+ * import com.pulumi.Pulumi;
+ * import com.pulumi.core.Output;
+ * import com.pulumi.command.remote.Command;
+ * import com.pulumi.command.remote.CommandArgs;
+ * import com.pulumi.command.remote.CopyToRemote;
+ * import com.pulumi.command.remote.inputs.*;
+ * import com.pulumi.resources.CustomResourceOptions;
+ * import com.pulumi.asset.FileArchive;
+ * import java.util.List;
+ * import java.util.ArrayList;
+ * import java.util.Map;
+ * import java.io.File;
+ * import java.nio.file.Files;
+ * import java.nio.file.Paths;
+ * 
+ * public class App {
+ *     public static void main(String[] args) {
+ *         Pulumi.run(App::stack);
+ *     }
+ * 
+ *     public static void stack(Context ctx) {
+ *         final var config = ctx.config();
+ *         final var serverPublicIp = config.require("serverPublicIp");
+ *         final var userName = config.require("userName");
+ *         final var privateKey = config.require("privateKey");
+ *         final var payload = config.require("payload");
+ *         final var destDir = config.require("destDir");
+ * 
+ *         final var archive = new FileArchive(payload);
+ * 
+ *         // The configuration of our SSH connection to the instance.
+ *         final var conn = ConnectionArgs.builder()
+ *             .host(serverPublicIp)
+ *             .user(userName)
+ *             .privateKey(privateKey)
+ *             .build();
+ * 
+ *         // Copy the files to the remote.
+ *         var copy = new CopyToRemote("copy", CopyToRemoteArgs.builder()
+ *             .connection(conn)
+ *             .source(archive)
+ *             .destination(destDir)
+ *             .build());
+ * 
+ *         // Verify that the expected files were copied to the remote.
+ *         // We want to run this after each copy, i.e., when something changed,
+ *         // so we use the asset to be copied as a trigger.
+ *         var find = new Command("find", CommandArgs.builder()
+ *             .connection(conn)
+ *             .create(String.format("find %s/%s | sort", destDir,payload))
+ *             .triggers(archive)
+ *             .build(), CustomResourceOptions.builder()
+ *                 .dependsOn(copy)
+ *                 .build());
+ * 
+ *         ctx.export("remoteContents", find.stdout());
+ *     }
+ * }
+ * }
+ * 
+ * */ @ResourceType(type="command:remote:CopyToRemote") public class CopyToRemote extends com.pulumi.resources.CustomResource { diff --git a/sdk/nodejs/local/command.ts b/sdk/nodejs/local/command.ts index c55daec4..f5094053 100644 --- a/sdk/nodejs/local/command.ts +++ b/sdk/nodejs/local/command.ts @@ -13,7 +13,50 @@ import * as utilities from "../utilities"; * This command can be inserted into the life cycles of other resources using the `dependsOn` or `parent` resource options. A command is considered to have failed when it finished with a non-zero exit code. This will fail the CRUD step of the `Command` resource. * * ## Example Usage - * ### Triggers + * + * ### Basic Example + * + * This example shows the simplest use case, simply running a command on `create` in the Pulumi lifecycle. + * + * ```typescript + * import { local } from "@pulumi/command"; + * + * const random = new local.Command("random", { + * create: "openssl rand -hex 16", + * }); + * + * export const output = random.stdout; + * ``` + * + * ### Invoking a Lambda during Pulumi Deployment + * + * This example show using a local command to invoke an AWS Lambda once it's deployed. The Lambda invocation could also depend on other resources. + * + * ```typescript + * import * as aws from "@pulumi/aws"; + * import { local } from "@pulumi/command"; + * import { getStack } from "@pulumi/pulumi"; + * + * const f = new aws.lambda.CallbackFunction("f", { + * publish: true, + * callback: async (ev: any) => { + * return `Stack ${ev.stackName} is deployed!`; + * } + * }); + * + * const invoke = new local.Command("execf", { + * create: `aws lambda invoke --function-name "$FN" --payload '{"stackName": "${getStack()}"}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '"' && rm out.txt`, + * environment: { + * FN: f.qualifiedArn, + * AWS_REGION: aws.config.region!, + * AWS_PAGER: "", + * }, + * }, { dependsOn: f }) + * + * export const output = invoke.stdout; + * ``` + * + * ### Using Triggers * * This example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run. * diff --git a/sdk/nodejs/remote/command.ts b/sdk/nodejs/remote/command.ts index 07fb5859..6ee2ae4d 100644 --- a/sdk/nodejs/remote/command.ts +++ b/sdk/nodejs/remote/command.ts @@ -11,8 +11,31 @@ import * as utilities from "../utilities"; * A command to run on a remote host. The connection is established via ssh. * * ## Example Usage - * ### Triggers * + * ### A Basic Example + * This program connects to a server and runs the `hostname` command. The output is then available via the `stdout` property. + * + * ```typescript + * import * as pulumi from "@pulumi/pulumi"; + * import * as command from "@pulumi/command"; + * + * const config = new pulumi.Config(); + * const server = config.require("server"); + * const userName = config.require("userName"); + * const privateKey = config.require("privateKey"); + * + * const hostnameCmd = new command.remote.Command("hostnameCmd", { + * create: "hostname", + * connection: { + * host: server, + * user: userName, + * privateKey: privateKey, + * }, + * }); + * export const hostname = hostnameCmd.stdout; + * ``` + * + * ### Triggers * This example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run. * * ```typescript diff --git a/sdk/nodejs/remote/copyToRemote.ts b/sdk/nodejs/remote/copyToRemote.ts index 34147ee1..a64397af 100644 --- a/sdk/nodejs/remote/copyToRemote.ts +++ b/sdk/nodejs/remote/copyToRemote.ts @@ -9,6 +9,64 @@ import * as utilities from "../utilities"; /** * Copy an Asset or Archive to a remote host. + * + * ## Example usage + * + * This example copies a local directory to a remote host via SSH. For brevity, the remote server is assumed to exist, but it could also be provisioned in the same Pulumi program. + * + * ```typescript + * import * as pulumi from "@pulumi/pulumi"; + * import { remote, types } from "@pulumi/command"; + * import * as fs from "fs"; + * import * as os from "os"; + * import * as path from "path"; + * + * export = async () => { + * const config = new pulumi.Config(); + * + * // Get the private key to connect to the server. If a key is + * // provided, use it, otherwise default to the standard id_rsa SSH key. + * const privateKeyBase64 = config.get("privateKeyBase64"); + * const privateKey = privateKeyBase64 ? + * Buffer.from(privateKeyBase64, 'base64').toString('ascii') : + * fs.readFileSync(path.join(os.homedir(), ".ssh", "id_rsa")).toString("utf8"); + * + * const serverPublicIp = config.require("serverPublicIp"); + * const userName = config.require("userName"); + * + * // The configuration of our SSH connection to the instance. + * const connection: types.input.remote.ConnectionArgs = { + * host: serverPublicIp, + * user: userName, + * privateKey: privateKey, + * }; + * + * // Set up source and target of the remote copy. + * const from = config.require("payload")!; + * const archive = new pulumi.asset.FileArchive(from); + * const to = config.require("destDir")!; + * + * // Copy the files to the remote. + * const copy = new remote.CopyToRemote("copy", { + * connection, + * source: archive, + * remotePath: to, + * }); + * + * // Verify that the expected files were copied to the remote. + * // We want to run this after each copy, i.e., when something changed, + * // so we use the asset to be copied as a trigger. + * const find = new remote.Command("ls", { + * connection, + * create: `find ${to}/${from} | sort`, + * triggers: [archive], + * }, { dependsOn: copy }); + * + * return { + * remoteContents: find.stdout + * } + * } + * ``` */ export class CopyToRemote extends pulumi.CustomResource { /** diff --git a/sdk/python/README.md b/sdk/python/README.md index b3929999..ae9ead01 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -265,21 +265,46 @@ curl \ There are cases where it's important to run some cleanup operation before destroying a resource such as when destroying the resource does not properly handle orderly cleanup. For example, destroying an EKS Cluster will not ensure that all Kubernetes object finalizers are run, which may lead to leaking external resources managed by those Kubernetes resources. This example shows how we can use a `delete`-only `Command` to ensure some cleanup is run within a cluster before destroying it. +```yaml +resources: + cluster: + type: eks:Cluster + + cleanupKubernetesNamespaces: + # We could also use `RemoteCommand` to run this from + # within a node in the cluster. + type: command:local:Command + properties: + # This will run before the cluster is destroyed. + # Everything else will need to depend on this resource + # to ensure this cleanup doesn't happen too early. + delete: | + kubectl --kubeconfig <(echo "$KUBECONFIG_DATA") delete namespace nginx + # Process substitution "<()" doesn't work in the default interpreter sh. + interpreter: ["/bin/bash", "-c"] + environment: + KUBECONFIG_DATA: "${cluster.kubeconfigJson}" +``` + ```ts -import { local } from "@pulumi/command"; +import * as pulumi from "@pulumi/pulumi"; +import * as command from "@pulumi/command"; import * as eks from "@pulumi/eks"; -import * as random from "@pulumi/random"; -import { interpolate } from "@pulumi/pulumi"; const cluster = new eks.Cluster("cluster", {}); // We could also use `RemoteCommand` to run this from within a node in the cluster -const cleanupKubernetesNamespaces = new local.Command("cleanupKubernetesNamespaces", { +const cleanupKubernetesNamespaces = new command.local.Command("cleanupKubernetesNamespaces", { // This will run before the cluster is destroyed. Everything else will need to // depend on this resource to ensure this cleanup doesn't happen too early. - delete: "kubectl delete --all namespaces", + "delete": "kubectl --kubeconfig <(echo \"$KUBECONFIG_DATA\") delete namespace nginx\n", + // Process substitution "<()" doesn't work in the default interpreter sh. + interpreter: [ + "/bin/bash", + "-c", + ], environment: { - KUBECONFIG: cluster.kubeconfig, + KUBECONFIG_DATA: cluster.kubeconfigJson, }, }); ``` diff --git a/sdk/python/pulumi_command/local/command.py b/sdk/python/pulumi_command/local/command.py index dc9f7b86..1f312dec 100644 --- a/sdk/python/pulumi_command/local/command.py +++ b/sdk/python/pulumi_command/local/command.py @@ -414,7 +414,67 @@ def __init__(__self__, This command can be inserted into the life cycles of other resources using the `dependsOn` or `parent` resource options. A command is considered to have failed when it finished with a non-zero exit code. This will fail the CRUD step of the `Command` resource. ## Example Usage - ### Triggers + + ### Basic Example + + This example shows the simplest use case, simply running a command on `create` in the Pulumi lifecycle. + + ```python + import pulumi + from pulumi_command import local + + random = local.Command("random", + create="openssl rand -hex 16" + ) + + pulumi.export("random", random.stdout) + ``` + + ### Invoking a Lambda during Pulumi Deployment + + This example show using a local command to invoke an AWS Lambda once it's deployed. The Lambda invocation could also depend on other resources. + + ```python + import pulumi + import json + import pulumi_aws as aws + import pulumi_command as command + + lambda_role = aws.iam.Role("lambdaRole", assume_role_policy=json.dumps({ + "Version": "2012-10-17", + "Statement": [{ + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }], + })) + + lambda_function = aws.lambda_.Function("lambdaFunction", + name="f", + publish=True, + role=lambda_role.arn, + handler="index.handler", + runtime=aws.lambda_.Runtime.NODE_JS20D_X, + code=pulumi.FileArchive("./handler")) + + aws_config = pulumi.Config("aws") + aws_region = aws_config.require("region") + + invoke_command = command.local.Command("invokeCommand", + create=f"aws lambda invoke --function-name \\"$FN\\" --payload '{{\\"stackName\\": \\"{pulumi.get_stack()}\\"}}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '\\"' && rm out.txt", + environment={ + "FN": lambda_function.arn, + "AWS_REGION": aws_region, + "AWS_PAGER": "", + }, + opts = pulumi.ResourceOptions(depends_on=[lambda_function])) + + pulumi.export("output", invoke_command.stdout) + ``` + + ### Using Triggers This example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run. @@ -556,7 +616,67 @@ def __init__(__self__, This command can be inserted into the life cycles of other resources using the `dependsOn` or `parent` resource options. A command is considered to have failed when it finished with a non-zero exit code. This will fail the CRUD step of the `Command` resource. ## Example Usage - ### Triggers + + ### Basic Example + + This example shows the simplest use case, simply running a command on `create` in the Pulumi lifecycle. + + ```python + import pulumi + from pulumi_command import local + + random = local.Command("random", + create="openssl rand -hex 16" + ) + + pulumi.export("random", random.stdout) + ``` + + ### Invoking a Lambda during Pulumi Deployment + + This example show using a local command to invoke an AWS Lambda once it's deployed. The Lambda invocation could also depend on other resources. + + ```python + import pulumi + import json + import pulumi_aws as aws + import pulumi_command as command + + lambda_role = aws.iam.Role("lambdaRole", assume_role_policy=json.dumps({ + "Version": "2012-10-17", + "Statement": [{ + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }], + })) + + lambda_function = aws.lambda_.Function("lambdaFunction", + name="f", + publish=True, + role=lambda_role.arn, + handler="index.handler", + runtime=aws.lambda_.Runtime.NODE_JS20D_X, + code=pulumi.FileArchive("./handler")) + + aws_config = pulumi.Config("aws") + aws_region = aws_config.require("region") + + invoke_command = command.local.Command("invokeCommand", + create=f"aws lambda invoke --function-name \\"$FN\\" --payload '{{\\"stackName\\": \\"{pulumi.get_stack()}\\"}}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '\\"' && rm out.txt", + environment={ + "FN": lambda_function.arn, + "AWS_REGION": aws_region, + "AWS_PAGER": "", + }, + opts = pulumi.ResourceOptions(depends_on=[lambda_function])) + + pulumi.export("output", invoke_command.stdout) + ``` + + ### Using Triggers This example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run. diff --git a/sdk/python/pulumi_command/remote/command.py b/sdk/python/pulumi_command/remote/command.py index b5b90403..c3e6e4be 100644 --- a/sdk/python/pulumi_command/remote/command.py +++ b/sdk/python/pulumi_command/remote/command.py @@ -216,8 +216,29 @@ def __init__(__self__, A command to run on a remote host. The connection is established via ssh. ## Example Usage - ### Triggers + ### A Basic Example + This program connects to a server and runs the `hostname` command. The output is then available via the `stdout` property. + + ```python + import pulumi + import pulumi_command as command + + config = pulumi.Config() + server = config.require("server") + user_name = config.require("userName") + private_key = config.require("privateKey") + hostname_cmd = command.remote.Command("hostnameCmd", + create="hostname", + connection=command.remote.ConnectionArgs( + host=server, + user=user_name, + private_key=private_key, + )) + pulumi.export("hostname", hostname_cmd.stdout) + ``` + + ### Triggers This example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run. ```python @@ -283,8 +304,29 @@ def __init__(__self__, A command to run on a remote host. The connection is established via ssh. ## Example Usage - ### Triggers + ### A Basic Example + This program connects to a server and runs the `hostname` command. The output is then available via the `stdout` property. + + ```python + import pulumi + import pulumi_command as command + + config = pulumi.Config() + server = config.require("server") + user_name = config.require("userName") + private_key = config.require("privateKey") + hostname_cmd = command.remote.Command("hostnameCmd", + create="hostname", + connection=command.remote.ConnectionArgs( + host=server, + user=user_name, + private_key=private_key, + )) + pulumi.export("hostname", hostname_cmd.stdout) + ``` + + ### Triggers This example defines several trigger values of various kinds. Changes to any of them will cause `cmd` to be re-run. ```python diff --git a/sdk/python/pulumi_command/remote/copy_to_remote.py b/sdk/python/pulumi_command/remote/copy_to_remote.py index 7ef60f5e..a2fabc01 100644 --- a/sdk/python/pulumi_command/remote/copy_to_remote.py +++ b/sdk/python/pulumi_command/remote/copy_to_remote.py @@ -95,6 +95,49 @@ def __init__(__self__, """ Copy an Asset or Archive to a remote host. + ## Example usage + + This example copies a local directory to a remote host via SSH. For brevity, the remote server is assumed to exist, but it could also be provisioned in the same Pulumi program. + + ```python + import pulumi + import pulumi_command as command + + config = pulumi.Config() + + server_public_ip = config.require("serverPublicIp") + user_name = config.require("userName") + private_key = config.require("privateKey") + payload = config.require("payload") + dest_dir = config.require("destDir") + + archive = pulumi.FileArchive(payload) + + # The configuration of our SSH connection to the instance. + conn = command.remote.ConnectionArgs( + host = server_public_ip, + user = user_name, + privateKey = private_key, + ) + + # Copy the files to the remote. + copy = command.remote.CopyToRemote("copy", + connection=conn, + source=archive, + destination=dest_dir) + + # Verify that the expected files were copied to the remote. + # We want to run this after each copy, i.e., when something changed, + # so we use the asset to be copied as a trigger. + find = command.remote.Command("find", + connection=conn, + create=f"find {dest_dir}/{payload} | sort", + triggers=[archive], + opts = pulumi.ResourceOptions(depends_on=[copy])) + + pulumi.export("remoteContents", find.stdout) + ``` + :param str resource_name: The name of the resource. :param pulumi.ResourceOptions opts: Options for the resource. :param pulumi.Input[pulumi.InputType['ConnectionArgs']] connection: The parameters with which to connect to the remote host. @@ -111,6 +154,49 @@ def __init__(__self__, """ Copy an Asset or Archive to a remote host. + ## Example usage + + This example copies a local directory to a remote host via SSH. For brevity, the remote server is assumed to exist, but it could also be provisioned in the same Pulumi program. + + ```python + import pulumi + import pulumi_command as command + + config = pulumi.Config() + + server_public_ip = config.require("serverPublicIp") + user_name = config.require("userName") + private_key = config.require("privateKey") + payload = config.require("payload") + dest_dir = config.require("destDir") + + archive = pulumi.FileArchive(payload) + + # The configuration of our SSH connection to the instance. + conn = command.remote.ConnectionArgs( + host = server_public_ip, + user = user_name, + privateKey = private_key, + ) + + # Copy the files to the remote. + copy = command.remote.CopyToRemote("copy", + connection=conn, + source=archive, + destination=dest_dir) + + # Verify that the expected files were copied to the remote. + # We want to run this after each copy, i.e., when something changed, + # so we use the asset to be copied as a trigger. + find = command.remote.Command("find", + connection=conn, + create=f"find {dest_dir}/{payload} | sort", + triggers=[archive], + opts = pulumi.ResourceOptions(depends_on=[copy])) + + pulumi.export("remoteContents", find.stdout) + ``` + :param str resource_name: The name of the resource. :param CopyToRemoteArgs args: The arguments to use to populate this resource's properties. :param pulumi.ResourceOptions opts: Options for the resource.