diff --git a/CHANGELOG.md b/CHANGELOG.md
index 17aafe15f3..abbbf56a7e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,11 @@
- Fix aliases for several resource kinds. (https://github.com/pulumi/pulumi-kubernetes/pull/990).
- Fix .NET resources with empty arguments. (https://github.com/pulumi/pulumi-kubernetes/pull/983).
+### Improvements
+
+- .NET SDK supports resources to work with YAML Kubernetes files and Helm charts.
+(https://github.com/pulumi/pulumi-kubernetes/pull/980).
+
## 1.5.3 (February 11, 2020)
### Bug fixes
diff --git a/cmd/pulumi-gen-kubernetes/main.go b/cmd/pulumi-gen-kubernetes/main.go
index 6960b388a3..9911c243b4 100644
--- a/cmd/pulumi-gen-kubernetes/main.go
+++ b/cmd/pulumi-gen-kubernetes/main.go
@@ -221,7 +221,7 @@ func writePythonClient(data map[string]interface{}, outdir, templateDir string)
func writeDotnetClient(data map[string]interface{}, outdir, templateDir string) {
- inputAPIcs, ouputAPIcs, kindsCs, err := gen.DotnetClient(data, templateDir)
+ inputAPIcs, ouputAPIcs, yamlcs, kindsCs, err := gen.DotnetClient(data, templateDir)
if err != nil {
panic(err)
}
@@ -247,6 +247,11 @@ func writeDotnetClient(data map[string]interface{}, outdir, templateDir string)
panic(err)
}
+ err = ioutil.WriteFile(fmt.Sprintf("%s/Yaml/Yaml.cs", outdir), []byte(yamlcs), 0777)
+ if err != nil {
+ panic(err)
+ }
+
for path, contents := range kindsCs {
filename := filepath.Join(outdir, path)
err := os.MkdirAll(filepath.Dir(filename), 0700)
diff --git a/pkg/gen/dotnet-templates/Pulumi.Kubernetes.csproj b/pkg/gen/dotnet-templates/Pulumi.Kubernetes.csproj
index 43ab885547..bbcff731ec 100644
--- a/pkg/gen/dotnet-templates/Pulumi.Kubernetes.csproj
+++ b/pkg/gen/dotnet-templates/Pulumi.Kubernetes.csproj
@@ -25,8 +25,9 @@
-
+
+
diff --git a/pkg/gen/dotnet-templates/Utilities.cs b/pkg/gen/dotnet-templates/Utilities.cs
index f3a380639b..8f52df1720 100644
--- a/pkg/gen/dotnet-templates/Utilities.cs
+++ b/pkg/gen/dotnet-templates/Utilities.cs
@@ -1,26 +1,145 @@
-// *** WARNING: this file was generated by the Pulumi Terraform Bridge (tfgen) Tool. ***
+// Copyright 2016-2020, Pulumi Corporation
+// *** WARNING: this file was generated by the Pulumi Kubernetes codegen tool. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
using System;
+using System.Diagnostics;
using System.IO;
+using System.Linq;
using System.Reflection;
-using Pulumi;
+using System.Text;
namespace Pulumi.Kubernetes
{
- static class Utilities
+ internal static class Utilities
{
- private readonly static string version;
- public static string Version => version;
+ public static string Version { get; }
static Utilities()
{
var assembly = typeof(Utilities).GetTypeInfo().Assembly;
- using (var stream = assembly.GetManifestResourceStream("Pulumi.Kubernetes.version.txt"))
- using (var reader = new StreamReader(stream))
+ using var stream = assembly.GetManifestResourceStream("Pulumi.Kubernetes.version.txt");
+ if (stream == null)
+ throw new Exception("Manifest file 'version.txt' not found");
+ using var reader = new StreamReader(stream);
+ Version = reader.ReadToEnd().Trim();
+ }
+
+ public static InvokeOptions WithVersion(this InvokeOptions? options)
+ {
+ if (options?.Version != null)
+ {
+ return options;
+ }
+ return new InvokeOptions
+ {
+ Parent = options?.Parent,
+ Provider = options?.Provider,
+ Version = Version,
+ };
+ }
+
+ public static string ExecuteCommand(string command, string[] flags)
+ {
+ using var process = new Process
+ {
+ StartInfo =
+ {
+ FileName = command,
+ Arguments = EscapeArguments(flags),
+ RedirectStandardOutput = true,
+ RedirectStandardError = true
+ }
+ };
+ process.Start();
+ string output = process.StandardOutput.ReadToEnd();
+ process.WaitForExit();
+ if (process.ExitCode > 0)
+ {
+ string error = process.StandardError.ReadToEnd();
+ throw new Exception(error);
+ }
+ return output;
+ }
+
+ ///
+ /// Convert an argument array to an argument string for using with Process.StartInfo.Arguments.
+ ///
+ private static string EscapeArguments(params string[] args)
+ => string.Join(" ", args.Select(EscapeArguments));
+
+ ///
+ /// Convert an argument array to an argument string for using with Process.StartInfo.Arguments.
+ ///
+ private static string EscapeArguments(string argument)
+ {
+ var escapedArgument = new StringBuilder();
+ var backslashCount = 0;
+ var needsQuotes = false;
+
+ foreach (var character in argument)
+ {
+ switch (character)
+ {
+ case '\\':
+ // Backslashes are simply passed through, except when they need
+ // to be escaped when followed by a \", e.g. the argument string
+ // \", which would be encoded to \\\"
+ backslashCount++;
+ escapedArgument.Append('\\');
+ break;
+
+ case '\"':
+ // Escape any preceding backslashes
+ escapedArgument.Append(new string('\\', backslashCount));
+
+ // Append an escaped double quote.
+ escapedArgument.Append("\\\"");
+
+ // Reset the backslash counter.
+ backslashCount = 0;
+ break;
+
+ case ' ':
+ case '\t':
+ // White spaces are escaped by surrounding the entire string with
+ // double quotes, which should be done at the end to prevent
+ // multiple wrappings.
+ needsQuotes = true;
+
+ // Append the whitespace
+ escapedArgument.Append(character);
+
+ // Reset the backslash counter.
+ backslashCount = 0;
+ break;
+
+ default:
+ // Reset the backslash counter.
+ backslashCount = 0;
+
+ // Append the current character
+ escapedArgument.Append(character);
+ break;
+ }
+ }
+
+ // No need to wrap in quotes
+ if (!needsQuotes)
{
- version = reader.ReadToEnd().Trim();
+ return escapedArgument.ToString();
}
+
+ // Prepend the "
+ escapedArgument.Insert(0, '"');
+
+ // Escape any preceding backslashes before appending the "
+ escapedArgument.Append(new string('\\', backslashCount));
+
+ // Append the final "
+ escapedArgument.Append('\"');
+
+ return escapedArgument.ToString();
}
}
}
diff --git a/pkg/gen/dotnet-templates/Yaml.cs.mustache b/pkg/gen/dotnet-templates/Yaml.cs.mustache
new file mode 100644
index 0000000000..4ffe2ff4c4
--- /dev/null
+++ b/pkg/gen/dotnet-templates/Yaml.cs.mustache
@@ -0,0 +1,297 @@
+// Copyright 2016-2020, Pulumi Corporation
+// *** WARNING: this file was generated by the Pulumi Kubernetes codegen tool. ***
+// *** Do not edit by hand unless you're certain you know what you are doing! ***
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using GlobExpressions;
+
+using TransformationAction = System.Func, Pulumi.CustomResourceOptions, System.Collections.Immutable.ImmutableDictionary>;
+
+namespace Pulumi.Kubernetes.Yaml
+{
+ ///
+ /// Base component for containers of Kubernetes resources.
+ ///
+ public abstract class CollectionComponentResource : ComponentResource
+ {
+ internal Output> Resources { get; private set; } = null!;
+
+ protected CollectionComponentResource(string type, string name, ResourceOptions? options = null)
+ : base(type, name, options)
+ {
+ }
+
+ protected void RegisterResources(Output> resources)
+ {
+ Resources = resources;
+ RegisterOutputs(new Dictionary { { "resources", resources } });
+ }
+
+ ///
+ /// Returns a resource defined by the given name.
+ ///
+ /// Resource name.
+ /// Optional namespace, e.g. "kube-prometheus-exporter-kubernetes".
+ /// The type of the resource.
+ public Output GetResource(string name, string? namespaceName = null)
+ where T : KubernetesResource
+ {
+ var type = typeof(T);
+ var groupVersionKind =
+ {{#Groups}}
+ {{#Versions}}
+ {{#TopLevelKindsAndAliases}}
+ type == typeof({{Group}}.{{Version}}.{{Kind}}) ? "{{RawAPIVersion}}/{{Kind}}" :
+ {{/TopLevelKindsAndAliases}}
+ {{/Versions}}
+ {{/Groups}}
+ throw new ArgumentException($"Unknown resource type {typeof(T).FullName}");
+ var id = namespaceName != null ? $"{namespaceName}/{name}" : name;
+ return Resources.Apply(r =>
+ {
+ if (!r.TryGetValue($"{groupVersionKind}::{id}", out var value))
+ {
+ var existingKeys = string.Join(", ", r.Keys);
+ throw new ArgumentException($"Resource {groupVersionKind}::{id} of type {type.FullName} and id {id} is not found, existing resources are {existingKeys}");
+ }
+
+ return (T) value;
+ });
+ }
+
+ ///
+ /// Returns a custom resource defined by the given group/version/kind and name.
+ ///
+ /// Group/version/kind, e.g. "monitoring.coreos.com/v1/ServiceMonitor".
+ ///
+ /// Resource name.
+ /// Optional namespace, e.g. "kube-prometheus-exporter-kubernetes".
+ public Output GetCustomResource(string groupVersionKind, string name, string? namespaceName = null)
+ {
+ var id = namespaceName != null ? $"{namespaceName}/{name}" : name;
+ return Resources.Apply(r => (CustomResource)r[$"{groupVersionKind}::{id}"]);
+ }
+ }
+
+ internal static class Parser
+ {
+ public static Output> Parse(ConfigGroupArgs config, ComponentResourceOptions? options)
+ {
+ var resources = Output.Create(ImmutableDictionary.Create());
+
+ if (config.Files != null)
+ {
+ var files = new List();
+ foreach (var file in config.Files)
+ {
+ if (IsUrl(file))
+ files.Add(file);
+ else
+ files.AddRange(Glob.Files(Directory.GetCurrentDirectory(), file));
+ }
+
+ foreach (var file in files)
+ {
+ var cf = new ConfigFile(
+ file,
+ new ConfigFileArgs
+ {
+ File = file,
+ Transformations = config.Transformations,
+ ResourcePrefix = config.ResourcePrefix
+ },
+ options);
+ resources = Output.Tuple(resources, cf.Resources).Apply(vs => vs.Item1.AddRange(vs.Item2));
+ }
+ }
+
+ if (config.Yaml != null)
+ {
+ var yamlResources = config.Yaml.ToOutput().Apply(texts =>
+ {
+ var yamls = texts
+ .Select(text =>
+ ParseYamlDocument(new ParseArgs
+ {
+ Objs = Invokes.YamlDecode(new YamlDecodeArgs { Text = text }),
+ Transformations = config.Transformations,
+ ResourcePrefix = config.ResourcePrefix
+ }, options))
+ .Select(output => (Input>)output)
+ .ToImmutableArray();
+ return Output.All(yamls);
+ });
+
+ resources = Output.Tuple(resources, yamlResources).Apply(vs =>
+ {
+ var builder = ImmutableDictionary.CreateBuilder();
+ builder.AddRange(vs.Item1);
+ foreach (var bs in vs.Item2)
+ builder.AddRange(bs);
+ return builder.ToImmutable();
+ });
+ }
+
+ if (config.Objs != null)
+ {
+ var docResources = ParseYamlDocument(new ParseArgs
+ {
+ Objs = config.Objs,
+ Transformations = config.Transformations,
+ ResourcePrefix = config.ResourcePrefix
+ }, options);
+ resources = Output.Tuple(resources, docResources).Apply(vs => vs.Item1.AddRange(vs.Item2));
+ }
+
+ return resources;
+
+ }
+
+ internal static bool IsUrl(string s) => s.StartsWith("http://") || s.StartsWith("https://");
+
+ internal static Output> ParseYamlDocument(ParseArgs config,
+ ComponentResourceOptions? options = null)
+ {
+ return config.Objs.ToOutput().Apply(objs =>
+ {
+
+ var inputs = objs
+ .SelectMany(obj => ParseYamlObject(obj, config.Transformations, config.ResourcePrefix, options))
+ .Select(output => (Input<(string, KubernetesResource)>) output)
+ .ToImmutableArray();
+
+ return Output.All(inputs)
+ .Apply(items =>
+ items.Select(obj => new KeyValuePair(obj.Item1, obj.Item2))
+ .ToImmutableDictionary());
+ });
+ }
+
+ private static Output<(string, KubernetesResource)>[] ParseYamlObject(ImmutableDictionary obj,
+ TransformationAction[]? transformations, string? resourcePrefix, ComponentResourceOptions? options = null)
+ {
+ if (obj == null || obj.Count == 0)
+ return new Output<(string, KubernetesResource)>[0];
+
+ // Create custom resource options based on component resource options.
+ var opts = new CustomResourceOptions
+ {
+ Parent = options?.Parent,
+ DependsOn = options?.DependsOn ?? new InputList(),
+ IgnoreChanges = options?.IgnoreChanges ?? new List(),
+ Version = options?.Version,
+ Provider = options?.Provider,
+ CustomTimeouts = options?.CustomTimeouts
+ };
+
+ // Allow users to change API objects before any validation.
+ if (transformations != null)
+ {
+ foreach (var transform in transformations)
+ obj = transform(obj, opts);
+ }
+
+ if (!(obj.ContainsKey("kind") && obj.ContainsKey("apiVersion")))
+ {
+ var serialized = JsonSerializer.Serialize(obj);
+ throw new FormatException($"Kubernetes resources require a kind and apiVersion: {serialized}");
+ }
+
+ var kind = (string)obj["kind"];
+ var apiVersion = (string)obj["apiVersion"];
+
+ // Recursively traverse built-in Kubernetes list types into a single set of "naked" resource
+ // definitions that we can register with the Pulumi engine.
+ //
+ // Kubernetes does not instantiate list types like `v1.List`. When the API server receives
+ // a list, it will recursively traverse it and perform the necessary operations on the
+ // each "instantiable" resource it finds. For example, `kubectl apply` on a
+ // `v1.ConfigMapList` will cause the API server to traverse the list, and `apply` each
+ // `v1.ConfigMap` it finds.
+ //
+ // Since Kubernetes does not instantiate list types directly, Pulumi also traverses lists
+ // for resource definitions that can be managed by Kubernetes, and registers those with the
+ // engine instead.
+ if (
+ apiVersion == "v1" && kind == "List"
+ {{#Groups}}
+ {{#Versions}}
+ {{#ListTopLevelKindsAndAliases}}
+ || apiVersion == "{{RawAPIVersion}}" && kind == "{{Kind}}"
+ {{/ListTopLevelKindsAndAliases}}
+ {{/Versions}}
+ {{/Groups}}
+ )
+ {
+ var objs = new List