Browse Source

allows specifying the Kubernetes imagePullSecrets (#884)

* allows specifying the Kubernetes imagePullSecrets in configuration to pull from a private registry

* removed trailing whitespace 🙄

* removes interactive prompt for pull secret

* adds copyright info

* renames registry.hostname to regisry.name in tye.yaml schema

* reverted unnecessary change to project file

* fixes registry entry in schema documentation

Co-authored-by: David Fowler <davidfowl@gmail.com>
pull/1494/head
Thomas Freudenberg 4 years ago
committed by GitHub
parent
commit
63431360f7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 40
      docs/reference/schema.md
  2. 4
      src/Microsoft.Tye.Core/ApplicationFactory.cs
  3. 12
      src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs
  4. 16
      src/Microsoft.Tye.Core/ConfigModel/ConfigRegistry.cs
  5. 4
      src/Microsoft.Tye.Core/ContainerRegistry.cs
  6. 10
      src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs
  7. 2
      src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs
  8. 58
      src/Microsoft.Tye.Core/Serialization/ConfigRegistryParser.cs
  9. 2
      src/tye/Program.DeployCommand.cs
  10. 14
      test/E2ETest/TyeBuildTests.Dockerfile.cs
  11. 6
      test/E2ETest/TyeBuildTests.cs
  12. 56
      test/E2ETest/TyeGenerateTests.cs
  13. 58
      test/E2ETest/testassets/generate/single-project-registrypullsecret.yaml

40
docs/reference/schema.md

@ -45,7 +45,7 @@ services:
```yaml ```yaml
name: myapplication name: myapplication
registry: exampleuser registry: ...
namespace: examplenamespace namespace: examplenamespace
network: examplenetwork network: examplenetwork
ingress: ... ingress: ...
@ -60,16 +60,9 @@ Configures the name of the application. This will appear in some Kubernetes labe
If the name name is not specified, then the lowercased directory name containing the `tye.yaml` file will be used as the default. If the name name is not specified, then the lowercased directory name containing the `tye.yaml` file will be used as the default.
#### `registry` (string) #### `registry` (`Registry`)
Allows storing the name of the container registry in configuration. This is used when building and deploying images for two purposes: Allows storing the name of the container registry and optional credentials secret in configuration.
- Determining how to tag the images
- Determining where to push the images
The registry could be a DockerHub username (`exampleuser`) or the hostname of a container registry (`example.azurecr.io`).
If this is not specified in configuration, interactive deployments will prompt for it.
#### `namespace` (string) #### `namespace` (string)
@ -105,6 +98,33 @@ Specifies the list of services. Applications must have at least one service.
Indicates the solution file (.sln) or filter (.slnf) to use when building project-based services in watch mode. If omitted, those services will be built individually. Specifying the solution [filter] can help reduce repeated builds of shared libraries when in watch mode. Indicates the solution file (.sln) or filter (.slnf) to use when building project-based services in watch mode. If omitted, those services will be built individually. Specifying the solution [filter] can help reduce repeated builds of shared libraries when in watch mode.
## Registry
Allows storing the name of the container registry in configuration. This is used when building and deploying images for two purposes:
- Determining how to tag the images
- Determining where to push the images
If this is not specified in configuration, interactive deployments will prompt for it.
### Registry Example
```yaml
registry:
name: example.azurecr.io
pullSecret: acr-secret
```
### Registry Properties
#### `name` (string) *required*
The `name` could be a DockerHub username (`exampleuser`) or the hostname of a container registry (`example.azurecr.io`).
#### `pullSecret` (string)
Specifies the optional secret, that Kubernetes should get the credentials from to pull the image from a private registry.
## Service ## Service
`Service` elements appear in a list within the `services` root property. `Service` elements appear in a list within the `services` root property.

4
src/Microsoft.Tye.Core/ApplicationFactory.cs

@ -48,9 +48,9 @@ namespace Microsoft.Tye
continue; continue;
} }
if (config == rootConfig && !string.IsNullOrEmpty(config.Registry)) if (config == rootConfig && config.Registry != null)
{ {
root.Registry = new ContainerRegistry(config.Registry); root.Registry = new ContainerRegistry(config.Registry.Hostname, config.Registry.PullSecret);
} }
if (config == rootConfig) if (config == rootConfig)

