blob: b5e2cf0f4da404275a702af835fc581a137f995a [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 Brunc88c82d2020-05-08 14:35:04 +020023 "os"
24 "os/exec"
Lorenz Brun8b0431a2020-07-13 16:56:36 +020025 "path/filepath"
Serge Bazanskic7359672020-10-30 16:38:57 +010026 "strings"
Lorenz Brun6acfc322020-05-13 17:01:26 +020027 "time"
28
Lorenz Brun8b0431a2020-07-13 16:56:36 +020029 ctr "github.com/containerd/containerd"
30 "github.com/containerd/containerd/namespaces"
Lorenz Brun8b0431a2020-07-13 16:56:36 +020031
Serge Bazanski31370b02021-01-07 16:31:14 +010032 "source.monogon.dev/metropolis/node/core/localstorage"
33 "source.monogon.dev/metropolis/pkg/supervisor"
Lorenz Brunc88c82d2020-05-08 14:35:04 +020034)
35
Lorenz Brun8b0431a2020-07-13 16:56:36 +020036const (
37 preseedNamespacesDir = "/containerd/preseed/"
38)
39
Lorenz Brun878f5f92020-05-12 16:15:39 +020040type Service struct {
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020041 EphemeralVolume *localstorage.EphemeralContainerdDirectory
Lorenz Brun878f5f92020-05-12 16:15:39 +020042}
Lorenz Brunc88c82d2020-05-08 14:35:04 +020043
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020044func (s *Service) Run(ctx context.Context) error {
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020045 cmd := exec.CommandContext(ctx, "/containerd/bin/containerd", "--config", "/containerd/conf/config.toml")
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020046 cmd.Env = []string{"PATH=/containerd/bin", "TMPDIR=" + s.EphemeralVolume.Tmp.FullPath()}
Lorenz Brun878f5f92020-05-12 16:15:39 +020047
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020048 runscFifo, err := os.OpenFile(s.EphemeralVolume.RunSCLogsFIFO.FullPath(), os.O_CREATE|os.O_RDONLY, os.ModeNamedPipe|0777)
49 if err != nil {
Lorenz Brun878f5f92020-05-12 16:15:39 +020050 return err
Lorenz Brunc88c82d2020-05-08 14:35:04 +020051 }
Serge Bazanski967be212020-11-02 11:26:59 +010052
53 if err := supervisor.Run(ctx, "runsc", s.logPump(runscFifo)); err != nil {
54 return fmt.Errorf("failed to start runsc log pump: %w", err)
55 }
56
57 if err := supervisor.Run(ctx, "preseed", s.runPreseed); err != nil {
58 return fmt.Errorf("failed to start preseed runnable: %w", err)
59 }
60 return supervisor.RunCommand(ctx, cmd)
61}
62
63// logPump returns a runnable that pipes data from a file/FIFO into its raw logger.
64// TODO(q3k): refactor this out to a generic function in supervisor or logtree.
65func (s *Service) logPump(fifo *os.File) supervisor.Runnable {
66 return func(ctx context.Context) error {
67 supervisor.Signal(ctx, supervisor.SignalHealthy)
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020068 for {
Serge Bazanski967be212020-11-02 11:26:59 +010069 // Quit if requested.
70 select {
71 case <-ctx.Done():
72 return ctx.Err()
73 default:
74 }
75
76 n, err := io.Copy(supervisor.RawLogger(ctx), fifo)
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020077 if n == 0 && err == nil {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020078 // Hack because pipes/FIFOs can return zero reads when nobody
79 // is writing. To avoid busy-looping, sleep a bit before
80 // retrying. This does not loose data since the FIFO internal
81 // buffer will stall writes when it becomes full. 10ms maximum
82 // stall in a non-latency critical process (reading debug logs)
83 // is not an issue for us.
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020084 time.Sleep(10 * time.Millisecond)
85 } else if err != nil {
Serge Bazanski967be212020-11-02 11:26:59 +010086 return fmt.Errorf("log pump failed: %v", err)
Serge Bazanskic2c7ad92020-07-13 17:20:09 +020087 }
88 }
Lorenz Brun8b0431a2020-07-13 16:56:36 +020089 }
Lorenz Brunc88c82d2020-05-08 14:35:04 +020090}
Lorenz Brun8b0431a2020-07-13 16:56:36 +020091
Serge Bazanski216fe7b2021-05-21 18:36:16 +020092// runPreseed loads OCI bundles in tar form from preseedNamespacesDir into
93// containerd at startup.
94// This can be run multiple times, containerd will automatically dedup the
95// layers. containerd uses namespaces to keep images (and everything else)
96// separate so to define where the images will be loaded to they need to be in
97// a folder named after the namespace they should be loaded into. containerd's
98// CRI plugin (which is built as part of containerd) uses a hardcoded namespace
99// ("k8s.io") for everything accessed through CRI, so if an image should be
100// available on K8s it needs to be in that namespace.
101// As an example if image helloworld should be loaded for use with Kubernetes,
102// the OCI bundle needs to be at <preseedNamespacesDir>/k8s.io/helloworld.tar.
103// No tagging beyond what's in the bundle is performed.
Lorenz Brun8b0431a2020-07-13 16:56:36 +0200104func (s *Service) runPreseed(ctx context.Context) error {
105 client, err := ctr.New(s.EphemeralVolume.ClientSocket.FullPath())
106 if err != nil {
107 return fmt.Errorf("failed to connect to containerd: %w", err)
108 }
109 logger := supervisor.Logger(ctx)
Lorenz Brun764a2de2021-11-22 16:26:36 +0100110 preseedNamespaceDirs, err := os.ReadDir(preseedNamespacesDir)
Lorenz Brun8b0431a2020-07-13 16:56:36 +0200111 if err != nil {
112 return fmt.Errorf("failed to open preseed dir: %w", err)
113 }
114 for _, dir := range preseedNamespaceDirs {
115 if !dir.IsDir() {
Serge Bazanskic7359672020-10-30 16:38:57 +0100116 logger.Warningf("Non-Directory %q found in preseed folder, ignoring", dir.Name())
Lorenz Brun8b0431a2020-07-13 16:56:36 +0200117 continue
118 }
119 namespace := dir.Name()
Lorenz Brun764a2de2021-11-22 16:26:36 +0100120 images, err := os.ReadDir(filepath.Join(preseedNamespacesDir, namespace))
Lorenz Brun8b0431a2020-07-13 16:56:36 +0200121 if err != nil {
122 return fmt.Errorf("failed to list namespace preseed directory for ns \"%v\": %w", namespace, err)
123 }
124 ctxWithNS := namespaces.WithNamespace(ctx, namespace)
125 for _, image := range images {
126 if image.IsDir() {
Serge Bazanskic7359672020-10-30 16:38:57 +0100127 logger.Warningf("Directory %q found in preseed namespaced folder, ignoring", image.Name())
Lorenz Brun8b0431a2020-07-13 16:56:36 +0200128 continue
129 }
130 imageFile, err := os.Open(filepath.Join(preseedNamespacesDir, namespace, image.Name()))
131 if err != nil {
132 return fmt.Errorf("failed to open preseed image \"%v\": %w", image.Name(), err)
133 }
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200134 // defer in this loop is fine since we're never going to preseed
135 // more than ~1M images which is where our file descriptor limit
136 // is.
Lorenz Brun8b0431a2020-07-13 16:56:36 +0200137 defer imageFile.Close()
138 importedImages, err := client.Import(ctxWithNS, imageFile)
139 if err != nil {
140 return fmt.Errorf("failed to import preseed image: %w", err)
141 }
142 var importedImageNames []string
143 for _, img := range importedImages {
144 importedImageNames = append(importedImageNames, img.Name)
145 }
Serge Bazanskic7359672020-10-30 16:38:57 +0100146 logger.Infof("Successfully imported preseeded bundle %s/%s into containerd", namespace, strings.Join(importedImageNames, ","))
Lorenz Brun8b0431a2020-07-13 16:56:36 +0200147 }
148 }
149 supervisor.Signal(ctx, supervisor.SignalHealthy)
150 supervisor.Signal(ctx, supervisor.SignalDone)
151 return nil
152}