Skip to content

Commit e9a3be9

Browse files
committed
feat: add support for configurable image pull policy
Adds an `image_pull_policy` option to the Wings configuration, allowing control over how container images are pulled: - `Always`: pulls the image on every server start (default if not set) - `IfNotPresent`: only pulls the image if it is missing locally - `Never`: requires the image to exist locally
1 parent 1863e4d commit e9a3be9

3 files changed

Lines changed: 120 additions & 16 deletions

File tree

config/config_docker.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ type DockerConfiguration struct {
9292
Type string `default:"local" json:"type" yaml:"type"`
9393
Config map[string]string `default:"{\"max-size\":\"5m\",\"max-file\":\"1\",\"compress\":\"false\",\"mode\":\"non-blocking\"}" json:"config" yaml:"config"`
9494
} `json:"log_config" yaml:"log_config"`
95+
96+
// ImagePullPolicy controls when images are pulled before a container is created.
97+
// Always: pull every time. IfNotPresent: pull only if missing locally. Never: require a local image.
98+
ImagePullPolicy ImagePullPolicy `default:"Always" json:"image_pull_policy" yaml:"image_pull_policy"`
9599
}
96100

97101
func (c DockerConfiguration) ContainerLogConfig() container.LogConfig {
@@ -183,3 +187,12 @@ func (o Overhead) GetMultiplier(memoryLimit int64) float64 {
183187

184188
return o.DefaultMultiplier
185189
}
190+
191+
// ImagePullPolicy controls when wings should pull a container image
192+
type ImagePullPolicy string
193+
194+
const (
195+
ImagePullPolicyAlways ImagePullPolicy = "Always"
196+
ImagePullPolicyIfNotPresent ImagePullPolicy = "IfNotPresent"
197+
ImagePullPolicyNever ImagePullPolicy = "Never"
198+
)

environment/docker/container.go

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -337,29 +337,60 @@ func (e *Environment) Readlog(lines int) ([]string, error) {
337337
return out, nil
338338
}
339339

340-
// Pulls the image from Docker. If there is an error while pulling the image
341-
// from the source but the image already exists locally, we will report that
342-
// error to the logger but continue with the process.
340+
// Pulls the image from Docker when docker.image_pull_policy requires it. If
341+
// there is an error while pulling the image from the source but the image
342+
// already exists locally, we will report that error to the logger but continue
343+
// with the process.
343344
//
344345
// The reasoning behind this is that Quay has had some serious outages as of
345346
// late, and we don't need to block all the servers from booting just because
346347
// of that. I'd imagine in a lot of cases an outage shouldn't affect users too
347348
// badly. It'll at least keep existing servers working correctly if anything.
348349
func (e *Environment) ensureImageExists(img string) error {
349-
e.Events().Publish(environment.DockerImagePullStarted, "")
350-
defer e.Events().Publish(environment.DockerImagePullCompleted, "")
351-
352350
// Images prefixed with a ~ are local images that we do not need to try and pull.
353351
if strings.HasPrefix(img, "~") {
354352
return nil
355353
}
356354

355+
policy := config.Get().Docker.ImagePullPolicy
356+
if policy == "" {
357+
policy = config.ImagePullPolicyAlways
358+
}
359+
357360
// Give it up to 15 minutes to pull the image. I think this should cover 99.8% of cases where an
358361
// image pull might fail. I can't imagine it will ever take more than 15 minutes to fully pull
359362
// an image. Let me know when I am inevitably wrong here...
360363
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
361364
defer cancel()
362365

366+
switch policy {
367+
case config.ImagePullPolicyNever:
368+
// check if the image exists and if not return an error
369+
exists, err := ImageExistsLocally(ctx, e.client, img)
370+
if err != nil {
371+
return err
372+
}
373+
if !exists {
374+
// The image doesn't exist locally so return an error
375+
return errors.Errorf("environment/docker: image %q is not present locally (docker.image_pull_policy is Never)", img)
376+
}
377+
return nil
378+
case config.ImagePullPolicyIfNotPresent:
379+
// check if the image exists and if not pull it
380+
exists, err := ImageExistsLocally(ctx, e.client, img)
381+
if err != nil {
382+
return err
383+
}
384+
if exists {
385+
// The image is already pulled so return
386+
return nil
387+
}
388+
// the image doesn't exist yet so proceed to pull it
389+
}
390+
391+
e.Events().Publish(environment.DockerImagePullStarted, "")
392+
defer e.Events().Publish(environment.DockerImagePullCompleted, "")
393+
363394
// Get a registry auth configuration from the config.
364395
var registryAuth *config.RegistryConfiguration
365396
for registry, c := range config.Get().Docker.Registries {
@@ -438,6 +469,22 @@ func (e *Environment) ensureImageExists(img string) error {
438469
return nil
439470
}
440471

472+
// ImageExistsLocally checks if the provided image tag already exists locally
473+
func ImageExistsLocally(ctx context.Context, client *client.Client, img string) (bool, error) {
474+
images, err := client.ImageList(ctx, image.ListOptions{})
475+
if err != nil {
476+
return false, errors.Wrap(err, "environment/docker: failed to list images")
477+
}
478+
for _, img2 := range images {
479+
for _, t := range img2.RepoTags {
480+
if t == img {
481+
return true, nil
482+
}
483+
}
484+
}
485+
return false, nil
486+
}
487+
441488
func (e *Environment) convertMounts() []mount.Mount {
442489
mounts := e.Configuration.Mounts()
443490
out := make([]mount.Mount, len(mounts))

server/install.go

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/docker/docker/api/types/image"
1818
"github.com/docker/docker/api/types/mount"
1919
"github.com/docker/docker/client"
20+
"github.com/pterodactyl/wings/environment/docker"
2021

2122
"github.com/pterodactyl/wings/config"
2223
"github.com/pterodactyl/wings/environment"
@@ -232,12 +233,55 @@ func (ip *InstallationProcess) writeScriptToDisk() error {
232233
return nil
233234
}
234235

235-
// Pulls the docker image to be used for the installation container.
236+
// Pulls the docker image to be used for the installation container when
237+
// docker.image_pull_policy requires it. If there is an error while pulling from
238+
// the source but the image already exists locally, we log a warning and continue.
236239
func (ip *InstallationProcess) pullInstallationImage() error {
240+
img := ip.Script.ContainerImage
241+
242+
// Images prefixed with a ~ are local images that we do not need to try and pull.
243+
if strings.HasPrefix(img, "~") {
244+
return nil
245+
}
246+
247+
policy := config.Get().Docker.ImagePullPolicy
248+
if policy == "" {
249+
policy = config.ImagePullPolicyAlways
250+
}
251+
252+
// Give it up to 15 minutes to pull the image
253+
ctx, cancel := context.WithTimeout(ip.Server.Context(), 15*time.Minute)
254+
defer cancel()
255+
256+
switch policy {
257+
case config.ImagePullPolicyNever:
258+
// check if the image exists and if not return an error
259+
exists, err := docker.ImageExistsLocally(ctx, ip.client, img)
260+
if err != nil {
261+
return err
262+
}
263+
if !exists {
264+
// The image doesn't exist locally so return an error
265+
return errors.Errorf("server/install: image %q is not present locally (docker.image_pull_policy is Never)", img)
266+
}
267+
return nil
268+
case config.ImagePullPolicyIfNotPresent:
269+
// check if the image exists and if not pull it
270+
exists, err := docker.ImageExistsLocally(ctx, ip.client, img)
271+
if err != nil {
272+
return err
273+
}
274+
if exists {
275+
// The image is already pulled so return
276+
return nil
277+
}
278+
// the image doesn't exist yet so proceed to pull it
279+
}
280+
237281
// Get a registry auth configuration from the config.
238282
var registryAuth *config.RegistryConfiguration
239283
for registry, c := range config.Get().Docker.Registries {
240-
if !strings.HasPrefix(ip.Script.ContainerImage, registry) {
284+
if !strings.HasPrefix(img, registry) {
241285
continue
242286
}
243287

@@ -258,23 +302,23 @@ func (ip *InstallationProcess) pullInstallationImage() error {
258302
imagePullOptions.RegistryAuth = b64
259303
}
260304

261-
r, err := ip.client.ImagePull(ip.Server.Context(), ip.Script.ContainerImage, imagePullOptions)
305+
r, err := ip.client.ImagePull(ctx, img, imagePullOptions)
262306
if err != nil {
263-
images, ierr := ip.client.ImageList(ip.Server.Context(), image.ListOptions{})
307+
images, ierr := ip.client.ImageList(ctx, image.ListOptions{})
264308
if ierr != nil {
265309
// Well damn, something has gone really wrong here, just go ahead and abort there
266310
// isn't much anything we can do to try and self-recover from this.
267311
return ierr
268312
}
269313

270-
for _, img := range images {
271-
for _, t := range img.RepoTags {
272-
if t != ip.Script.ContainerImage {
314+
for _, img2 := range images {
315+
for _, t := range img2.RepoTags {
316+
if t != img {
273317
continue
274318
}
275319

276320
log.WithFields(log.Fields{
277-
"image": ip.Script.ContainerImage,
321+
"image": img,
278322
"err": err.Error(),
279323
}).Warn("unable to pull requested image from remote source, however the image exists locally")
280324

@@ -284,11 +328,11 @@ func (ip *InstallationProcess) pullInstallationImage() error {
284328
}
285329
}
286330

287-
return err
331+
return errors.Wrapf(err, "failed to pull %q installation container image", img)
288332
}
289333
defer r.Close()
290334

291-
log.WithField("image", ip.Script.ContainerImage).Debug("pulling docker image... this could take a bit of time")
335+
log.WithField("image", img).Debug("pulling docker image... this could take a bit of time")
292336

293337
// Block continuation until the image has been pulled successfully.
294338
scanner := bufio.NewScanner(r)

0 commit comments

Comments
 (0)