From ce7a9f03146da33007656ded64be3148d6ec8d77 Mon Sep 17 00:00:00 2001 From: theunrealgeek Date: Sun, 17 May 2020 00:37:58 -0700 Subject: supporting k8s Deployment objects Signed-off-by: Aditya Kamath --- pkg/domain/infra/abi/play.go | 88 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 74 insertions(+), 14 deletions(-) (limited to 'pkg/domain/infra/abi') diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index 6d0919d2b..772cea5fd 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -26,6 +26,7 @@ import ( "github.com/ghodss/yaml" "github.com/pkg/errors" "github.com/sirupsen/logrus" + v1apps "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" ) @@ -38,13 +39,7 @@ const ( func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options entities.PlayKubeOptions) (*entities.PlayKubeReport, error) { var ( - containers []*libpod.Container - pod *libpod.Pod - podOptions []libpod.PodCreateOption - podYAML v1.Pod - registryCreds *types.DockerAuthConfig - writer io.Writer - report entities.PlayKubeReport + kubeObject v1.ObjectReference ) content, err := ioutil.ReadFile(path) @@ -52,19 +47,79 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options en return nil, err } - if err := yaml.Unmarshal(content, &podYAML); err != nil { + if err := yaml.Unmarshal(content, &kubeObject); err != nil { return nil, errors.Wrapf(err, "unable to read %q as YAML", path) } // NOTE: pkg/bindings/play is also parsing the file. // A pkg/kube would be nice to refactor and abstract // parts of the K8s-related code. - if podYAML.Kind != "Pod" { - return nil, errors.Errorf("invalid YAML kind: %q. Pod is the only supported Kubernetes YAML kind", podYAML.Kind) + switch kubeObject.Kind { + case "Pod": + var podYAML v1.Pod + var podTemplateSpec v1.PodTemplateSpec + if err := yaml.Unmarshal(content, &podYAML); err != nil { + return nil, errors.Wrapf(err, "unable to read YAML %q as Kube Pod", path) + } + podTemplateSpec.ObjectMeta = podYAML.ObjectMeta + podTemplateSpec.Spec = podYAML.Spec + return ic.playKubePod(ctx, podTemplateSpec.ObjectMeta.Name, &podTemplateSpec, options) + case "Deployment": + var deploymentYAML v1apps.Deployment + if err := yaml.Unmarshal(content, &deploymentYAML); err != nil { + return nil, errors.Wrapf(err, "unable to read YAML %q as Kube Deployment", path) + } + return ic.playKubeDeployment(ctx, &deploymentYAML, options) + default: + return nil, errors.Errorf("invalid YAML kind: %q. [Pod|Deployment] are the only supported Kubernetes Kinds", kubeObject.Kind) } +} + +func (ic *ContainerEngine) playKubeDeployment(ctx context.Context, deploymentYAML *v1apps.Deployment, options entities.PlayKubeOptions) (*entities.PlayKubeReport, error) { + var ( + deploymentName string + podSpec v1.PodTemplateSpec + numReplicas int32 + i int32 + report entities.PlayKubeReport + ) + + deploymentName = deploymentYAML.ObjectMeta.Name + if deploymentName == "" { + return nil, errors.Errorf("Deployment does not have a name") + } + numReplicas = 1 + if deploymentYAML.Spec.Replicas != nil { + numReplicas = *deploymentYAML.Spec.Replicas + } + podSpec = deploymentYAML.Spec.Template + + // create "replicas" number of pods + for i = 0; i < numReplicas; i++ { + podName := fmt.Sprintf("%s-pod-%d", deploymentName, i) + podReport, err := ic.playKubePod(ctx, podName, &podSpec, options) + if err != nil { + return nil, errors.Wrapf(err, "Error encountered while bringing up pod %s", podName) + } + report.Pods = append(report.Pods, podReport.Pods...) + report.Containers = append(report.Containers, podReport.Containers...) + report.Logs = append(report.Logs, podReport.Logs...) + } + return &report, nil +} + +func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podYAML *v1.PodTemplateSpec, options entities.PlayKubeOptions) (*entities.PlayKubeReport, error) { + var ( + containers []*libpod.Container + pod *libpod.Pod + podOptions []libpod.PodCreateOption + registryCreds *types.DockerAuthConfig + writer io.Writer + report entities.PlayKubeReport + ) + // check for name collision between pod and container - podName := podYAML.ObjectMeta.Name if podName == "" { return nil, errors.Errorf("pod does not have a name") } @@ -239,7 +294,7 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options en if err != nil { return nil, err } - conf, err := kubeContainerToCreateConfig(ctx, container, ic.Libpod, newImage, namespaces, volumes, pod.ID(), podInfraID, seccompPaths) + conf, err := kubeContainerToCreateConfig(ctx, container, ic.Libpod, newImage, namespaces, volumes, pod.ID(), podName, podInfraID, seccompPaths) if err != nil { return nil, err } @@ -259,7 +314,7 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options en } } - report.Pod = pod.ID() + report.Pods = append(report.Pods, pod.ID()) for _, ctr := range containers { report.Containers = append(report.Containers, ctr.ID()) } @@ -351,7 +406,7 @@ func setupSecurityContext(securityConfig *createconfig.SecurityConfig, userConfi } // kubeContainerToCreateConfig takes a v1.Container and returns a createconfig describing a container -func kubeContainerToCreateConfig(ctx context.Context, containerYAML v1.Container, runtime *libpod.Runtime, newImage *image.Image, namespaces map[string]string, volumes map[string]string, podID, infraID string, seccompPaths *kubeSeccompPaths) (*createconfig.CreateConfig, error) { +func kubeContainerToCreateConfig(ctx context.Context, containerYAML v1.Container, runtime *libpod.Runtime, newImage *image.Image, namespaces map[string]string, volumes map[string]string, podID, podName, infraID string, seccompPaths *kubeSeccompPaths) (*createconfig.CreateConfig, error) { var ( containerConfig createconfig.CreateConfig pidConfig createconfig.PidConfig @@ -369,6 +424,11 @@ func kubeContainerToCreateConfig(ctx context.Context, containerYAML v1.Container containerConfig.Image = containerYAML.Image containerConfig.ImageID = newImage.ID() containerConfig.Name = containerYAML.Name + + if podName != "" { + containerConfig.Name = fmt.Sprintf("%s-%s", podName, containerYAML.Name) + } + containerConfig.Tty = containerYAML.TTY containerConfig.Pod = podID -- cgit v1.2.3-54-g00ecf From 478f296fb345ce9edc707aa4bcd588f8ffd55bb8 Mon Sep 17 00:00:00 2001 From: theunrealgeek Date: Sat, 23 May 2020 20:47:30 -0700 Subject: Modify PlayKubeReport to preserve pod->container mapping Signed-off-by: Aditya Kamath --- cmd/podman/play/kube.go | 32 +++++++++++++++++--------------- pkg/domain/entities/play.go | 13 +++++++++---- pkg/domain/infra/abi/play.go | 18 +++++++++++------- 3 files changed, 37 insertions(+), 26 deletions(-) (limited to 'pkg/domain/infra/abi') diff --git a/cmd/podman/play/kube.go b/cmd/podman/play/kube.go index d2cfe8d43..17f3b430d 100644 --- a/cmd/podman/play/kube.go +++ b/cmd/podman/play/kube.go @@ -92,8 +92,10 @@ func kube(cmd *cobra.Command, args []string) error { return err } - for _, l := range report.Logs { - fmt.Fprintf(os.Stderr, l) + for _, pod := range report.Pods { + for _, l := range pod.Logs { + fmt.Fprintf(os.Stderr, l) + } } switch len(report.Pods) { @@ -105,19 +107,19 @@ func kube(cmd *cobra.Command, args []string) error { fmt.Printf("Pods:\n") } for _, pod := range report.Pods { - fmt.Println(pod) - } - - switch len(report.Containers) { - case 0: - return nil - case 1: - fmt.Printf("Container:\n") - default: - fmt.Printf("Containers:\n") - } - for _, ctr := range report.Containers { - fmt.Println(ctr) + fmt.Println(pod.ID) + + switch len(pod.Containers) { + case 0: + return nil + case 1: + fmt.Printf("Container:\n") + default: + fmt.Printf("Containers:\n") + } + for _, ctr := range pod.Containers { + fmt.Println(ctr) + } } return nil diff --git a/pkg/domain/entities/play.go b/pkg/domain/entities/play.go index fff86daf9..58602d3f9 100644 --- a/pkg/domain/entities/play.go +++ b/pkg/domain/entities/play.go @@ -26,12 +26,17 @@ type PlayKubeOptions struct { SeccompProfileRoot string } -// PlayKubeReport contains the results of running play kube. -type PlayKubeReport struct { - // Pods - the IDs of the created pods. - Pods []string +// PlayKubePods represents a single pod and associated containers created by play kube +type PlayKubePod struct { + ID string // Containers - the IDs of the containers running in the created pod. Containers []string // Logs - non-fatal erros and log messages while processing. Logs []string } + +// PlayKubeReport contains the results of running play kube. +type PlayKubeReport struct { + // Pods - pods created by play kube. + Pods []PlayKubePod +} diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index 772cea5fd..ce18930b7 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -103,8 +103,6 @@ func (ic *ContainerEngine) playKubeDeployment(ctx context.Context, deploymentYAM return nil, errors.Wrapf(err, "Error encountered while bringing up pod %s", podName) } report.Pods = append(report.Pods, podReport.Pods...) - report.Containers = append(report.Containers, podReport.Containers...) - report.Logs = append(report.Logs, podReport.Logs...) } return &report, nil } @@ -116,6 +114,7 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY podOptions []libpod.PodCreateOption registryCreds *types.DockerAuthConfig writer io.Writer + playKubePod entities.PlayKubePod report entities.PlayKubeReport ) @@ -125,7 +124,7 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY } for _, n := range podYAML.Spec.Containers { if n.Name == podName { - report.Logs = append(report.Logs, + playKubePod.Logs = append(playKubePod.Logs, fmt.Sprintf("a container exists with the same name (%q) as the pod in your YAML file; changing pod name to %s_pod\n", podName, podName)) podName = fmt.Sprintf("%s_pod", podName) } @@ -314,11 +313,13 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY } } - report.Pods = append(report.Pods, pod.ID()) + playKubePod.ID = pod.ID() for _, ctr := range containers { - report.Containers = append(report.Containers, ctr.ID()) + playKubePod.Containers = append(playKubePod.Containers, ctr.ID()) } + report.Pods = append(report.Pods, playKubePod) + return &report, nil } @@ -425,9 +426,12 @@ func kubeContainerToCreateConfig(ctx context.Context, containerYAML v1.Container containerConfig.ImageID = newImage.ID() containerConfig.Name = containerYAML.Name - if podName != "" { - containerConfig.Name = fmt.Sprintf("%s-%s", podName, containerYAML.Name) + // podName should be non-empty for Deployment objects to be able to create + // multiple pods having containers with unique names + if podName == "" { + return nil, errors.Errorf("kubeContainerToCreateConfig got empty podName") } + containerConfig.Name = fmt.Sprintf("%s-%s", podName, containerYAML.Name) containerConfig.Tty = containerYAML.TTY -- cgit v1.2.3-54-g00ecf From 103c9225a991f771fc171260aade1125ef1ccf49 Mon Sep 17 00:00:00 2001 From: theunrealgeek Date: Mon, 25 May 2020 00:11:58 -0700 Subject: Fix existing tests Signed-off-by: Aditya Kamath --- pkg/domain/infra/abi/play.go | 6 ++++-- test/e2e/play_kube_test.go | 48 +++++++++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 20 deletions(-) (limited to 'pkg/domain/infra/abi') diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index ce18930b7..98b278271 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -424,7 +424,6 @@ func kubeContainerToCreateConfig(ctx context.Context, containerYAML v1.Container containerConfig.Image = containerYAML.Image containerConfig.ImageID = newImage.ID() - containerConfig.Name = containerYAML.Name // podName should be non-empty for Deployment objects to be able to create // multiple pods having containers with unique names @@ -446,7 +445,10 @@ func kubeContainerToCreateConfig(ctx context.Context, containerYAML v1.Container setupSecurityContext(&securityConfig, &userConfig, containerYAML) - securityConfig.SeccompProfilePath = seccompPaths.findForContainer(containerConfig.Name) + // Since we prefix the container name with pod name to work-around the uniqueness requirement, + // seccom stuff should reference the actual container name from the YAML + // but apply to the containers with the prefixed name + securityConfig.SeccompProfilePath = seccompPaths.findForContainer(containerYAML.Name) containerConfig.Command = []string{} if imageData != nil && imageData.Config != nil { diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index 9daf266b8..e51e56f9a 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -208,6 +208,10 @@ func withPullPolicy(policy string) ctrOption { } } +func getCtrNameInPod(pod *Pod) string { + return fmt.Sprintf("%s-%s", pod.Name, defaultCtrName) +} + var _ = Describe("Podman generate kube", func() { var ( tempdir string @@ -245,14 +249,15 @@ var _ = Describe("Podman generate kube", func() { }) It("podman play kube test correct command", func() { - err := generateKubeYaml(getPod(), kubeYaml) + pod := getPod() + err := generateKubeYaml(pod, kubeYaml) Expect(err).To(BeNil()) kube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) kube.WaitWithDefaultTimeout() Expect(kube.ExitCode()).To(Equal(0)) - inspect := podmanTest.Podman([]string{"inspect", defaultCtrName}) + inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod)}) inspect.WaitWithDefaultTimeout() Expect(inspect.ExitCode()).To(Equal(0)) Expect(inspect.OutputToString()).To(ContainSubstring(defaultCtrCmd[0])) @@ -268,26 +273,27 @@ var _ = Describe("Podman generate kube", func() { kube.WaitWithDefaultTimeout() Expect(kube.ExitCode()).To(Equal(0)) - logs := podmanTest.Podman([]string{"logs", defaultCtrName}) + logs := podmanTest.Podman([]string{"logs", getCtrNameInPod(p)}) logs.WaitWithDefaultTimeout() Expect(logs.ExitCode()).To(Equal(0)) Expect(logs.OutputToString()).To(ContainSubstring("hello")) - inspect := podmanTest.Podman([]string{"inspect", defaultCtrName, "--format", "'{{ .Config.Cmd }}'"}) + inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(p), "--format", "'{{ .Config.Cmd }}'"}) inspect.WaitWithDefaultTimeout() Expect(inspect.ExitCode()).To(Equal(0)) Expect(inspect.OutputToString()).To(ContainSubstring("hello")) }) It("podman play kube test hostname", func() { - err := generateKubeYaml(getPod(), kubeYaml) + pod := getPod() + err := generateKubeYaml(pod, kubeYaml) Expect(err).To(BeNil()) kube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) kube.WaitWithDefaultTimeout() Expect(kube.ExitCode()).To(Equal(0)) - inspect := podmanTest.Podman([]string{"inspect", defaultCtrName, "--format", "{{ .Config.Hostname }}"}) + inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod), "--format", "{{ .Config.Hostname }}"}) inspect.WaitWithDefaultTimeout() Expect(inspect.ExitCode()).To(Equal(0)) Expect(inspect.OutputToString()).To(Equal(defaultPodName)) @@ -295,6 +301,7 @@ var _ = Describe("Podman generate kube", func() { It("podman play kube test with customized hostname", func() { hostname := "myhostname" + pod := getPod(withHostname(hostname)) err := generateKubeYaml(getPod(withHostname(hostname)), kubeYaml) Expect(err).To(BeNil()) @@ -302,7 +309,7 @@ var _ = Describe("Podman generate kube", func() { kube.WaitWithDefaultTimeout() Expect(kube.ExitCode()).To(Equal(0)) - inspect := podmanTest.Podman([]string{"inspect", defaultCtrName, "--format", "{{ .Config.Hostname }}"}) + inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod), "--format", "{{ .Config.Hostname }}"}) inspect.WaitWithDefaultTimeout() Expect(inspect.ExitCode()).To(Equal(0)) Expect(inspect.OutputToString()).To(Equal(hostname)) @@ -312,14 +319,15 @@ var _ = Describe("Podman generate kube", func() { capAdd := "CAP_SYS_ADMIN" ctr := getCtr(withCapAdd([]string{capAdd}), withCmd([]string{"cat", "/proc/self/status"})) - err := generateKubeYaml(getPod(withCtr(ctr)), kubeYaml) + pod := getPod(withCtr(ctr)) + err := generateKubeYaml(pod, kubeYaml) Expect(err).To(BeNil()) kube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) kube.WaitWithDefaultTimeout() Expect(kube.ExitCode()).To(Equal(0)) - inspect := podmanTest.Podman([]string{"inspect", defaultCtrName}) + inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod)}) inspect.WaitWithDefaultTimeout() Expect(inspect.ExitCode()).To(Equal(0)) Expect(inspect.OutputToString()).To(ContainSubstring(capAdd)) @@ -329,14 +337,15 @@ var _ = Describe("Podman generate kube", func() { capDrop := "CAP_CHOWN" ctr := getCtr(withCapDrop([]string{capDrop})) - err := generateKubeYaml(getPod(withCtr(ctr)), kubeYaml) + pod := getPod(withCtr(ctr)) + err := generateKubeYaml(pod, kubeYaml) Expect(err).To(BeNil()) kube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) kube.WaitWithDefaultTimeout() Expect(kube.ExitCode()).To(Equal(0)) - inspect := podmanTest.Podman([]string{"inspect", defaultCtrName}) + inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod)}) inspect.WaitWithDefaultTimeout() Expect(inspect.ExitCode()).To(Equal(0)) Expect(inspect.OutputToString()).To(ContainSubstring(capDrop)) @@ -344,14 +353,15 @@ var _ = Describe("Podman generate kube", func() { It("podman play kube no security context", func() { // expect play kube to not fail if no security context is specified - err := generateKubeYaml(getPod(withCtr(getCtr(withSecurityContext(false)))), kubeYaml) + pod := getPod(withCtr(getCtr(withSecurityContext(false)))) + err := generateKubeYaml(pod, kubeYaml) Expect(err).To(BeNil()) kube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) kube.WaitWithDefaultTimeout() Expect(kube.ExitCode()).To(Equal(0)) - inspect := podmanTest.Podman([]string{"inspect", defaultCtrName}) + inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod)}) inspect.WaitWithDefaultTimeout() Expect(inspect.ExitCode()).To(Equal(0)) }) @@ -367,7 +377,8 @@ var _ = Describe("Podman generate kube", func() { ctrAnnotation := "container.seccomp.security.alpha.kubernetes.io/" + defaultCtrName ctr := getCtr(withCmd([]string{"pwd"})) - err = generateKubeYaml(getPod(withCtr(ctr), withAnnotation(ctrAnnotation, "localhost/"+filepath.Base(jsonFile))), kubeYaml) + pod := getPod(withCtr(ctr), withAnnotation(ctrAnnotation, "localhost/"+filepath.Base(jsonFile))) + err = generateKubeYaml(pod, kubeYaml) Expect(err).To(BeNil()) // CreateSeccompJson will put the profile into podmanTest.TempDir. Use --seccomp-profile-root to tell play kube where to look @@ -375,7 +386,7 @@ var _ = Describe("Podman generate kube", func() { kube.WaitWithDefaultTimeout() Expect(kube.ExitCode()).To(Equal(0)) - logs := podmanTest.Podman([]string{"logs", defaultCtrName}) + logs := podmanTest.Podman([]string{"logs", getCtrNameInPod(pod)}) logs.WaitWithDefaultTimeout() Expect(logs.ExitCode()).To(Equal(0)) Expect(logs.OutputToString()).To(ContainSubstring("Operation not permitted")) @@ -392,7 +403,8 @@ var _ = Describe("Podman generate kube", func() { ctr := getCtr(withCmd([]string{"pwd"})) - err = generateKubeYaml(getPod(withCtr(ctr), withAnnotation("seccomp.security.alpha.kubernetes.io/pod", "localhost/"+filepath.Base(jsonFile))), kubeYaml) + pod := getPod(withCtr(ctr), withAnnotation("seccomp.security.alpha.kubernetes.io/pod", "localhost/"+filepath.Base(jsonFile))) + err = generateKubeYaml(pod, kubeYaml) Expect(err).To(BeNil()) // CreateSeccompJson will put the profile into podmanTest.TempDir. Use --seccomp-profile-root to tell play kube where to look @@ -400,7 +412,7 @@ var _ = Describe("Podman generate kube", func() { kube.WaitWithDefaultTimeout() Expect(kube.ExitCode()).To(Equal(0)) - logs := podmanTest.Podman([]string{"logs", defaultCtrName}) + logs := podmanTest.Podman([]string{"logs", getCtrNameInPod(pod)}) logs.WaitWithDefaultTimeout() Expect(logs.ExitCode()).To(Equal(0)) Expect(logs.OutputToString()).To(ContainSubstring("Operation not permitted")) @@ -519,7 +531,7 @@ spec: kube.WaitWithDefaultTimeout() Expect(kube.ExitCode()).To(Equal(0)) - inspect := podmanTest.Podman([]string{"inspect", "demo_kube"}) + inspect := podmanTest.Podman([]string{"inspect", "demo_pod-demo_kube"}) inspect.WaitWithDefaultTimeout() Expect(inspect.ExitCode()).To(Equal(0)) -- cgit v1.2.3-54-g00ecf From a338cd4e8a4c72dbdc1d460f3c19aff7871ee2c4 Mon Sep 17 00:00:00 2001 From: theunrealgeek Date: Wed, 3 Jun 2020 12:57:08 -0700 Subject: Update comment related to seccomp profiles in play kube Signed-off-by: Aditya Kamath --- pkg/domain/infra/abi/play.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pkg/domain/infra/abi') diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index 98b278271..e06251ca7 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -446,7 +446,7 @@ func kubeContainerToCreateConfig(ctx context.Context, containerYAML v1.Container setupSecurityContext(&securityConfig, &userConfig, containerYAML) // Since we prefix the container name with pod name to work-around the uniqueness requirement, - // seccom stuff should reference the actual container name from the YAML + // the seccom profile should reference the actual container name from the YAML // but apply to the containers with the prefixed name securityConfig.SeccompProfilePath = seccompPaths.findForContainer(containerYAML.Name) -- cgit v1.2.3-54-g00ecf