diff --git a/config/config_docker.go b/config/config_docker.go index 4b447de45..258fa9f99 100644 --- a/config/config_docker.go +++ b/config/config_docker.go @@ -92,6 +92,10 @@ type DockerConfiguration struct { Type string `default:"local" json:"type" yaml:"type"` Config map[string]string `default:"{\"max-size\":\"5m\",\"max-file\":\"1\",\"compress\":\"false\",\"mode\":\"non-blocking\"}" json:"config" yaml:"config"` } `json:"log_config" yaml:"log_config"` + + // ImagePullPolicy controls when images are pulled before a container is created. + // Always: pull every time. IfNotPresent: pull only if missing locally. Never: require a local image. + ImagePullPolicy ImagePullPolicy `default:"Always" json:"image_pull_policy" yaml:"image_pull_policy"` } func (c DockerConfiguration) ContainerLogConfig() container.LogConfig { @@ -183,3 +187,12 @@ func (o Overhead) GetMultiplier(memoryLimit int64) float64 { return o.DefaultMultiplier } + +// ImagePullPolicy controls when wings should pull a container image +type ImagePullPolicy string + +const ( + ImagePullPolicyAlways ImagePullPolicy = "Always" + ImagePullPolicyIfNotPresent ImagePullPolicy = "IfNotPresent" + ImagePullPolicyNever ImagePullPolicy = "Never" +) diff --git a/environment/docker/container.go b/environment/docker/container.go index 46b6744cc..fd8f31223 100644 --- a/environment/docker/container.go +++ b/environment/docker/container.go @@ -337,29 +337,60 @@ func (e *Environment) Readlog(lines int) ([]string, error) { return out, nil } -// Pulls the image from Docker. If there is an error while pulling the image -// from the source but the image already exists locally, we will report that -// error to the logger but continue with the process. +// Pulls the image from Docker when docker.image_pull_policy requires it. If +// there is an error while pulling the image from the source but the image +// already exists locally, we will report that error to the logger but continue +// with the process. // // The reasoning behind this is that Quay has had some serious outages as of // late, and we don't need to block all the servers from booting just because // of that. I'd imagine in a lot of cases an outage shouldn't affect users too // badly. It'll at least keep existing servers working correctly if anything. func (e *Environment) ensureImageExists(img string) error { - e.Events().Publish(environment.DockerImagePullStarted, "") - defer e.Events().Publish(environment.DockerImagePullCompleted, "") - // Images prefixed with a ~ are local images that we do not need to try and pull. if strings.HasPrefix(img, "~") { return nil } + policy := config.Get().Docker.ImagePullPolicy + if policy == "" { + policy = config.ImagePullPolicyAlways + } + // Give it up to 15 minutes to pull the image. I think this should cover 99.8% of cases where an // image pull might fail. I can't imagine it will ever take more than 15 minutes to fully pull // an image. Let me know when I am inevitably wrong here... ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) defer cancel() + switch policy { + case config.ImagePullPolicyNever: + // check if the image exists and if not return an error + exists, err := ImageExistsLocally(ctx, e.client, img) + if err != nil { + return err + } + if !exists { + // The image doesn't exist locally so return an error + return errors.Errorf("environment/docker: image %q is not present locally (docker.image_pull_policy is Never)", img) + } + return nil + case config.ImagePullPolicyIfNotPresent: + // check if the image exists and if not pull it + exists, err := ImageExistsLocally(ctx, e.client, img) + if err != nil { + return err + } + if exists { + // The image is already pulled so return + return nil + } + // the image doesn't exist yet so proceed to pull it + } + + e.Events().Publish(environment.DockerImagePullStarted, "") + defer e.Events().Publish(environment.DockerImagePullCompleted, "") + // Get a registry auth configuration from the config. var registryAuth *config.RegistryConfiguration for registry, c := range config.Get().Docker.Registries { @@ -438,6 +469,22 @@ func (e *Environment) ensureImageExists(img string) error { return nil } +// ImageExistsLocally checks if the provided image tag already exists locally +func ImageExistsLocally(ctx context.Context, client *client.Client, img string) (bool, error) { + images, err := client.ImageList(ctx, image.ListOptions{}) + if err != nil { + return false, errors.Wrap(err, "environment/docker: failed to list images") + } + for _, img2 := range images { + for _, t := range img2.RepoTags { + if t == img { + return true, nil + } + } + } + return false, nil +} + func (e *Environment) convertMounts() []mount.Mount { mounts := e.Configuration.Mounts() out := make([]mount.Mount, len(mounts)) diff --git a/server/install.go b/server/install.go index 8c29f1c7a..e084c630f 100644 --- a/server/install.go +++ b/server/install.go @@ -17,6 +17,7 @@ import ( "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/client" + "github.com/pterodactyl/wings/environment/docker" "github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/environment" @@ -232,12 +233,55 @@ func (ip *InstallationProcess) writeScriptToDisk() error { return nil } -// Pulls the docker image to be used for the installation container. +// Pulls the docker image to be used for the installation container when +// docker.image_pull_policy requires it. If there is an error while pulling from +// the source but the image already exists locally, we log a warning and continue. func (ip *InstallationProcess) pullInstallationImage() error { + img := ip.Script.ContainerImage + + // Images prefixed with a ~ are local images that we do not need to try and pull. + if strings.HasPrefix(img, "~") { + return nil + } + + policy := config.Get().Docker.ImagePullPolicy + if policy == "" { + policy = config.ImagePullPolicyAlways + } + + // Give it up to 15 minutes to pull the image + ctx, cancel := context.WithTimeout(ip.Server.Context(), 15*time.Minute) + defer cancel() + + switch policy { + case config.ImagePullPolicyNever: + // check if the image exists and if not return an error + exists, err := docker.ImageExistsLocally(ctx, ip.client, img) + if err != nil { + return err + } + if !exists { + // The image doesn't exist locally so return an error + return errors.Errorf("server/install: image %q is not present locally (docker.image_pull_policy is Never)", img) + } + return nil + case config.ImagePullPolicyIfNotPresent: + // check if the image exists and if not pull it + exists, err := docker.ImageExistsLocally(ctx, ip.client, img) + if err != nil { + return err + } + if exists { + // The image is already pulled so return + return nil + } + // the image doesn't exist yet so proceed to pull it + } + // Get a registry auth configuration from the config. var registryAuth *config.RegistryConfiguration for registry, c := range config.Get().Docker.Registries { - if !strings.HasPrefix(ip.Script.ContainerImage, registry) { + if !strings.HasPrefix(img, registry) { continue } @@ -258,23 +302,23 @@ func (ip *InstallationProcess) pullInstallationImage() error { imagePullOptions.RegistryAuth = b64 } - r, err := ip.client.ImagePull(ip.Server.Context(), ip.Script.ContainerImage, imagePullOptions) + r, err := ip.client.ImagePull(ctx, img, imagePullOptions) if err != nil { - images, ierr := ip.client.ImageList(ip.Server.Context(), image.ListOptions{}) + images, ierr := ip.client.ImageList(ctx, image.ListOptions{}) if ierr != nil { // Well damn, something has gone really wrong here, just go ahead and abort there // isn't much anything we can do to try and self-recover from this. return ierr } - for _, img := range images { - for _, t := range img.RepoTags { - if t != ip.Script.ContainerImage { + for _, img2 := range images { + for _, t := range img2.RepoTags { + if t != img { continue } log.WithFields(log.Fields{ - "image": ip.Script.ContainerImage, + "image": img, "err": err.Error(), }).Warn("unable to pull requested image from remote source, however the image exists locally") @@ -284,11 +328,11 @@ func (ip *InstallationProcess) pullInstallationImage() error { } } - return err + return errors.Wrapf(err, "failed to pull %q installation container image", img) } defer r.Close() - log.WithField("image", ip.Script.ContainerImage).Debug("pulling docker image... this could take a bit of time") + log.WithField("image", img).Debug("pulling docker image... this could take a bit of time") // Block continuation until the image has been pulled successfully. scanner := bufio.NewScanner(r)