blob: 6b9908132c6a6789d874a61124f41768f718024f [file] [log] [blame]
Lorenz Brunc88c82d2020-05-08 14:35:04 +02001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17package containerd
18
19import (
20 "context"
Lorenz Brun878f5f92020-05-12 16:15:39 +020021 "fmt"
Lorenz Brun6acfc322020-05-13 17:01:26 +020022 "io"
Lorenz Brun8b0431a2020-07-13 16:56:36 +020023 "io/ioutil"
Lorenz Brunc88c82d2020-05-08 14:35:04 +020024 "os"
25 "os/exec"
Lorenz Brun8b0431a2020-07-13 16:56:36 +020026 "path/filepath"
Serge Bazanskic7359672020-10-30 16:38:57 +010027 "strings"
Lorenz Brun6acfc322020-05-13 17:01:26 +020028 "time"
29
Lorenz Brun8b0431a2020-07-13 16:56:36 +020030 ctr "github.com/containerd/containerd"
31 "github.com/containerd/containerd/namespaces"
Lorenz Brun8b0431a2020-07-13 16:56:36 +020032
Serge Bazanski31370b02021-01-07 16:31:14 +010033 "source.monogon.dev/metropolis/node/core/localstorage"
34 "source.monogon.dev/metropolis/pkg/supervisor"
Lorenz Brunc88c82d2020-05-08 14:35:04 +020035)
36
Lorenz Brun8b0431a2020-07-13 16:56:36 +020037const (
38 preseedNamespacesDir = "/containerd/preseed/"
39)
40
Lorenz Brun878f5f92020-05-12 16:15:39 +020041type Service struct {
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020042 EphemeralVolume *localstorage.EphemeralContainerdDirectory
Lorenz Brun878f5f92020-05-12 16:15:39 +020043}
Lorenz Brunc88c82d2020-05-08 14:35:04 +020044
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020045func (s *Service) Run(ctx context.Context) error {
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020046 cmd := exec.CommandContext(ctx, "/containerd/bin/containerd", "--config", "/containerd/conf/config.toml")
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020047 cmd.Env = []string{"PATH=/containerd/bin", "TMPDIR=" + s.EphemeralVolume.Tmp.FullPath()}
Lorenz Brun878f5f92020-05-12 16:15:39 +020048
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020049 runscFifo, err := os.OpenFile(s.EphemeralVolume.RunSCLogsFIFO.FullPath(), os.O_CREATE|os.O_RDONLY, os.ModeNamedPipe|0777)
50 if err != nil {
Lorenz Brun878f5f92020-05-12 16:15:39 +020051 return err
Lorenz Brunc88c82d2020-05-08 14:35:04 +020052 }
Serge Bazanski967be212020-11-02 11:26:59 +010053
54 if err := supervisor.Run(ctx, "runsc", s.logPump(runscFifo)); err != nil {
55 return fmt.Errorf("failed to start runsc log pump: %w", err)
56 }
57
58 if err := supervisor.Run(ctx, "preseed", s.runPreseed); err != nil {
59 return fmt.Errorf("failed to start preseed runnable: %w", err)
60 }
61 return supervisor.RunCommand(ctx, cmd)
62}
63
64// logPump returns a runnable that pipes data from a file/FIFO into its raw logger.
65// TODO(q3k): refactor this out to a generic function in supervisor or logtree.
66func (s *Service) logPump(fifo *os.File) supervisor.Runnable {
67 return func(ctx context.Context) error {
68 supervisor.Signal(ctx, supervisor.SignalHealthy)
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020069 for {
Serge Bazanski967be212020-11-02 11:26:59 +010070 // Quit if requested.
71 select {
72 case <-ctx.Done():
73 return ctx.Err()
74 default:
75 }
76
77 n, err := io.Copy(supervisor.RawLogger(ctx), fifo)
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020078 if n == 0 && err == nil {
79 // Hack because pipes/FIFOs can return zero reads when nobody is writing. To avoid busy-looping,
80 // sleep a bit before retrying. This does not loose data since the FIFO internal buffer will
81 // stall writes when it becomes full. 10ms maximum stall in a non-latency critical process (reading
82 // debug logs) is not an issue for us.
83 time.Sleep(10 * time.Millisecond)
84 } else if err != nil {
Serge Bazanski967be212020-11-02 11:26:59 +010085 return fmt.Errorf("log pump failed: %v", err)
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020086 }
87 }
Lorenz Brun8b0431a2020-07-13 16:56:36 +020088 }
Lorenz Brunc88c82d2020-05-08 14:35:04 +020089}
Lorenz Brun8b0431a2020-07-13 16:56:36 +020090
91// runPreseed loads OCI bundles in tar form from preseedNamespacesDir into containerd at startup.
92// This can be run multiple times, containerd will automatically dedup the layers.
93// containerd uses namespaces to keep images (and everything else) separate so to define where the images will be loaded
94// to they need to be in a folder named after the namespace they should be loaded into.
95// containerd's CRI plugin (which is built as part of containerd) uses a hardcoded namespace ("k8s.io") for everything
96// accessed through CRI, so if an image should be available on K8s it needs to be in that namespace.
97// As an example if image helloworld should be loaded for use with Kubernetes, the OCI bundle needs to be at
98// <preseedNamespacesDir>/k8s.io/helloworld.tar. No tagging beyond what's in the bundle is performed.
99func (s *Service) runPreseed(ctx context.Context) error {
100 client, err := ctr.New(s.EphemeralVolume.ClientSocket.FullPath())
101 if err != nil {
102 return fmt.Errorf("failed to connect to containerd: %w", err)
103 }
104 logger := supervisor.Logger(ctx)
105 preseedNamespaceDirs, err := ioutil.ReadDir(preseedNamespacesDir)
106 if err != nil {
107 return fmt.Errorf("failed to open preseed dir: %w", err)
108 }
109 for _, dir := range preseedNamespaceDirs {
110 if !dir.IsDir() {
Serge Bazanskic7359672020-10-30 16:38:57 +0100111 logger.Warningf("Non-Directory %q found in preseed folder, ignoring", dir.Name())
Lorenz Brun8b0431a2020-07-13 16:56:36 +0200112 continue
113 }
114 namespace := dir.Name()
115 images, err := ioutil.ReadDir(filepath.Join(preseedNamespacesDir, namespace))
116 if err != nil {
117 return fmt.Errorf("failed to list namespace preseed directory for ns \"%v\": %w", namespace, err)
118 }
119 ctxWithNS := namespaces.WithNamespace(ctx, namespace)
120 for _, image := range images {
121 if image.IsDir() {
Serge Bazanskic7359672020-10-30 16:38:57 +0100122 logger.Warningf("Directory %q found in preseed namespaced folder, ignoring", image.Name())
Lorenz Brun8b0431a2020-07-13 16:56:36 +0200123 continue
124 }
125 imageFile, err := os.Open(filepath.Join(preseedNamespacesDir, namespace, image.Name()))
126 if err != nil {
127 return fmt.Errorf("failed to open preseed image \"%v\": %w", image.Name(), err)
128 }
129 // defer in this loop is fine since we're never going to preseed more than ~1M images which is where our
130 // file descriptor limit is.
131 defer imageFile.Close()
132 importedImages, err := client.Import(ctxWithNS, imageFile)
133 if err != nil {
134 return fmt.Errorf("failed to import preseed image: %w", err)
135 }
136 var importedImageNames []string
137 for _, img := range importedImages {
138 importedImageNames = append(importedImageNames, img.Name)
139 }
Serge Bazanskic7359672020-10-30 16:38:57 +0100140 logger.Infof("Successfully imported preseeded bundle %s/%s into containerd", namespace, strings.Join(importedImageNames, ","))
Lorenz Brun8b0431a2020-07-13 16:56:36 +0200141 }
142 }
143 supervisor.Signal(ctx, supervisor.SignalHealthy)
144 supervisor.Signal(ctx, supervisor.SignalDone)
145 return nil
146}