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  }