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
name: myapplication
registry: exampleuser
registry: ...
namespace: examplenamespace
network: examplenetwork
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.
#### `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:
- 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.
Allows storing the name of the container registry and optional credentials secret in configuration.
#### `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.
## 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` 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;
}
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)

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

@ -30,7 +30,7 @@ namespace Microsoft.Tye.ConfigModel
public string? Namespace { get; set; }
public string? Registry { get; set; }
public ConfigRegistry? Registry { 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)
{
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 ContainerRegistry(string hostname)
public ContainerRegistry(string hostname, string? pullSecret)
{
Hostname = hostname ?? throw new ArgumentNullException(nameof(hostname));
PullSecret = pullSecret;
}
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");
}
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)
{
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);
break;
case "registry":
app.Registry = YamlParser.GetScalarValue(key, child.Value);
app.Registry = ConfigRegistryParser.HandleRegistry(key, child.Value);
break;
case "containerEngine":
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);
if (!string.IsNullOrWhiteSpace(registry))
{
application.Registry = new ContainerRegistry(registry.Trim());
application.Registry = new ContainerRegistry(registry.Trim(), null);
}
}
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 application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
application.Registry = new ContainerRegistry("test");
application.Registry = new ContainerRegistry("test", null);
try
{
@ -70,7 +70,7 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
application.Registry = new ContainerRegistry("test");
application.Registry = new ContainerRegistry("test", null);
try
{
@ -105,7 +105,7 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
application.Registry = new ContainerRegistry("test");
application.Registry = new ContainerRegistry("test", null);
try
{
@ -143,7 +143,7 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
application.Registry = new ContainerRegistry("test");
application.Registry = new ContainerRegistry("test", null);
try
{
@ -181,7 +181,7 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
application.Registry = new ContainerRegistry("test");
application.Registry = new ContainerRegistry("test", null);
try
{
@ -228,7 +228,7 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile, "netcoreapp3.1");
application.Registry = new ContainerRegistry("test");
application.Registry = new ContainerRegistry("test", null);
try
{
@ -262,7 +262,7 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile, "netcoreapp3.1");
application.Registry = new ContainerRegistry("test");
application.Registry = new ContainerRegistry("test", null);
try
{

6
test/E2ETest/TyeBuildTests.cs

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

56
test/E2ETest/TyeGenerateTests.cs

@ -41,7 +41,7 @@ namespace E2ETest
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
// Need to add docker registry for generate
application.Registry = new ContainerRegistry("test");
application.Registry = new ContainerRegistry("test", null);
try
{
@ -79,7 +79,7 @@ namespace E2ETest
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
// Need to add docker registry for generate
application.Registry = new ContainerRegistry("test");
application.Registry = new ContainerRegistry("test", null);
try
{
@ -120,7 +120,7 @@ namespace E2ETest
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
// Need to add docker registry for generate
application.Registry = new ContainerRegistry("test");
application.Registry = new ContainerRegistry("test", null);
try
{
@ -146,9 +146,9 @@ namespace E2ETest
[ConditionalFact]
[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 environment = "production";
@ -160,21 +160,61 @@ namespace E2ETest
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
// Need to add docker registry with pull secret for generate
application.Registry = new ContainerRegistry("test", "credsecret");
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}-noregistry.yaml");
var expectedContent = await File.ReadAllTextAsync($"testassets/generate/{projectName}-registrypullsecret.yaml");
YamlAssert.Equals(expectedContent, content, output);
await DockerAssert.AssertImageExistsAsync(output, "test-project");
await DockerAssert.AssertImageExistsAsync(output, "test/test-project");
}
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