github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/worker/runtime/backend.go (about) 1 // Package backend provides the implementation of a Garden server backed by 2 // containerd. 3 // 4 // See https://containerd.io/, and https://github.com/cloudfoundry/garden. 5 // 6 package runtime 7 8 import ( 9 "context" 10 "fmt" 11 "time" 12 13 "code.cloudfoundry.org/garden" 14 "github.com/pf-qiu/concourse/v6/worker/runtime/libcontainerd" 15 bespec "github.com/pf-qiu/concourse/v6/worker/runtime/spec" 16 "github.com/containerd/containerd" 17 "github.com/containerd/containerd/cio" 18 "github.com/containerd/containerd/errdefs" 19 ) 20 21 var _ garden.Backend = (*GardenBackend)(nil) 22 23 // GardenBackend implements a Garden backend backed by `containerd`. 24 // 25 type GardenBackend struct { 26 client libcontainerd.Client 27 killer Killer 28 network Network 29 rootfsManager RootfsManager 30 userNamespace UserNamespace 31 initBinPath string 32 33 maxContainers int 34 requestTimeout time.Duration 35 createLock TimeoutWithByPassLock 36 } 37 38 //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . UserNamespace 39 40 type UserNamespace interface { 41 MaxValidIds() (uid, gid uint32, err error) 42 } 43 44 func WithUserNamespace(s UserNamespace) GardenBackendOpt { 45 return func(b *GardenBackend) { 46 b.userNamespace = s 47 } 48 } 49 50 // GardenBackendOpt defines a functional option that when applied, modifies the 51 // configuration of a GardenBackend. 52 // 53 type GardenBackendOpt func(b *GardenBackend) 54 55 // WithRootfsManager configures the RootfsManager used by the backend. 56 // 57 func WithRootfsManager(r RootfsManager) GardenBackendOpt { 58 return func(b *GardenBackend) { 59 b.rootfsManager = r 60 } 61 } 62 63 // WithKiller configures the killer used to terminate tasks. 64 // 65 func WithKiller(k Killer) GardenBackendOpt { 66 return func(b *GardenBackend) { 67 b.killer = k 68 } 69 } 70 71 // WithNetwork configures the network used by the backend. 72 // 73 func WithNetwork(n Network) GardenBackendOpt { 74 return func(b *GardenBackend) { 75 b.network = n 76 } 77 } 78 79 // WithMaxContainers configures the max number of containers that can be created 80 // 81 func WithMaxContainers(limit int) GardenBackendOpt { 82 return func(b *GardenBackend) { 83 b.maxContainers = limit 84 } 85 } 86 87 // WithRequestTimeout configures the request timeout 88 // Currently only used as timeout for acquiring the create container lock 89 func WithRequestTimeout(requestTimeout time.Duration) GardenBackendOpt { 90 return func(b *GardenBackend) { 91 b.requestTimeout = requestTimeout 92 } 93 } 94 95 // WithInitBinPath configures the path to the init binary that is injected into every container. 96 // The init binary just sits there doing nothing until Concourse decides it's time to attach to the container 97 // and exec the actual command 98 func WithInitBinPath(initBinPath string) GardenBackendOpt { 99 return func(b *GardenBackend) { 100 b.initBinPath = initBinPath 101 } 102 } 103 104 // NewGardenBackend instantiates a GardenBackend with tweakable configurations passed as Config. 105 // 106 func NewGardenBackend(client libcontainerd.Client, opts ...GardenBackendOpt) (b GardenBackend, err error) { 107 if client == nil { 108 err = ErrInvalidInput("nil client") 109 return 110 } 111 112 b = GardenBackend{ 113 client: client, 114 } 115 for _, opt := range opts { 116 opt(&b) 117 } 118 119 var enableLock bool 120 if b.maxContainers != 0 { 121 enableLock = true 122 } 123 b.createLock = NewTimeoutLimitLock(b.requestTimeout, enableLock) 124 125 if b.network == nil { 126 b.network, err = NewCNINetwork() 127 if err != nil { 128 return b, fmt.Errorf("network init: %w", err) 129 } 130 } 131 132 if b.killer == nil { 133 b.killer = NewKiller() 134 } 135 136 if b.rootfsManager == nil { 137 b.rootfsManager = NewRootfsManager() 138 } 139 140 if b.userNamespace == nil { 141 b.userNamespace = NewUserNamespace() 142 } 143 144 // Because the garden server is created programmatically in the integration tests, add 145 // a sane default path 146 if b.initBinPath == "" { 147 b.initBinPath = bespec.DefaultInitBinPath 148 } 149 150 return b, nil 151 } 152 153 // Start initializes the client. 154 // 155 func (b *GardenBackend) Start() (err error) { 156 err = b.client.Init() 157 if err != nil { 158 return fmt.Errorf("client init: %w", err) 159 } 160 161 err = b.network.SetupRestrictedNetworks() 162 if err != nil { 163 return fmt.Errorf("setup restricted networks failed: %w", err) 164 } 165 166 return 167 } 168 169 // Stop closes the client's underlying connections and frees any resources 170 // associated with it. 171 // 172 func (b *GardenBackend) Stop() { 173 _ = b.client.Stop() 174 } 175 176 // Ping pings the garden server in order to check connectivity. 177 // 178 func (b *GardenBackend) Ping() (err error) { 179 err = b.client.Version(context.Background()) 180 if err != nil { 181 return fmt.Errorf("getting containerd version: %w", err) 182 } 183 184 return 185 } 186 187 // Create creates a new container. 188 // 189 func (b *GardenBackend) Create(gdnSpec garden.ContainerSpec) (garden.Container, error) { 190 ctx := context.Background() 191 192 cont, err := b.createContainer(ctx, gdnSpec) 193 if err != nil { 194 return nil, fmt.Errorf("new container: %w", err) 195 } 196 197 err = b.startTask(ctx, cont) 198 if err != nil { 199 return nil, fmt.Errorf("starting task: %w", err) 200 } 201 202 return NewContainer( 203 cont, 204 b.killer, 205 b.rootfsManager, 206 ), nil 207 } 208 209 func (b *GardenBackend) createContainer(ctx context.Context, gdnSpec garden.ContainerSpec) (containerd.Container, error) { 210 err := b.createLock.Acquire(ctx) 211 if err != nil { 212 return nil, fmt.Errorf("acquiring create container lock: %w", err) 213 214 } 215 defer b.createLock.Release() 216 217 err = b.checkContainerCapacity(ctx) 218 if err != nil { 219 return nil, fmt.Errorf("checking container capacity: %w", err) 220 } 221 222 maxUid, maxGid, err := b.userNamespace.MaxValidIds() 223 if err != nil { 224 return nil, fmt.Errorf("getting uid and gid maps: %w", err) 225 } 226 227 oci, err := bespec.OciSpec(b.initBinPath, gdnSpec, maxUid, maxGid) 228 if err != nil { 229 return nil, fmt.Errorf("garden spec to oci spec: %w", err) 230 } 231 232 netMounts, err := b.network.SetupMounts(gdnSpec.Handle) 233 if err != nil { 234 return nil, fmt.Errorf("network setup mounts: %w", err) 235 } 236 237 oci.Mounts = append(oci.Mounts, netMounts...) 238 239 return b.client.NewContainer(ctx, gdnSpec.Handle, gdnSpec.Properties, oci) 240 } 241 242 func (b *GardenBackend) startTask(ctx context.Context, cont containerd.Container) error { 243 task, err := cont.NewTask(ctx, cio.NullIO, containerd.WithNoNewKeyring) 244 if err != nil { 245 return fmt.Errorf("new task: %w", err) 246 } 247 248 err = b.network.Add(ctx, task) 249 if err != nil { 250 return fmt.Errorf("network add: %w", err) 251 } 252 253 return task.Start(ctx) 254 } 255 256 // Destroy gracefully destroys a container. 257 // 258 func (b *GardenBackend) Destroy(handle string) error { 259 if handle == "" { 260 return ErrInvalidInput("empty handle") 261 } 262 263 ctx := context.Background() 264 265 container, err := b.client.GetContainer(ctx, handle) 266 if err != nil { 267 return fmt.Errorf("get container: %w", err) 268 } 269 270 task, err := container.Task(ctx, cio.Load) 271 if err != nil { 272 if !errdefs.IsNotFound(err) { 273 return fmt.Errorf("task lookup: %w", err) 274 } 275 276 err = container.Delete(ctx) 277 if err != nil { 278 return fmt.Errorf("deleting container: %w", err) 279 } 280 281 return nil 282 } 283 284 err = b.killer.Kill(ctx, task, KillGracefully) 285 if err != nil { 286 return fmt.Errorf("gracefully killing task: %w", err) 287 } 288 289 err = b.network.Remove(ctx, task) 290 if err != nil { 291 return fmt.Errorf("network remove: %w", err) 292 } 293 294 _, err = task.Delete(ctx, containerd.WithProcessKill) 295 if err != nil { 296 return fmt.Errorf("task remove: %w", err) 297 } 298 299 err = container.Delete(ctx) 300 if err != nil { 301 return fmt.Errorf("deleting container: %w", err) 302 } 303 304 return nil 305 } 306 307 // Containers lists all containers filtered by properties (which are ANDed 308 // together). 309 // 310 func (b *GardenBackend) Containers(properties garden.Properties) (containers []garden.Container, err error) { 311 filters, err := propertiesToFilterList(properties) 312 if err != nil { 313 return 314 } 315 316 res, err := b.client.Containers(context.Background(), filters...) 317 if err != nil { 318 err = fmt.Errorf("list containers: %w", err) 319 return 320 } 321 322 containers = make([]garden.Container, len(res)) 323 for i, containerdContainer := range res { 324 containers[i] = NewContainer( 325 containerdContainer, 326 b.killer, 327 b.rootfsManager, 328 ) 329 } 330 331 return 332 } 333 334 // Lookup returns the container with the specified handle. 335 // 336 func (b *GardenBackend) Lookup(handle string) (garden.Container, error) { 337 if handle == "" { 338 return nil, ErrInvalidInput("empty handle") 339 } 340 341 containerdContainer, err := b.client.GetContainer(context.Background(), handle) 342 if err != nil { 343 return nil, fmt.Errorf("get container: %w", err) 344 } 345 346 return NewContainer( 347 containerdContainer, 348 b.killer, 349 b.rootfsManager, 350 ), nil 351 } 352 353 // GraceTime returns the value of the "garden.grace-time" property 354 // 355 func (b *GardenBackend) GraceTime(container garden.Container) (duration time.Duration) { 356 property, err := container.Property(GraceTimeKey) 357 if err != nil { 358 return 0 359 } 360 361 _, err = fmt.Sscanf(property, "%d", &duration) 362 if err != nil { 363 return 0 364 } 365 366 return duration 367 } 368 369 // Capacity - Not Implemented 370 // 371 func (b *GardenBackend) Capacity() (capacity garden.Capacity, err error) { 372 err = ErrNotImplemented 373 return 374 } 375 376 // BulkInfo - Not Implemented 377 // 378 func (b *GardenBackend) BulkInfo(handles []string) (info map[string]garden.ContainerInfoEntry, err error) { 379 err = ErrNotImplemented 380 return 381 } 382 383 // BulkMetrics - Not Implemented 384 // 385 func (b *GardenBackend) BulkMetrics(handles []string) (metrics map[string]garden.ContainerMetricsEntry, err error) { 386 err = ErrNotImplemented 387 return 388 } 389 390 // checkContainerCapacity ensures that Garden.MaxContainers is respected 391 // 392 func (b *GardenBackend) checkContainerCapacity(ctx context.Context) error { 393 if b.maxContainers == 0 { 394 return nil 395 } 396 397 containers, err := b.client.Containers(ctx) 398 if err != nil { 399 return fmt.Errorf("getting list of containers: %w", err) 400 } 401 402 if len(containers) >= b.maxContainers { 403 return fmt.Errorf("max containers reached") 404 } 405 return nil 406 }