diff --git a/docs/content/en/docs/workflows/debug.md b/docs/content/en/docs/workflows/debug.md index 1495a0aacfb..497be5e7b53 100644 --- a/docs/content/en/docs/workflows/debug.md +++ b/docs/content/en/docs/workflows/debug.md @@ -30,6 +30,7 @@ Debugging is currently supported for: - NodeJS (runtime ID: `nodejs`) - Java and JVM languages (runtime ID: `jvm`) - Python (runtime ID: `python`) + - .NET Core (runtime ID: `netcore`) Note that many debuggers may require additional information for the location of source files. We are looking for ways to identify this information and to pass it back if found. @@ -101,6 +102,53 @@ The DAP is supported by Visual Studio Code, [Eclipse LSP4e](https://projects.ecl [and other editors and IDEs](https://microsoft.github.io/debug-adapter-protocol/implementors/tools/). DAP is not yet supported by JetBrains IDEs like PyCharm. +#### .NET Core + +.NET Core applications are configured to be deployed along with `vsdbg`. + +In order to configure your application for debugging, your app must be: + +- Identified as being dotnet-based by having an entrypoint using [dotnet](https://github.com/dotnet/sdk) cli + or one of the following environment variables `ASPNETCORE_URLS`, `DOTNET_RUNNING_IN_CONTAINER`, + `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT`. +- Built with the `--configuration Debug` options to disable optimizations. + +**Note for users of [VS Code's debug adapter for C#](https://github.com/OmniSharp/omnisharp-vscode):** +the following configuration can be used to debug a container. It assumes that your code is deployed +in `/app` or `/src` folder in the container. If that is not the case, the `sourceFileMap` property +should be changed to match the correct folder. `processId` is usually 1 but might be different if you +have an unusual entrypoint. You can also use `"${command:pickRemoteProcess}"` instead if supported by +your base image. (`//` comments must be stripped.) +```json +{ + "name": "Skaffold Debug", + "type": "coreclr", + "request": "attach", + "processId" : 1, + "justMyCode": true, // set to `true` in debug configuration and `false` in release configuration + "pipeTransport": { + "pipeProgram": "kubectl", + "pipeArgs": [ + "exec", + "-i", + "", // name of the pod you debug. + "--" + ], + "pipeCwd": "${workspaceFolder}", + "debuggerPath": "/dbg/netcore/vsdbg", // location where vsdbg binary installed. + "quoteArgs": false + }, + "sourceFileMap": { + // Change this mapping if your app in not deployed in /src or /app in your docker image + "/src": "${workspaceFolder}", + "/app": "${workspaceFolder}" + // May also be like this, depending of your repository layout + // "/src": "${workspaceFolder}/src", + // "/app": "${workspaceFolder}/src/" + } +} +``` + ## IDE Support via Events and Metadata `debug` provides additional support for IDEs to detect the debuggable containers and to determine diff --git a/integration/debug_test.go b/integration/debug_test.go index c38923a9a7d..66b4d55e615 100644 --- a/integration/debug_test.go +++ b/integration/debug_test.go @@ -39,19 +39,19 @@ func TestDebug(t *testing.T) { { description: "kubectl", deployments: []string{"java"}, - pods: []string{"nodejs", "npm", "python3", "go"}, + pods: []string{"nodejs", "npm", "python3", "go", "netcore"}, }, { description: "kustomize", args: []string{"--profile", "kustomize"}, deployments: []string{"java"}, - pods: []string{"nodejs", "npm", "python3", "go"}, + pods: []string{"nodejs", "npm", "python3", "go", "netcore"}, }, { description: "buildpacks", args: []string{"--profile", "buildpacks"}, deployments: []string{"java"}, - pods: []string{"nodejs", "npm", "python3", "go"}, + pods: []string{"nodejs", "npm", "python3", "go", "netcore"}, }, } for _, test := range tests { diff --git a/integration/testdata/debug/.gitignore b/integration/testdata/debug/.gitignore index 1bd722694bd..fa1b78aea1e 100644 --- a/integration/testdata/debug/.gitignore +++ b/integration/testdata/debug/.gitignore @@ -1,2 +1,4 @@ node_modules *.swp +netcore/**/obj +netcore/**/bin \ No newline at end of file diff --git a/integration/testdata/debug/kustomization.yaml b/integration/testdata/debug/kustomization.yaml index 25c3745769a..120281340b4 100644 --- a/integration/testdata/debug/kustomization.yaml +++ b/integration/testdata/debug/kustomization.yaml @@ -4,3 +4,4 @@ resources: - npm/k8s/pod.yaml - python3/k8s/pod.yaml - go/k8s/pod.yaml + - netcore/k8s/pod.yaml diff --git a/integration/testdata/debug/netcore/Dockerfile b/integration/testdata/debug/netcore/Dockerfile new file mode 100644 index 00000000000..2d523b84c65 --- /dev/null +++ b/integration/testdata/debug/netcore/Dockerfile @@ -0,0 +1,23 @@ +# See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile +# to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build +COPY ["src/HelloWorld/HelloWorld.csproj", "src/HelloWorld/"] +RUN dotnet restore "src/HelloWorld/HelloWorld.csproj" +COPY . . +WORKDIR "/src/HelloWorld" +RUN ls -al +RUN dotnet build "HelloWorld.csproj" --configuration Debug -o /app/build + +FROM build AS publish +RUN dotnet publish "HelloWorld.csproj" --configuration Debug -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "HelloWorld.dll"] \ No newline at end of file diff --git a/integration/testdata/debug/netcore/k8s/pod.yaml b/integration/testdata/debug/netcore/k8s/pod.yaml new file mode 100644 index 00000000000..d5e06c872f5 --- /dev/null +++ b/integration/testdata/debug/netcore/k8s/pod.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Pod +metadata: + name: netcore +spec: + containers: + - name: dotnet-web + image: skaffold-debug-netcore + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + failureThreshold: 4 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + failureThreshold: 30 + timeoutSeconds: 5 \ No newline at end of file diff --git a/integration/testdata/debug/netcore/src/HelloWorld/Controllers/HomeController.cs b/integration/testdata/debug/netcore/src/HelloWorld/Controllers/HomeController.cs new file mode 100644 index 00000000000..9dca6c561f7 --- /dev/null +++ b/integration/testdata/debug/netcore/src/HelloWorld/Controllers/HomeController.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; + +namespace HelloWorld.Controllers +{ + [ApiController] + [Route("/")] + public class HomeController : ControllerBase + { + [HttpGet] + public string Get() + { + return "Ok"; + } + } +} diff --git a/integration/testdata/debug/netcore/src/HelloWorld/HelloWorld.csproj b/integration/testdata/debug/netcore/src/HelloWorld/HelloWorld.csproj new file mode 100644 index 00000000000..40e2b902fae --- /dev/null +++ b/integration/testdata/debug/netcore/src/HelloWorld/HelloWorld.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.1 + + + diff --git a/integration/testdata/debug/netcore/src/HelloWorld/Program.cs b/integration/testdata/debug/netcore/src/HelloWorld/Program.cs new file mode 100644 index 00000000000..7a863520d22 --- /dev/null +++ b/integration/testdata/debug/netcore/src/HelloWorld/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace HelloWorld +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/integration/testdata/debug/netcore/src/HelloWorld/Startup.cs b/integration/testdata/debug/netcore/src/HelloWorld/Startup.cs new file mode 100644 index 00000000000..d9b2e14709d --- /dev/null +++ b/integration/testdata/debug/netcore/src/HelloWorld/Startup.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace HelloWorld +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/integration/testdata/debug/netcore/src/HelloWorld/appsettings.json b/integration/testdata/debug/netcore/src/HelloWorld/appsettings.json new file mode 100644 index 00000000000..81ff877711d --- /dev/null +++ b/integration/testdata/debug/netcore/src/HelloWorld/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/integration/testdata/debug/skaffold.yaml b/integration/testdata/debug/skaffold.yaml index 3ad84918c42..e92943bd234 100644 --- a/integration/testdata/debug/skaffold.yaml +++ b/integration/testdata/debug/skaffold.yaml @@ -15,6 +15,8 @@ build: context: python3 - image: skaffold-debug-go context: go + - image: skaffold-debug-netcore + context: netcore deploy: kubectl: @@ -24,6 +26,7 @@ deploy: - npm/k8s/pod.yaml - python3/k8s/pod.yaml - go/k8s/pod.yaml + - netcore/k8s/pod.yaml profiles: - name: kustomize @@ -54,3 +57,7 @@ profiles: context: go buildpacks: builder: "gcr.io/buildpacks/builder:v1" + - image: skaffold-debug-netcore + context: netcore + buildpacks: + builder: "gcr.io/buildpacks/builder:v1" diff --git a/pkg/skaffold/debug/transform.go b/pkg/skaffold/debug/transform.go index a61ad7b8128..52e60776811 100644 --- a/pkg/skaffold/debug/transform.go +++ b/pkg/skaffold/debug/transform.go @@ -71,7 +71,7 @@ import ( type ContainerDebugConfiguration struct { // Artifact is the corresponding artifact's image name used in the skaffold.yaml Artifact string `json:"artifact,omitempty"` - // Runtime represents the underlying language runtime (`go`, `jvm`, `nodejs`, `python`) + // Runtime represents the underlying language runtime (`go`, `jvm`, `nodejs`, `python`, `netcore`) Runtime string `json:"runtime,omitempty"` // WorkingDir is the working directory in the image configuration; may be empty WorkingDir string `json:"workingDir,omitempty"` diff --git a/pkg/skaffold/debug/transform_netcore.go b/pkg/skaffold/debug/transform_netcore.go new file mode 100644 index 00000000000..871affec5d8 --- /dev/null +++ b/pkg/skaffold/debug/transform_netcore.go @@ -0,0 +1,76 @@ +/* +Copyright 2020 The Skaffold Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package debug + +import ( + "strings" + + "github.com/sirupsen/logrus" + v1 "k8s.io/api/core/v1" +) + +type netcoreTransformer struct{} + +func init() { + containerTransforms = append(containerTransforms, netcoreTransformer{}) +} + +// isLaunchingNetcore determines if the arguments seems to be invoking dotnet +func isLaunchingNetcore(args []string) bool { + if len(args) < 2 { + return false + } + + if args[0] == "dotnet" || strings.HasSuffix(args[0], "/dotnet") { + return true + } + + if args[0] == "exec" && (args[1] == "dotnet" || strings.HasSuffix(args[1], "/dotnet")) { + return true + } + + return false +} + +func (t netcoreTransformer) IsApplicable(config imageConfiguration) bool { + // Some official base images (eg: dotnet/core/runtime-deps) contain the following env vars + for _, v := range []string{"ASPNETCORE_URLS", "DOTNET_RUNNING_IN_CONTAINER", "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"} { + if _, found := config.env[v]; found { + return true + } + } + + if len(config.entrypoint) > 0 && !isEntrypointLauncher(config.entrypoint) { + return isLaunchingNetcore(config.entrypoint) + } + + if len(config.arguments) > 0 { + return isLaunchingNetcore(config.arguments) + } + + return false +} + +// Apply configures a container definition for vsdbg. +// Returns a simple map describing the debug configuration details. +func (t netcoreTransformer) Apply(container *v1.Container, config imageConfiguration, portAlloc portAllocator) (ContainerDebugConfiguration, string, error) { + logrus.Infof("Configuring %q for netcore debugging", container.Name) + + return ContainerDebugConfiguration{ + Runtime: "netcore", + }, "netcore", nil +} diff --git a/pkg/skaffold/debug/transform_netcore_test.go b/pkg/skaffold/debug/transform_netcore_test.go new file mode 100644 index 00000000000..fb95bee766f --- /dev/null +++ b/pkg/skaffold/debug/transform_netcore_test.go @@ -0,0 +1,131 @@ +/* +Copyright 2020 The Skaffold Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package debug + +import ( + "testing" + + v1 "k8s.io/api/core/v1" + + "github.com/GoogleContainerTools/skaffold/testutil" +) + +func TestNetcoreTransformer_IsApplicable(t *testing.T) { + tests := []struct { + description string + source imageConfiguration + launcher string + result bool + }{ + { + description: "ASPNETCORE_URLS", + source: imageConfiguration{env: map[string]string{"ASPNETCORE_URLS": "http://+:80"}}, + result: true, + }, + { + description: "DOTNET_RUNNING_IN_CONTAINER", + source: imageConfiguration{env: map[string]string{"DOTNET_RUNNING_IN_CONTAINER": "true"}}, + result: true, + }, + { + description: "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", + source: imageConfiguration{env: map[string]string{"DOTNET_SYSTEM_GLOBALIZATION_INVARIANT": "true"}}, + result: true, + }, + { + description: "entrypoint with dotnet", + source: imageConfiguration{entrypoint: []string{"dotnet", "myapp.dll"}}, + result: true, + }, + { + description: "entrypoint /bin/sh", + source: imageConfiguration{entrypoint: []string{"/bin/sh"}}, + result: false, + }, + { + description: "launcher entrypoint exec", + source: imageConfiguration{entrypoint: []string{"launcher"}, arguments: []string{"exec", "dotnet", "myapp.dll"}}, + launcher: "launcher", + result: true, + }, + { + description: "launcher entrypoint and random dotnet string", + source: imageConfiguration{entrypoint: []string{"launcher"}, arguments: []string{"echo", "dotnet"}}, + launcher: "launcher", + result: false, + }, + { + description: "nothing", + source: imageConfiguration{}, + result: false, + }, + } + + for _, test := range tests { + testutil.Run(t, test.description, func(t *testutil.T) { + t.Override(&entrypointLaunchers, []string{test.launcher}) + result := netcoreTransformer{}.IsApplicable(test.source) + + t.CheckDeepEqual(test.result, result) + }) + } +} + +func TestNetcoreTransformerApply(t *testing.T) { + tests := []struct { + description string + containerSpec v1.Container + configuration imageConfiguration + shouldErr bool + result v1.Container + debugConfig ContainerDebugConfiguration + image string + }{ + { + description: "empty", + containerSpec: v1.Container{}, + configuration: imageConfiguration{}, + + debugConfig: ContainerDebugConfiguration{Runtime: "netcore"}, + image: "netcore", + shouldErr: false, + }, + { + description: "basic", + containerSpec: v1.Container{}, + configuration: imageConfiguration{entrypoint: []string{"dotnet", "myapp.dll"}}, + + result: v1.Container{}, + debugConfig: ContainerDebugConfiguration{Runtime: "netcore"}, + image: "netcore", + shouldErr: false, + }, + } + var identity portAllocator = func(port int32) int32 { + return port + } + for _, test := range tests { + testutil.Run(t, test.description, func(t *testutil.T) { + config, image, err := netcoreTransformer{}.Apply(&test.containerSpec, test.configuration, identity) + + t.CheckError(test.shouldErr, err) + t.CheckDeepEqual(test.result, test.containerSpec) + t.CheckDeepEqual(test.debugConfig, config) + t.CheckDeepEqual(test.image, image) + }) + } +}