| package containers |
| |
| import ( |
| "context" |
| "fmt" |
| "log" |
| "os" |
| "syscall" |
| |
| containerd "github.com/containerd/containerd/v2/client" |
| "github.com/containerd/containerd/v2/pkg/cio" |
| "github.com/containerd/containerd/v2/pkg/namespaces" |
| "github.com/containerd/containerd/v2/pkg/oci" |
| "github.com/opencontainers/runtime-spec/specs-go" |
| ) |
| |
| // GetOrDownloadImage gets an image from the containerd client if the image is |
| // already available locally, and downloads and unpacks it if it is not. |
| func GetOrDownloadImage(ctx context.Context, client *containerd.Client, imageName string) (containerd.Image, error) { |
| image, err := client.GetImage(ctx, imageName) |
| if err != nil { |
| log.Printf("error getting local image: %v\n", err) |
| } |
| if err != nil || image == nil { |
| image, err = client.Pull(ctx, imageName, containerd.WithPullUnpack) |
| if err != nil { |
| return nil, fmt.Errorf("error pulling image: %v", err) |
| } else { |
| log.Println("finished pulling image") |
| } |
| } |
| |
| return image, nil |
| } |
| |
| // A ContainerConfig is a simplified set of configurations which can be used to |
| // build an OCI container. |
| type ContainerConfig struct { |
| // Name is the name that will be given to the container. |
| Name string |
| // Command is the command that will serve as the container entrypoint. If empty, |
| // the default command for the container will be used. |
| Command string |
| // Args is the set of arguments which will be passed to the container command. |
| Args []string |
| // Cwd is the working directory where the command will be run. |
| Cwd string |
| // Env is a map from variable names to values which will be passed to the container |
| // as environment variables. |
| Env map[string]string |
| // DirMounts is the set of directories which will be mapped from the host to the container. |
| // The directories will be created automatically if they don't exist. |
| DirMounts []specs.Mount |
| // FileMounts is the set of files that will be mapped from the host to the container. |
| // It is an error if the expected file doesn't exist in the host. |
| FileMounts []specs.Mount |
| // Priviliged determines whether or not the container will be run in privileged mode. |
| Privileged bool |
| // NetHost determines whether or not the container will use the host's networking. |
| NetHost bool |
| // Opts is any additional options which will be used to create the container. |
| Opts []oci.SpecOpts |
| } |
| |
| // Run downloads an image if it's not available locally, then runs the container |
| // command specified by config and waits for its completion. |
| // Returns the exit status of the container, or 1 on other failure. |
| func Run(ctx context.Context, imageName string, config ContainerConfig, containerNamespace, containerdSocket string) (uint32, error) { |
| client, err := containerd.New(containerdSocket) |
| if err != nil { |
| log.Fatalf("error connecting to containerd socket: %v\n", err) |
| } |
| defer client.Close() |
| |
| ctx = namespaces.WithNamespace(ctx, containerNamespace) |
| |
| image, err := GetOrDownloadImage(ctx, client, imageName) |
| if err != nil { |
| return 1, err |
| } |
| |
| container, err := NewContainer(ctx, client, image, config) |
| if err != nil { |
| return 1, err |
| } |
| defer func() { |
| if err := DeleteContainer(ctx, client, container); err != nil { |
| log.Printf("could not delete container: %v\n", err) |
| } |
| }() |
| |
| return RunContainerTask(ctx, client, container) |
| } |
| |
| // NewContainer builds and returns a new container from the provided image and config. |
| func NewContainer( |
| ctx context.Context, |
| client *containerd.Client, |
| image containerd.Image, |
| config ContainerConfig, |
| ) (containerd.Container, error) { |
| processArgs := append([]string{config.Command}, config.Args...) |
| |
| var envVarsArray []string |
| for name, value := range config.Env { |
| envVarsArray = append(envVarsArray, fmt.Sprintf("%s=%s", name, value)) |
| } |
| |
| for _, mount := range config.DirMounts { |
| if err := os.MkdirAll(mount.Source, 0777); err != nil { |
| return nil, fmt.Errorf("could not find or create mount directory %s: %v", mount.Source, err) |
| } |
| } |
| |
| // The order matters here! The image config has to come first. |
| opts := []oci.SpecOpts{ |
| oci.WithImageConfig(image), |
| oci.WithEnv(envVarsArray), |
| oci.WithProcessArgs(processArgs...), |
| oci.WithProcessCwd("/"), |
| oci.WithMounts(config.DirMounts), |
| oci.WithMounts(config.FileMounts), |
| } |
| |
| if config.Privileged { |
| opts = append(opts, oci.WithPrivileged) |
| } |
| |
| if config.NetHost { |
| hostname, err := os.Hostname() |
| if err != nil { |
| return nil, fmt.Errorf("could not get hostname: %v\n", err) |
| } |
| |
| hostNetworkOpts := []oci.SpecOpts{ |
| oci.WithHostNamespace(specs.NetworkNamespace), |
| oci.WithHostHostsFile, |
| oci.WithHostResolvconf, |
| oci.WithEnv([]string{fmt.Sprintf("HOSTNAME=%s", hostname)}), |
| } |
| opts = append(opts, hostNetworkOpts...) |
| } |
| |
| opts = append(opts, config.Opts...) |
| |
| spec := containerd.WithNewSpec(opts...) |
| |
| container, err := client.NewContainer( |
| ctx, |
| config.Name, |
| containerd.WithNewSnapshot(fmt.Sprintf("%s-rootfs", config.Name), image), |
| spec, |
| ) |
| if err != nil { |
| return nil, fmt.Errorf("error creating container: %v", err) |
| } |
| |
| return container, nil |
| } |
| |
| // RunContainerTask runs a container's command and waits for its completion. |
| // Returns the exit status of the container, or 1 on other failure. |
| func RunContainerTask(ctx context.Context, client *containerd.Client, container containerd.Container) (uint32, error) { |
| cioCreator := cio.NewCreator(cio.WithStdio) |
| task, err := container.NewTask(ctx, cioCreator) |
| if err != nil { |
| return 1, fmt.Errorf("error creating task: %v", err) |
| } |
| defer func() { |
| if err := DeleteTask(ctx, task); err != nil { |
| log.Printf("could not delete container task: %v\n", err) |
| } |
| }() |
| |
| exitStatusC, err := task.Wait(ctx) |
| if err != nil { |
| return 1, fmt.Errorf("error waiting for task completion: %v", err) |
| } |
| |
| err = task.Start(ctx) |
| if err != nil { |
| return 1, fmt.Errorf("error starting task: %v", err) |
| } |
| |
| status := <-exitStatusC |
| code, _, err := status.Result() |
| if err != nil { |
| return code, err |
| } |
| |
| return code, nil |
| } |
| |
| // DeleteContainer deletes a container and its associated snapshot. |
| func DeleteContainer(ctx context.Context, client *containerd.Client, container containerd.Container) error { |
| if err := container.Delete(ctx, containerd.WithSnapshotCleanup); err != nil { |
| return fmt.Errorf("failed to delete container: %v", err) |
| } |
| |
| return nil |
| } |
| |
| // DeleteTask deletes a task. |
| // If the task is not already stopped, this will kill it by calling KillTask. |
| func DeleteTask(ctx context.Context, task containerd.Task) error { |
| taskStatus, err := task.Status(ctx) |
| if err != nil { |
| log.Printf("failed to get task status: %v\n", err) |
| } |
| if taskStatus.Status != containerd.Stopped { |
| if err := KillTask(ctx, task); err != nil { |
| return fmt.Errorf("failed to kill task: %v", err) |
| } |
| } |
| |
| if _, err = task.Delete(ctx); err != nil { |
| log.Printf("error deleting task: %v\n", err) |
| } |
| |
| return nil |
| } |
| |
| // KillTask attempts to kill a task by sending SIGTERM. |
| // If the SIGTERM does not kill the task, a SIGKILL will be sent as well. |
| func KillTask(ctx context.Context, task containerd.Task) error { |
| if err := task.Kill(ctx, syscall.SIGTERM); err != nil { |
| return fmt.Errorf("could not kill task: %v", err) |
| } |
| |
| taskStatus, err := task.Status(ctx) |
| if err != nil || taskStatus.Status != containerd.Stopped { |
| log.Printf("task could not be killed with SIGTERM, sending SIGKILL: %v\n", err) |
| if err := task.Kill(ctx, syscall.SIGKILL); err != nil { |
| return fmt.Errorf("could not kill task: %v", err) |
| } |
| } |
| |
| return nil |
| } |