Add init debugging support

This adds Delve into the initramfs and a conditional hook which attaches Delve to our init
after the network is up. This allows for breakpoint-debugging the init itself, at least after the
very early node bringup.

Test Plan:
`bazel run -c dbg //:launch`, then use IDEA's Go Remote target to connect to localhost:2345
and set a breakpoint.

Bug: T786

X-Origin-Diff: phab/D581
GitOrigin-RevId: f6b32e7b7f4d36c8492df3e11ee97588817dbd8e
diff --git a/core/BUILD b/core/BUILD
index fcfc049..23cb537 100644
--- a/core/BUILD
+++ b/core/BUILD
@@ -1,5 +1,14 @@
 load("//core/build:def.bzl", "smalltown_initramfs")
 
+# debug_build checks if we're building in debug mode and enables various debug features for the image. Currently this
+# is only used for attaching a Delve debugger to init when it's enabled.
+config_setting(
+    name = "debug_build",
+    values = {
+        "compilation_mode": "dbg",
+    },
+)
+
 smalltown_initramfs(
     name = "initramfs",
     extra_dirs = [
@@ -39,6 +48,9 @@
         "@com_github_cilium_cilium//cilium": "/cilium/bin/cilium",
         "@com_github_cilium_cilium//daemon": "/cilium/bin/daemon",
         "@com_github_cilium_cilium//operator": "/cilium/bin/operator",
+
+        # Delve
+        "@com_github_go_delve_delve//cmd/dlv:dlv": "/dlv",
     },
 )
 
diff --git a/core/cmd/init/BUILD.bazel b/core/cmd/init/BUILD.bazel
index 1e5db46..af37ea6 100644
--- a/core/cmd/init/BUILD.bazel
+++ b/core/cmd/init/BUILD.bazel
@@ -2,13 +2,18 @@
 
 go_library(
     name = "go_default_library",
+    # keep
     srcs = [
         "main.go",
         "switchroot.go",
-    ],
+    ] + select({
+        "//core:debug_build": ["debug_enabled.go"],
+        "//conditions:default": ["debug_disabled.go"],
+    }),
     importpath = "git.monogon.dev/source/nexantic.git/core/cmd/init",
     visibility = ["//visibility:private"],
     deps = [
+        "//core/internal/common:go_default_library",  # keep
         "//core/internal/common/supervisor:go_default_library",
         "//core/internal/network:go_default_library",
         "//core/internal/node:go_default_library",
diff --git a/core/cmd/init/debug_disabled.go b/core/cmd/init/debug_disabled.go
new file mode 100644
index 0000000..06bcad0
--- /dev/null
+++ b/core/cmd/init/debug_disabled.go
@@ -0,0 +1,23 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// 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 main
+
+import "git.monogon.dev/source/nexantic.git/core/internal/network"
+
+// initializeDebugger does nothing in a non-debug build
+func initializeDebugger(*network.Service) {
+}
diff --git a/core/cmd/init/debug_enabled.go b/core/cmd/init/debug_enabled.go
new file mode 100644
index 0000000..1c2af00
--- /dev/null
+++ b/core/cmd/init/debug_enabled.go
@@ -0,0 +1,42 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// 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 main
+
+import (
+	"context"
+	"fmt"
+	"os/exec"
+
+	"git.monogon.dev/source/nexantic.git/core/internal/common"
+	"git.monogon.dev/source/nexantic.git/core/internal/network"
+)
+
+// initializeDebugger attaches Delve to ourselves and exposes it on common.DebuggerPort
+// This is coupled to compilation_mode=dbg because otherwise Delve doesn't have the necessary DWARF debug info
+func initializeDebugger(networkSvc *network.Service) {
+	go func() {
+		// This is intentionally delayed until network becomes available since Delve for some reason connects to itself
+		// and in early-boot no network interface is available to do that through. Also external access isn't possible
+		// early on anyways.
+		networkSvc.GetIP(context.Background(), true)
+		dlvCmd := exec.Command("/dlv", "--headless=true", fmt.Sprintf("--listen=:%v", common.DebuggerPort),
+			"--accept-multiclient", "--only-same-user=false", "attach", "--continue", "1", "/init")
+		if err := dlvCmd.Start(); err != nil {
+			panic(err)
+		}
+	}()
+}
diff --git a/core/cmd/init/main.go b/core/cmd/init/main.go
index 4fc949d..f5b09f8 100644
--- a/core/cmd/init/main.go
+++ b/core/cmd/init/main.go
@@ -86,6 +86,9 @@
 		panic(err)
 	}
 
+	// This function initializes a headless Delve if this is a debug build or does nothing if it's not
+	initializeDebugger(networkSvc)
+
 	supervisor.New(context.Background(), logger, func(ctx context.Context) error {
 		if err := supervisor.Run(ctx, "network", networkSvc.Run); err != nil {
 			return err
diff --git a/core/cmd/launch/main.go b/core/cmd/launch/main.go
index ff5c4d5..9bb4732 100644
--- a/core/cmd/launch/main.go
+++ b/core/cmd/launch/main.go
@@ -34,7 +34,7 @@
 		<-sigs
 		cancel()
 	}()
-	if err := launch.Launch(ctx, launch.Options{Ports: launch.IdentityPortMap()}); err != nil {
+	if err := launch.Launch(ctx, launch.Options{Ports: launch.IdentityPortMap(), SerialPort: os.Stdout}); err != nil {
 		if err == ctx.Err() {
 			return
 		}
diff --git a/core/internal/common/setup.go b/core/internal/common/setup.go
index 531b688..fa5cd59 100644
--- a/core/internal/common/setup.go
+++ b/core/internal/common/setup.go
@@ -35,6 +35,7 @@
 	ExternalServicePort = 7836
 	DebugServicePort    = 7837
 	KubernetesAPIPort   = 6443
+	DebuggerPort        = 2345
 )
 
 const (
diff --git a/core/internal/launch/launch.go b/core/internal/launch/launch.go
index 8aa865f..774b432 100644
--- a/core/internal/launch/launch.go
+++ b/core/internal/launch/launch.go
@@ -148,7 +148,7 @@
 }
 
 var requiredPorts = []uint16{common.ConsensusPort, common.NodeServicePort, common.MasterServicePort,
-	common.ExternalServicePort, common.DebugServicePort, common.KubernetesAPIPort}
+	common.ExternalServicePort, common.DebugServicePort, common.KubernetesAPIPort, common.DebuggerPort}
 
 // IdentityPortMap returns a port map where each VM port is mapped onto itself on the host. This is mainly useful
 // for development against Smalltown. The dbg command requires this mapping.