Browse Source

Support validating and prompting for secrets

We'll give you more info and choices if you're trying to deploy
something that would depend on k8s secrets you have haven't configured
yet.

Basically when we would use a secret we'll check k8s first, and then
if it's not found:
- Prompt for it
- Create it if you told us what to put there
- Otherwise print to the commandline the command you could use to
  create it.
pull/84/head
Ryan Nowak 7 years ago
parent
commit
7b7f160c99
  1. 166
      src/Tye.Core/ValidateSecretStep.cs
  2. 33
      src/tye/Program.DeployCommand.cs
  3. 2
      src/tye/Program.InitCommand.cs

166
src/Tye.Core/ValidateSecretStep.cs

@ -0,0 +1,166 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.CommandLine.Invocation;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using k8s;
using k8s.Models;
using Microsoft.Rest;
using YamlDotNet.RepresentationModel;
namespace Tye
{
public sealed class ValidateSecretStep : ServiceExecutor.Step
{
public override string DisplayText => "Validating Secrets...";
public string Environment { get; set; } = "production";
public bool Interactive { get; set; }
public bool Force { get; set; }
// Keep track of secrets we've seen so we don't validate them twice.
public HashSet<string> Secrets { get; } = new HashSet<string>();
public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
{
var bindings = service.Outputs.OfType<ComputedBindings>().FirstOrDefault();
if (bindings is null)
{
return;
}
foreach (var binding in bindings.Bindings)
{
if (binding is SecretInputBinding secretInputBinding)
{
if (!Secrets.Add(secretInputBinding.Name))
{
output.WriteDebugLine($"Already validated secret '{secretInputBinding.Name}'.");
continue;
}
output.WriteDebugLine($"Validating secret '{secretInputBinding.Name}'.");
var config = KubernetesClientConfiguration.BuildDefaultConfig();
var kubernetes = new Kubernetes(config);
try
{
var result = await kubernetes.ReadNamespacedSecretWithHttpMessagesAsync(secretInputBinding.Name, config.Namespace ?? "default");
output.WriteInfoLine($"Found existing secret '{secretInputBinding.Name}'.");
continue;
}
catch (HttpOperationException ex) when (ex.Response.StatusCode == HttpStatusCode.NotFound)
{
// The kubernetes client uses exceptions for 404s.
}
catch (Exception ex)
{
output.WriteDebugLine("Failed to query secret.");
output.WriteDebugLine(ex.ToString());
throw new CommandException("Unable connect to kubernetes.", ex);
}
if (Force)
{
output.WriteDebugLine("Skipping because force was specified.");
continue;
}
if (!Interactive)
{
throw new CommandException(
$"The secret '{secretInputBinding.Name}' used for service '{secretInputBinding.Service.Service.Name}' is missing from the deployment environment. " +
$"Rerun the command with --interactive to specify the value interactively, or with --force to skip validation. Alternatively " +
$"use the following command to manually create the secret." + System.Environment.NewLine +
$"kubectl create secret generic {secretInputBinding.Name} --from-literal=connectionstring=<value>");
}
// If we get here then we should create the sceret.
var text = output.Prompt($"Enter the connection string to use for service '{secretInputBinding.Service.Service.Name}'", allowEmpty: true);
if (string.IsNullOrWhiteSpace(text))
{
output.WriteAlways($"Skipping creation of secret for '{secretInputBinding.Service.Service.Name}'. This may prevent creation of pods until secrets are created.");
output.WriteAlways($"Manually create a secret with:");
output.WriteAlways($"kubectl create secret generic {secretInputBinding.Name} --from-literal=connectionstring=<value>");
continue;
}
var secret = new V1Secret(type: "Opaque", stringData: new Dictionary<string, string>()
{
{ "connectionstring", text },
});
secret.Metadata = new V1ObjectMeta();
secret.Metadata.Name = secretInputBinding.Name;
output.WriteDebugLine($"Creating secret '{secret.Metadata.Name}'.");
try
{
await kubernetes.CreateNamespacedSecretWithHttpMessagesAsync(secret, config.Namespace ?? "default");
output.WriteInfoLine($"Created secret '{secret.Metadata.Name}'.");
}
catch (Exception ex)
{
output.WriteDebugLine("Failed to create secret.");
output.WriteDebugLine(ex.ToString());
throw new CommandException("Failed to create secret.", ex);
}
}
}
var yaml = service.Outputs.OfType<IYamlManifestOutput>().ToArray();
if (yaml.Length == 0)
{
output.WriteDebugLine($"No yaml manifests found for service '{service.FriendlyName}'. Skipping.");
return;
}
using var tempFile = TempFile.Create();
output.WriteDebugLine($"Writing output to '{tempFile.FilePath}'.");
{
using var stream = File.OpenWrite(tempFile.FilePath);
using var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: -1, leaveOpen: true);
var yamlStream = new YamlStream(yaml.Select(y => y.Yaml));
yamlStream.Save(writer, assignAnchors: false);
}
// kubectl apply logic is implemented in the client in older versions of k8s. The capability
// to get the same behavior in the server isn't present in every version that's relevant.
//
// https://kubernetes.io/docs/reference/using-api/api-concepts/#server-side-apply
//
output.WriteDebugLine("Running 'kubectl apply'.");
output.WriteCommandLine("kubectl", $"apply -f \"{tempFile.FilePath}\"");
var capture = output.Capture();
var exitCode = await Process.ExecuteAsync(
$"kubectl",
$"apply -f \"{tempFile.FilePath}\"",
System.Environment.CurrentDirectory,
stdOut: capture.StdOut,
stdErr: capture.StdErr);
output.WriteDebugLine($"Done running 'kubectl apply' exit code: {exitCode}");
if (exitCode != 0)
{
throw new CommandException("'kubectl apply' failed.");
}
output.WriteInfoLine($"Deployed service '{service.FriendlyName}'.");
}
}
}