12
src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs

@ -30,7 +30,7 @@ namespace Microsoft.Tye.ConfigModel
public string? Namespace { get; set; } public string? Namespace { get; set; }
public string? Registry { get; set; } public ConfigRegistry? Registry { get; set; }
public ContainerEngineType? ContainerEngineType { get; set; } public ContainerEngineType? ContainerEngineType { get; set; }
@ -64,6 +64,16 @@ namespace Microsoft.Tye.ConfigModel
} }
} }
if (config.Registry != null)
{
if (!Validator.TryValidateObject(config.Registry, new ValidationContext(config.Registry), results, validateAllProperties: true))
{
throw new TyeYamlException(
"Registry validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}
}
foreach (var service in config.Services) foreach (var service in config.Services)
{ {
context = new ValidationContext(service); context = new ValidationContext(service);

16
src/Microsoft.Tye.Core/ConfigModel/ConfigRegistry.cs

@ -0,0 +1,16 @@
// 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.ComponentModel.DataAnnotations;
namespace Microsoft.Tye.ConfigModel
{
public class ConfigRegistry
{
[Required]
public string Hostname { get; set; } = null!;
public string? PullSecret { get; set; }
}
}

4
src/Microsoft.Tye.Core/ContainerRegistry.cs

@ -8,11 +8,13 @@ namespace Microsoft.Tye
{ {
public sealed class ContainerRegistry public sealed class ContainerRegistry
{ {
public ContainerRegistry(string hostname) public ContainerRegistry(string hostname, string? pullSecret)
{ {
Hostname = hostname ?? throw new ArgumentNullException(nameof(hostname)); Hostname = hostname ?? throw new ArgumentNullException(nameof(hostname));
PullSecret = pullSecret;
} }
public string Hostname { get; } public string Hostname { get; }
public string? PullSecret { get; }
} }
} }

10
src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs

@ -491,6 +491,16 @@ namespace Microsoft.Tye
volumeMount.Add("mountPath", "/var/tye/diagnostics"); volumeMount.Add("mountPath", "/var/tye/diagnostics");
} }
if (!string.IsNullOrWhiteSpace(application.Registry?.PullSecret))
{
var imagePullSecrets = new YamlSequenceNode();
spec.Add("imagePullSecrets", imagePullSecrets);
var secretNode = new YamlMappingNode();
imagePullSecrets.Add(secretNode);
secretNode.Add("name", application.Registry.PullSecret);
}
if (!project.RelocateDiagnosticsDomainSockets) if (!project.RelocateDiagnosticsDomainSockets)
{ {
return new KubernetesDeploymentOutput(project.Name, new YamlDocument(root)); return new KubernetesDeploymentOutput(project.Name, new YamlDocument(root));

2
src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs

@ -31,7 +31,7 @@ namespace Tye.Serialization
app.Network = YamlParser.GetScalarValue(key, child.Value); app.Network = YamlParser.GetScalarValue(key, child.Value);
break; break;
case "registry": case "registry":
app.Registry = YamlParser.GetScalarValue(key, child.Value); app.Registry = ConfigRegistryParser.HandleRegistry(key, child.Value);
break; break;
case "containerEngine": case "containerEngine":
string engine = YamlParser.GetScalarValue(key, child.Value); string engine = YamlParser.GetScalarValue(key, child.Value);

58
src/Microsoft.Tye.Core/Serialization/ConfigRegistryParser.cs

@ -0,0 +1,58 @@
// 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 Microsoft.Tye.ConfigModel;
using YamlDotNet.RepresentationModel;
namespace Tye.Serialization
{
public class ConfigRegistryParser
{
public static ConfigRegistry HandleRegistry(string key, YamlNode node)
{
ConfigRegistry configRegistry;
if (node.NodeType == YamlNodeType.Scalar)
{
configRegistry = new ConfigRegistry
{
Hostname = ((YamlScalarNode)node).Value!
};
}
else if (node.NodeType == YamlNodeType.Mapping)
{
configRegistry = HandleRegistryMapping((YamlMappingNode)node);
}
else
{
throw new TyeYamlException(node.Start, CoreStrings.FormatExpectedYamlScalar(key));
}
return configRegistry;
}
private static ConfigRegistry HandleRegistryMapping(YamlMappingNode mappingNode)
{
var configRegistry = new ConfigRegistry();
foreach (var child in mappingNode.Children)
{
var key = YamlParser.GetScalarValue(child.Key);
switch (key)
{
case "name":
configRegistry.Hostname = YamlParser.GetScalarValue(key, child.Value);
break;
case "pullSecret":
configRegistry.PullSecret = YamlParser.GetScalarValue(key, child.Value);
break;
default:
throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key));
}
}
return configRegistry;
}
}
}

2
src/tye/Program.DeployCommand.cs

@ -118,7 +118,7 @@ namespace Microsoft.Tye
var registry = output.Prompt("Enter the Container Registry (ex: 'example.azurecr.io' for Azure or 'example' for dockerhub)", allowEmpty: !requireRegistry); var registry = output.Prompt("Enter the Container Registry (ex: 'example.azurecr.io' for Azure or 'example' for dockerhub)", allowEmpty: !requireRegistry);
if (!string.IsNullOrWhiteSpace(registry)) if (!string.IsNullOrWhiteSpace(registry))
{ {
application.Registry = new ContainerRegistry(registry.Trim()); application.Registry = new ContainerRegistry(registry.Trim(), null);
} }
} }
else if (application.Registry is null && requireRegistry) else if (application.Registry is null && requireRegistry)

