blob: c3dd4a0fa1216f0abc8b2a4fdc4c461000d71ac7 [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 {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020079 // Hack because pipes/FIFOs can return zero reads when nobody
80 // is writing. To avoid busy-looping, sleep a bit before
81 // retrying. This does not loose data since the FIFO internal
82 // buffer will stall writes when it becomes full. 10ms maximum
83 // stall in a non-latency critical process (reading debug logs)
84 // is not an issue for us.
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020085 time.Sleep(10 * time.Millisecond)
86 } else if err != nil {
Serge Bazanski967be212020-11-02 11:26:59 +010087 return fmt.Errorf("log pump failed: %v", err)
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020088 }
89 }
Lorenz Brun8b0431a2020-07-13 16:56:36 +020090 }
Lorenz Brunc88c82d2020-05-08 14:35:04 +020091}
Lorenz Brun8b0431a2020-07-13 16:56:36 +020092
Serge Bazanski216fe7b2021-05-21 18:36:16 +020093// runPreseed loads OCI bundles in tar form from preseedNamespacesDir into
94// containerd at startup.
95// This can be run multiple times, containerd will automatically dedup the
96// layers. containerd uses namespaces to keep images (and everything else)
97// separate so to define where the images will be loaded to they need to be in
98// a folder named after the namespace they should be loaded into. containerd's
99// CRI plugin (which is built as part of containerd) uses a hardcoded namespace
100// ("k8s.io") for everything accessed through CRI, so if an image should be
101// available on K8s it needs to be in that namespace.
102// As an example if image helloworld should be loaded for use with Kubernetes,
103// the OCI bundle needs to be at <preseedNamespacesDir>/k8s.io/helloworld.tar.
104// No tagging beyond what's in the bundle is performed.
Lorenz Brun8b0431a2020-07-13 16:56:36 +0200105func (s *Service) runPreseed(ctx context.Context) error {
106 client, err := ctr.New(s.EphemeralVolume.ClientSocket.FullPath())
107 if err != nil {
108 return fmt.Errorf("failed to connect to containerd: %w", err)
109 }
110 logger := supervisor.Logger(ctx)
111 preseedNamespaceDirs, err := ioutil.ReadDir(preseedNamespacesDir)
112 if err != nil {
113 return fmt.Errorf("failed to open preseed dir: %w", err)
114 }
115 for _, dir := range preseedNamespaceDirs {
116 if !dir.IsDir() {
Serge Bazanskic7359672020-10-30 16:38:57 +0100117 logger.Warningf("Non-Directory %q found in preseed folder, ignoring", dir.Name())
Lorenz Brun8b0431a2020-07-13 16:56:36 +0200118 continue
119 }
120 namespace := dir.Name()
121 images, err := ioutil.ReadDir(filepath.Join(preseedNamespacesDir, namespace))
122 if err != nil {
123 return fmt.Errorf("failed to list namespace preseed directory for ns \"%v\": %w", namespace, err)
124 }
125 ctxWithNS := namespaces.WithNamespace(ctx, namespace)
126 for _, image := range images {
127 if image.IsDir() {
Serge Bazanskic7359672020-10-30 16:38:57 +0100128 logger.Warningf("Directory %q found in preseed namespaced folder, ignoring", image.Name())
Lorenz Brun8b0431a2020-07-13 16:56:36 +0200129 continue
130 }
131 imageFile, err := os.Open(filepath.Join(preseedNamespacesDir, namespace, image.Name()))
132 if err != nil {
133 return fmt.Errorf("failed to open preseed image \"%v\": %w", image.Name(), err)
134 }
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200135 // defer in this loop is fine since we're never going to preseed
136 // more than ~1M images which is where our file descriptor limit
137 // is.
Lorenz Brun8b0431a2020-07-13 16:56:36 +0200138 defer imageFile.Close()
139 importedImages, err := client.Import(ctxWithNS, imageFile)
140 if err != nil {
141 return fmt.Errorf("failed to import preseed image: %w", err)
142 }
143 var importedImageNames []string
144 for _, img := range importedImages {
145 importedImageNames = append(importedImageNames, img.Name)
146 }
Serge Bazanskic7359672020-10-30 16:38:57 +0100147 logger.Infof("Successfully imported preseeded bundle %s/%s into containerd", namespace, strings.Join(importedImageNames, ","))
Lorenz Brun8b0431a2020-07-13 16:56:36 +0200148 }
149 }
150 supervisor.Signal(ctx, supervisor.SignalHealthy)
151 supervisor.Signal(ctx, supervisor.SignalDone)
152 return nil
153}