33
src/tye/Program.DeployCommand.cs

@ -23,7 +23,13 @@ namespace Tye
StandardOptions.Verbosity,
};
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, bool>((console, path, verbosity, interactive) =>
command.AddOption(new Option(new[] { "-f", "--force" })
{
Description = "Override validation and force deployment.",
Required = false
});
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, bool, bool>((console, path, verbosity, interactive, force) =>
{
// Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654
if (path is null)
@ -32,14 +38,24 @@ namespace Tye
}
var application = ConfigFactory.FromFile(path);
return ExecuteDeployAsync(new OutputContext(console, verbosity), application, environment: "production", interactive);
return ExecuteDeployAsync(new OutputContext(console, verbosity), application, environment: "production", interactive, force);
});
return command;
}
private static async Task ExecuteDeployAsync(OutputContext output, ConfigApplication application, string environment, bool interactive)
private static async Task ExecuteDeployAsync(OutputContext output, ConfigApplication application, string environment, bool interactive, bool force)
{
if (!await KubectlDetector.Instance.IsKubectlInstalled.Value)
{
throw new CommandException($"Cannot apply manifests because kubectl is not installed.");
}
if (!await KubectlDetector.Instance.IsKubectlConnectedToCluster.Value)
{
throw new CommandException($"Cannot apply manifests because kubectl is not connected to a cluster.");
}
var temporaryApplication = await CreateApplicationAdapterAsync(output, application, interactive);
var steps = new List<ServiceExecutor.Step>()
{
@ -47,6 +63,7 @@ namespace Tye
new PublishProjectStep(),
new BuildDockerImageStep() { Environment = environment, },
new PushDockerImageStep() { Environment = environment, },
new ValidateSecretStep() { Environment = environment, Interactive = interactive, Force = force, },
};
steps.Add(new GenerateKubernetesManifestStep() { Environment = environment, });
@ -161,16 +178,6 @@ namespace Tye
await ApplicationYamlWriter.WriteAsync(output, writer, application);
}
if (!await KubectlDetector.Instance.IsKubectlInstalled.Value)
{
throw new CommandException($"Cannot apply manifests because kubectl is not installed.");
}
if (!await KubectlDetector.Instance.IsKubectlConnectedToCluster.Value)
{
throw new CommandException($"Cannot apply manifests because kubectl is not connected to a cluster.");
}
output.WriteDebugLine("Running 'kubectl apply'.");
output.WriteCommandLine("kubectl", $"apply -f \"{tempFile.FilePath}\"");
var capture = output.Capture();

2
src/tye/Program.InitCommand.cs

@ -21,7 +21,7 @@ namespace Tye
CommonArguments.Path_Optional,
};
command.AddOption(new Option("--force")
command.AddOption(new Option(new[] { "-f", "--force" })
{
Description = "Overrides the tye.yaml file if already present for project.",
Required = false

Loading…
Cancel
Save