14
test/E2ETest/TyeBuildTests.Dockerfile.cs

@ -34,7 +34,7 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug); var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
application.Registry = new ContainerRegistry("test"); application.Registry = new ContainerRegistry("test", null);
try try
{ {
@ -70,7 +70,7 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug); var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
application.Registry = new ContainerRegistry("test"); application.Registry = new ContainerRegistry("test", null);
try try
{ {
@ -105,7 +105,7 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug); var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
application.Registry = new ContainerRegistry("test"); application.Registry = new ContainerRegistry("test", null);
try try
{ {
@ -143,7 +143,7 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug); var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
application.Registry = new ContainerRegistry("test"); application.Registry = new ContainerRegistry("test", null);
try try
{ {
@ -181,7 +181,7 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug); var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
application.Registry = new ContainerRegistry("test"); application.Registry = new ContainerRegistry("test", null);
try try
{ {
@ -228,7 +228,7 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug); var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile, "netcoreapp3.1"); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile, "netcoreapp3.1");
application.Registry = new ContainerRegistry("test"); application.Registry = new ContainerRegistry("test", null);
try try
{ {
@ -262,7 +262,7 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug); var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile, "netcoreapp3.1"); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile, "netcoreapp3.1");
application.Registry = new ContainerRegistry("test"); application.Registry = new ContainerRegistry("test", null);
try try
{ {

6
test/E2ETest/TyeBuildTests.cs

@ -38,7 +38,7 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug); var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
application.Registry = new ContainerRegistry("test"); application.Registry = new ContainerRegistry("test", null);
try try
{ {
@ -69,7 +69,7 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug); var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
application.Registry = new ContainerRegistry("test"); application.Registry = new ContainerRegistry("test", null);
try try
{ {
@ -103,7 +103,7 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug); var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
application.Registry = new ContainerRegistry("test"); application.Registry = new ContainerRegistry("test", null);
try try
{ {

56
test/E2ETest/TyeGenerateTests.cs

@ -41,7 +41,7 @@ namespace E2ETest
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
// Need to add docker registry for generate // Need to add docker registry for generate
application.Registry = new ContainerRegistry("test"); application.Registry = new ContainerRegistry("test", null);
try try
{ {
@ -79,7 +79,7 @@ namespace E2ETest
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
// Need to add docker registry for generate // Need to add docker registry for generate
application.Registry = new ContainerRegistry("test"); application.Registry = new ContainerRegistry("test", null);
try try
{ {
@ -120,7 +120,7 @@ namespace E2ETest
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
// Need to add docker registry for generate // Need to add docker registry for generate
application.Registry = new ContainerRegistry("test"); application.Registry = new ContainerRegistry("test", null);
try try
{ {
@ -146,9 +146,9 @@ namespace E2ETest
[ConditionalFact] [ConditionalFact]
[SkipIfDockerNotRunning] [SkipIfDockerNotRunning]
public async Task GenerateWorksWithoutRegistry() public async Task GenerateWorksWithRegistryPullSecret()
{ {
await DockerAssert.DeleteDockerImagesAsync(output, "test-project"); await DockerAssert.DeleteDockerImagesAsync(output, "test/test-project");
var projectName = "single-project"; var projectName = "single-project";
var environment = "production"; var environment = "production";
@ -160,21 +160,61 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug); var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
// Need to add docker registry with pull secret for generate
application.Registry = new ContainerRegistry("test", "credsecret");
try try
{ {
await GenerateHost.ExecuteGenerateAsync(outputContext, application, environment, interactive: false); await GenerateHost.ExecuteGenerateAsync(outputContext, application, environment, interactive: false);
// name of application is the folder // name of application is the folder
var content = await File.ReadAllTextAsync(Path.Combine(projectDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml")); var content = await File.ReadAllTextAsync(Path.Combine(projectDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml"));
var expectedContent = await File.ReadAllTextAsync($"testassets/generate/{projectName}-noregistry.yaml"); var expectedContent = await File.ReadAllTextAsync($"testassets/generate/{projectName}-registrypullsecret.yaml");
YamlAssert.Equals(expectedContent, content, output); YamlAssert.Equals(expectedContent, content, output);
await DockerAssert.AssertImageExistsAsync(output, "test-project"); await DockerAssert.AssertImageExistsAsync(output, "test/test-project");
} }
finally finally
{ {
await DockerAssert.DeleteDockerImagesAsync(output, "test-project"); await DockerAssert.DeleteDockerImagesAsync(output, "test/test-project");
}
}
[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task GenerateWorksWithoutRegistry()
{
await DockerAssert.DeleteDockerImagesAsync(output, "test/test-project");
var projectName = "single-project";
var environment = "production";
using var projectDirectory = CopyTestProjectDirectory(projectName);
var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"));
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
// Need to add docker registry for generate
application.Registry = new ContainerRegistry("test", null);
try
{
await GenerateHost.ExecuteGenerateAsync(outputContext, application, environment, interactive: false);
// name of application is the folder
var content = await File.ReadAllTextAsync(Path.Combine(projectDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml"));
var expectedContent = await File.ReadAllTextAsync($"testassets/generate/{projectName}.yaml");
YamlAssert.Equals(expectedContent, content, output);
await DockerAssert.AssertImageExistsAsync(output, "test/test-project");
}
finally
{
await DockerAssert.DeleteDockerImagesAsync(output, "test/test-project");
} }
} }

58
test/E2ETest/testassets/generate/single-project-registrypullsecret.yaml

@ -0,0 +1,58 @@
kind: Deployment
apiVersion: apps/v1
metadata:
name: test-project
labels:
app.kubernetes.io/name: 'test-project'
app.kubernetes.io/part-of: 'single-project'
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: test-project
template:
metadata:
labels:
app.kubernetes.io/name: 'test-project'
app.kubernetes.io/part-of: 'single-project'
spec:
containers:
- name: test-project
image: test/test-project:1.0.0
imagePullPolicy: Always
env:
- name: DOTNET_LOGGING__CONSOLE__DISABLECOLORS
value: 'true'
- name: ASPNETCORE_URLS
value: 'http://*'
- name: PORT
value: '80'
- name: SERVICE__TEST-PROJECT__PROTOCOL
value: 'http'
- name: SERVICE__TEST-PROJECT__PORT
value: '80'
- name: SERVICE__TEST-PROJECT__HOST
value: 'test-project'
ports:
- containerPort: 80
imagePullSecrets:
- name: credsecret
...
---
kind: Service
apiVersion: v1
metadata:
name: test-project
labels:
app.kubernetes.io/name: 'test-project'
app.kubernetes.io/part-of: 'single-project'
spec:
selector:
app.kubernetes.io/name: test-project
type: ClusterIP
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
...
Loading…
Cancel
Save