Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions config/config_docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"
)
59 changes: 53 additions & 6 deletions environment/docker/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
Expand Down
64 changes: 54 additions & 10 deletions server/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}

Expand All @@ -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")

Expand All @@ -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)
Expand Down