From ae44f395420e4fffdcaa93dc06c1dccd696db52f Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Thu, 14 Jan 2021 10:45:06 -0800 Subject: [PATCH] Revert ingress update for 1.18 or lower (#893) --- ...DeployApplicationKubernetesManifestStep.cs | 2 +- .../GenerateIngressKubernetesManifestStep.cs | 5 +- .../KubernetesManifestGenerator.cs | 40 +++-- src/Microsoft.Tye.Core/ValidateIngressStep.cs | 9 +- src/shared/KubectlDetector.cs | 28 ++- src/tye/Program.DeployCommand.cs | 2 +- test/E2ETest/Microsoft.Tye.E2ETests.csproj | 3 + test/E2ETest/TyeGenerateTests.cs | 12 +- .../generate/apps-with-ingress.1.18.yaml | 162 ++++++++++++++++++ ...gress.yaml => apps-with-ingress.1.19.yaml} | 0 10 files changed, 236 insertions(+), 27 deletions(-) create mode 100644 test/E2ETest/testassets/generate/apps-with-ingress.1.18.yaml rename test/E2ETest/testassets/generate/{apps-with-ingress.yaml => apps-with-ingress.1.19.yaml} (100%) diff --git a/src/Microsoft.Tye.Core/DeployApplicationKubernetesManifestStep.cs b/src/Microsoft.Tye.Core/DeployApplicationKubernetesManifestStep.cs index 68f2259e..89a2a5e4 100644 --- a/src/Microsoft.Tye.Core/DeployApplicationKubernetesManifestStep.cs +++ b/src/Microsoft.Tye.Core/DeployApplicationKubernetesManifestStep.cs @@ -18,7 +18,7 @@ namespace Microsoft.Tye { using var step = output.BeginStep("Applying Kubernetes Manifests..."); - if (!await KubectlDetector.IsKubectlInstalledAsync(output)) + if (await KubectlDetector.GetKubernetesServerVersion(output) == null) { throw new CommandException($"Cannot apply manifests because kubectl is not installed."); } diff --git a/src/Microsoft.Tye.Core/GenerateIngressKubernetesManifestStep.cs b/src/Microsoft.Tye.Core/GenerateIngressKubernetesManifestStep.cs index c62d0da0..67685e05 100644 --- a/src/Microsoft.Tye.Core/GenerateIngressKubernetesManifestStep.cs +++ b/src/Microsoft.Tye.Core/GenerateIngressKubernetesManifestStep.cs @@ -12,10 +12,9 @@ namespace Microsoft.Tye public string Environment { get; set; } = "production"; - public override Task ExecuteAsync(OutputContext output, ApplicationBuilder application, IngressBuilder ingress) + public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, IngressBuilder ingress) { - ingress.Outputs.Add(KubernetesManifestGenerator.CreateIngress(output, application, ingress)); - return Task.CompletedTask; + ingress.Outputs.Add(await KubernetesManifestGenerator.CreateIngress(output, application, ingress)); } } } diff --git a/src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs b/src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs index 055c6cec..2e28dc93 100644 --- a/src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs +++ b/src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs @@ -5,6 +5,7 @@ using System; using System.Globalization; using System.Linq; +using System.Threading.Tasks; using YamlDotNet.Core; using YamlDotNet.RepresentationModel; @@ -12,15 +13,25 @@ namespace Microsoft.Tye { internal static class KubernetesManifestGenerator { - public static KubernetesIngressOutput CreateIngress( + private static System.Version MinIngressVersion = new System.Version(1, 19); + + public static async Task CreateIngress( OutputContext output, ApplicationBuilder application, IngressBuilder ingress) { var root = new YamlMappingNode(); + var k8sVersion = await KubectlDetector.GetKubernetesServerVersion(output); root.Add("kind", "Ingress"); - root.Add("apiVersion", "networking.k8s.io/v1"); + if (k8sVersion >= MinIngressVersion) + { + root.Add("apiVersion", "networking.k8s.io/v1"); + } + else + { + root.Add("apiVersion", "extensions/v1beta1"); + } var metadata = new YamlMappingNode(); root.Add("metadata", metadata); @@ -74,10 +85,6 @@ namespace Microsoft.Tye var backend = new YamlMappingNode(); path.Add("backend", backend); - var backendService = new YamlMappingNode(); - backend.Add("service", backendService); - - backendService.Add("name", ingressRule.Service); var service = application.Services.FirstOrDefault(s => s.Name == ingressRule.Service); if (service is null) @@ -91,11 +98,25 @@ namespace Microsoft.Tye throw new InvalidOperationException($"Could not resolve an http binding for service '{service.Name}'."); } - var backendPort = new YamlMappingNode(); + if (k8sVersion >= MinIngressVersion) + { + var backendService = new YamlMappingNode(); + backend.Add("service", backendService); - backendService.Add("port", backendPort); + backendService.Add("name", ingressRule.Service); - backendPort.Add("number", (binding.Port ?? 80).ToString(CultureInfo.InvariantCulture)); + var backendPort = new YamlMappingNode(); + + backendService.Add("port", backendPort); + + backendPort.Add("number", (binding.Port ?? 80).ToString(CultureInfo.InvariantCulture)); + path.Add("pathType", "Prefix"); + } + else + { + backend.Add("serviceName", ingressRule.Service); + backend.Add("servicePort", (binding.Port ?? 80).ToString(CultureInfo.InvariantCulture)); + } // Tye implements path matching similar to this example: // https://kubernetes.github.io/ingress-nginx/examples/rewrite/ @@ -120,7 +141,6 @@ namespace Microsoft.Tye } // Only support prefix matching for now - path.Add("pathType", "Prefix"); } } diff --git a/src/Microsoft.Tye.Core/ValidateIngressStep.cs b/src/Microsoft.Tye.Core/ValidateIngressStep.cs index 55e20c86..f4709fb6 100644 --- a/src/Microsoft.Tye.Core/ValidateIngressStep.cs +++ b/src/Microsoft.Tye.Core/ValidateIngressStep.cs @@ -43,7 +43,7 @@ namespace Microsoft.Tye return; } - if (!await KubectlDetector.IsKubectlInstalledAsync(output)) + if (await KubectlDetector.GetKubernetesServerVersion(output) == null) { throw new CommandException($"Cannot validate ingress because kubectl is not installed."); } @@ -141,9 +141,10 @@ namespace Microsoft.Tye { // If we get here then we should deploy the ingress controller. - // For some reason, the first time we apply the ingress controller, an exception is thrown - // saying "ingress-nginx-admission-create" is invalid. Therefore, we are going to blindly assume the - // controller successfully ran. + // The first time we apply the ingress controller, the validating webhook will not have started. + // This causes an error to be returned from the process. As this always happens, we are going to + // not check the error returned and assume the kubectl command worked. This is double checked in + // the future as well when we try to create the ingress resource. output.WriteDebugLine($"Running 'kubectl apply'"); output.WriteCommandLine("kubectl", $"apply -f \"https://aka.ms/tye/ingress/deploy\""); diff --git a/src/shared/KubectlDetector.cs b/src/shared/KubectlDetector.cs index 575dd2ca..740a9793 100644 --- a/src/shared/KubectlDetector.cs +++ b/src/shared/KubectlDetector.cs @@ -3,16 +3,17 @@ // See the LICENSE file in the project root for more information. using System; +using System.Text.Json; using System.Threading.Tasks; namespace Microsoft.Tye { internal static class KubectlDetector { - private static Lazy> _kubectlInstalled = new Lazy>(DetectKubectlInstalled); + private static Lazy> _kubectlInstalled = new Lazy>(GetKubectlVersion); private static Lazy> _kubectlConnectedToCluster = new Lazy>(DetectKubectlConnectedToCluster); - public static Task IsKubectlInstalledAsync(OutputContext output) + public static Task GetKubernetesServerVersion(OutputContext output) { if (!_kubectlInstalled.IsValueCreated) { @@ -30,19 +31,34 @@ namespace Microsoft.Tye return _kubectlConnectedToCluster.Value; } - private static async Task DetectKubectlInstalled() + private static async Task GetKubectlVersion() { try { // Ignoring the exit code and relying on Process to throw if kubectl is not found // kubectl version will return non-zero if you're not connected to a cluster. - await ProcessUtil.RunAsync("kubectl", "version", throwOnError: false); - return true; + var result = await ProcessUtil.RunAsync("kubectl", "version -o json", throwOnError: false); + + var output = result.StandardOutput; + using var jsonDoc = JsonDocument.Parse(output); + foreach (JsonProperty element in jsonDoc.RootElement.EnumerateObject()) + { + if (element.Name != "serverVersion") + { + continue; + } + var major = int.Parse(element.Value.GetProperty("major").GetString()); + var minor = int.Parse(element.Value.GetProperty("minor").GetString()); + var version = new Version(major, minor); + return version; + } + + return null; } catch (Exception) { // Unfortunately, process throws - return false; + return null; } } diff --git a/src/tye/Program.DeployCommand.cs b/src/tye/Program.DeployCommand.cs index 4cef89e6..d8527994 100644 --- a/src/tye/Program.DeployCommand.cs +++ b/src/tye/Program.DeployCommand.cs @@ -64,7 +64,7 @@ namespace Microsoft.Tye { var watch = System.Diagnostics.Stopwatch.StartNew(); - if (!await KubectlDetector.IsKubectlInstalledAsync(output)) + if (await KubectlDetector.GetKubernetesServerVersion(output) == null) { throw new CommandException($"Cannot apply manifests because kubectl is not installed."); } diff --git a/test/E2ETest/Microsoft.Tye.E2ETests.csproj b/test/E2ETest/Microsoft.Tye.E2ETests.csproj index c1106d7e..71705c10 100644 --- a/test/E2ETest/Microsoft.Tye.E2ETests.csproj +++ b/test/E2ETest/Microsoft.Tye.E2ETests.csproj @@ -24,11 +24,14 @@ + + + diff --git a/test/E2ETest/TyeGenerateTests.cs b/test/E2ETest/TyeGenerateTests.cs index 2d1c1deb..7077feb3 100644 --- a/test/E2ETest/TyeGenerateTests.cs +++ b/test/E2ETest/TyeGenerateTests.cs @@ -379,9 +379,17 @@ namespace E2ETest // name of application is the folder var content = await File.ReadAllTextAsync(Path.Combine(projectDirectory.DirectoryPath, $"{applicationName}-generate-{environment}.yaml")); - var expectedContent = await File.ReadAllTextAsync($"testassets/generate/{applicationName}.yaml"); - YamlAssert.Equals(expectedContent, content, output); + if (await KubectlDetector.GetKubernetesServerVersion(outputContext) >= new Version(1, 19)) + { + var expectedContent = await File.ReadAllTextAsync($"testassets/generate/{applicationName}.1.19.yaml"); + YamlAssert.Equals(expectedContent, content, output); + } + else + { + var expectedContent = await File.ReadAllTextAsync($"testassets/generate/{applicationName}.1.18.yaml"); + YamlAssert.Equals(expectedContent, content, output); + } await DockerAssert.AssertImageExistsAsync(output, "appa"); await DockerAssert.AssertImageExistsAsync(output, "appb"); diff --git a/test/E2ETest/testassets/generate/apps-with-ingress.1.18.yaml b/test/E2ETest/testassets/generate/apps-with-ingress.1.18.yaml new file mode 100644 index 00000000..ae2ce23f --- /dev/null +++ b/test/E2ETest/testassets/generate/apps-with-ingress.1.18.yaml @@ -0,0 +1,162 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: appa + labels: + app.kubernetes.io/name: 'appa' + app.kubernetes.io/part-of: 'apps-with-ingress' +spec: + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/name: appa + template: + metadata: + labels: + app.kubernetes.io/name: 'appa' + app.kubernetes.io/part-of: 'apps-with-ingress' + spec: + containers: + - name: appa + image: appa:1.0.0 + imagePullPolicy: Always + env: + - name: DOTNET_LOGGING__CONSOLE__DISABLECOLORS + value: 'true' + - name: ASPNETCORE_URLS + value: 'http://*' + - name: PORT + value: '80' + - name: SERVICE__APPA__PROTOCOL + value: 'http' + - name: SERVICE__APPA__PORT + value: '80' + - name: SERVICE__APPA__HOST + value: 'appa' + - name: SERVICE__APPB__PROTOCOL + value: 'http' + - name: SERVICE__APPB__PORT + value: '80' + - name: SERVICE__APPB__HOST + value: 'appb' + ports: + - containerPort: 80 +... +--- +kind: Service +apiVersion: v1 +metadata: + name: appa + labels: + app.kubernetes.io/name: 'appa' + app.kubernetes.io/part-of: 'apps-with-ingress' +spec: + selector: + app.kubernetes.io/name: appa + type: ClusterIP + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 80 +... +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: appb + labels: + app.kubernetes.io/name: 'appb' + app.kubernetes.io/part-of: 'apps-with-ingress' +spec: + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/name: appb + template: + metadata: + labels: + app.kubernetes.io/name: 'appb' + app.kubernetes.io/part-of: 'apps-with-ingress' + spec: + containers: + - name: appb + image: appb:1.0.0 + imagePullPolicy: Always + env: + - name: DOTNET_LOGGING__CONSOLE__DISABLECOLORS + value: 'true' + - name: ASPNETCORE_URLS + value: 'http://*' + - name: PORT + value: '80' + - name: SERVICE__APPB__PROTOCOL + value: 'http' + - name: SERVICE__APPB__PORT + value: '80' + - name: SERVICE__APPB__HOST + value: 'appb' + - name: SERVICE__APPA__PROTOCOL + value: 'http' + - name: SERVICE__APPA__PORT + value: '80' + - name: SERVICE__APPA__HOST + value: 'appa' + ports: + - containerPort: 80 +... +--- +kind: Service +apiVersion: v1 +metadata: + name: appb + labels: + app.kubernetes.io/name: 'appb' + app.kubernetes.io/part-of: 'apps-with-ingress' +spec: + selector: + app.kubernetes.io/name: appb + type: ClusterIP + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 80 +... +--- +kind: Ingress +apiVersion: extensions/v1beta1 +metadata: + name: ingress + annotations: + kubernetes.io/ingress.class: 'nginx' + nginx.ingress.kubernetes.io/rewrite-target: '/$2' + labels: + app.kubernetes.io/part-of: 'apps-with-ingress' +spec: + rules: + - http: + paths: + - backend: + serviceName: appa + servicePort: 80 + path: /A(/|$)(.*) + - backend: + serviceName: appb + servicePort: 80 + path: /B(/|$)(.*) + - host: a.example.com + http: + paths: + - backend: + serviceName: appa + servicePort: 80 + path: /()(.*) + - host: b.example.com + http: + paths: + - backend: + serviceName: appb + servicePort: 80 + path: /()(.*) +... diff --git a/test/E2ETest/testassets/generate/apps-with-ingress.yaml b/test/E2ETest/testassets/generate/apps-with-ingress.1.19.yaml similarity index 100% rename from test/E2ETest/testassets/generate/apps-with-ingress.yaml rename to test/E2ETest/testassets/generate/apps-with-ingress.1.19.yaml