github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/pkg/autoupdate/autoupdate.go (about) 1 package autoupdate 2 3 import ( 4 "context" 5 "os" 6 "sort" 7 8 "github.com/containers/image/v5/docker" 9 "github.com/containers/image/v5/docker/reference" 10 "github.com/containers/image/v5/manifest" 11 "github.com/containers/image/v5/transports/alltransports" 12 "github.com/containers/libpod/libpod" 13 "github.com/containers/libpod/libpod/define" 14 "github.com/containers/libpod/libpod/image" 15 "github.com/containers/libpod/pkg/systemd" 16 systemdGen "github.com/containers/libpod/pkg/systemd/generate" 17 "github.com/containers/libpod/pkg/util" 18 "github.com/pkg/errors" 19 "github.com/sirupsen/logrus" 20 ) 21 22 // Label denotes the container/pod label key to specify auto-update policies in 23 // container labels. 24 const Label = "io.containers.autoupdate" 25 26 // Policy represents an auto-update policy. 27 type Policy string 28 29 const ( 30 // PolicyDefault is the default policy denoting no auto updates. 31 PolicyDefault Policy = "disabled" 32 // PolicyNewImage is the policy to update as soon as there's a new image found. 33 PolicyNewImage = "image" 34 ) 35 36 // Map for easy lookups of supported policies. 37 var supportedPolicies = map[string]Policy{ 38 "": PolicyDefault, 39 "disabled": PolicyDefault, 40 "image": PolicyNewImage, 41 } 42 43 // LookupPolicy looksup the corresponding Policy for the specified 44 // string. If none is found, an errors is returned including the list of 45 // supported policies. 46 // 47 // Note that an empty string resolved to PolicyDefault. 48 func LookupPolicy(s string) (Policy, error) { 49 policy, exists := supportedPolicies[s] 50 if exists { 51 return policy, nil 52 } 53 54 // Sort the keys first as maps are non-deterministic. 55 keys := []string{} 56 for k := range supportedPolicies { 57 if k != "" { 58 keys = append(keys, k) 59 } 60 } 61 sort.Strings(keys) 62 63 return "", errors.Errorf("invalid auto-update policy %q: valid policies are %+q", s, keys) 64 } 65 66 // ValidateImageReference checks if the specified imageName is a fully-qualified 67 // image reference to the docker transport (without digest). Such a reference 68 // includes a domain, name and tag (e.g., quay.io/podman/stable:latest). The 69 // reference may also be prefixed with "docker://" explicitly indicating that 70 // it's a reference to the docker transport. 71 func ValidateImageReference(imageName string) error { 72 // Make sure the input image is a docker. 73 imageRef, err := alltransports.ParseImageName(imageName) 74 if err == nil && imageRef.Transport().Name() != docker.Transport.Name() { 75 return errors.Errorf("auto updates require the docker image transport but image is of transport %q", imageRef.Transport().Name()) 76 } else if err != nil { 77 repo, err := reference.Parse(imageName) 78 if err != nil { 79 return errors.Wrap(err, "error enforcing fully-qualified docker transport reference for auto updates") 80 } 81 if _, ok := repo.(reference.NamedTagged); !ok { 82 return errors.Errorf("auto updates require fully-qualified image references (no tag): %q", imageName) 83 } 84 if _, ok := repo.(reference.Digested); ok { 85 return errors.Errorf("auto updates require fully-qualified image references without digest: %q", imageName) 86 } 87 } 88 return nil 89 } 90 91 // AutoUpdate looks up containers with a specified auto-update policy and acts 92 // accordingly. If the policy is set to PolicyNewImage, it checks if the image 93 // on the remote registry is different than the local one. If the image digests 94 // differ, it pulls the remote image and restarts the systemd unit running the 95 // container. 96 // 97 // It returns a slice of successfully restarted systemd units and a slice of 98 // errors encountered during auto update. 99 func AutoUpdate(runtime *libpod.Runtime) ([]string, []error) { 100 // Create a map from `image ID -> []*Container`. 101 containerMap, errs := imageContainersMap(runtime) 102 if len(containerMap) == 0 { 103 return nil, errs 104 } 105 106 // Create a map from `image ID -> *image.Image` for image lookups. 107 imagesSlice, err := runtime.ImageRuntime().GetImages() 108 if err != nil { 109 return nil, []error{err} 110 } 111 imageMap := make(map[string]*image.Image) 112 for i := range imagesSlice { 113 imageMap[imagesSlice[i].ID()] = imagesSlice[i] 114 } 115 116 // Connect to DBUS. 117 conn, err := systemd.ConnectToDBUS() 118 if err != nil { 119 logrus.Errorf(err.Error()) 120 return nil, []error{err} 121 } 122 defer conn.Close() 123 124 // Update images. 125 containersToRestart := []*libpod.Container{} 126 updatedRawImages := make(map[string]bool) 127 for imageID, containers := range containerMap { 128 image, exists := imageMap[imageID] 129 if !exists { 130 errs = append(errs, errors.Errorf("container image ID %q not found in local storage", imageID)) 131 return nil, errs 132 } 133 // Now we have to check if the image of any containers must be updated. 134 // Note that the image ID is NOT enough for this check as a given image 135 // may have multiple tags. 136 for i, ctr := range containers { 137 rawImageName := ctr.RawImageName() 138 if rawImageName == "" { 139 errs = append(errs, errors.Errorf("error auto-updating container %q: raw-image name is empty", ctr.ID())) 140 } 141 needsUpdate, err := newerImageAvailable(runtime, image, rawImageName) 142 if err != nil { 143 errs = append(errs, errors.Wrapf(err, "error auto-updating container %q: image check for %q failed", ctr.ID(), rawImageName)) 144 continue 145 } 146 if !needsUpdate { 147 continue 148 } 149 logrus.Infof("Auto-updating container %q using image %q", ctr.ID(), rawImageName) 150 if _, updated := updatedRawImages[rawImageName]; !updated { 151 _, err = updateImage(runtime, rawImageName) 152 if err != nil { 153 errs = append(errs, errors.Wrapf(err, "error auto-updating container %q: image update for %q failed", ctr.ID(), rawImageName)) 154 continue 155 } 156 updatedRawImages[rawImageName] = true 157 } 158 containersToRestart = append(containersToRestart, containers[i]) 159 } 160 } 161 162 // Restart containers. 163 updatedUnits := []string{} 164 for _, ctr := range containersToRestart { 165 labels := ctr.Labels() 166 unit, exists := labels[systemdGen.EnvVariable] 167 if !exists { 168 // Shouldn't happen but let's be sure of it. 169 errs = append(errs, errors.Errorf("error auto-updating container %q: no %s label found", ctr.ID(), systemdGen.EnvVariable)) 170 continue 171 } 172 _, err := conn.RestartUnit(unit, "replace", nil) 173 if err != nil { 174 errs = append(errs, errors.Wrapf(err, "error auto-updating container %q: restarting systemd unit %q failed", ctr.ID(), unit)) 175 continue 176 } 177 logrus.Infof("Successfully restarted systemd unit %q", unit) 178 updatedUnits = append(updatedUnits, unit) 179 } 180 181 return updatedUnits, errs 182 } 183 184 // imageContainersMap generates a map[image ID] -> [containers using the image] 185 // of all containers with a valid auto-update policy. 186 func imageContainersMap(runtime *libpod.Runtime) (map[string][]*libpod.Container, []error) { 187 allContainers, err := runtime.GetAllContainers() 188 if err != nil { 189 return nil, []error{err} 190 } 191 192 errors := []error{} 193 imageMap := make(map[string][]*libpod.Container) 194 for i, ctr := range allContainers { 195 state, err := ctr.State() 196 if err != nil { 197 errors = append(errors, err) 198 continue 199 } 200 // Only update running containers. 201 if state != define.ContainerStateRunning { 202 continue 203 } 204 // Only update containers with the specific label/policy set. 205 labels := ctr.Labels() 206 if value, exists := labels[Label]; exists { 207 policy, err := LookupPolicy(value) 208 if err != nil { 209 errors = append(errors, err) 210 continue 211 } 212 if policy != PolicyNewImage { 213 continue 214 } 215 } 216 // Now we know that `ctr` is configured for auto updates. 217 id, _ := ctr.Image() 218 imageMap[id] = append(imageMap[id], allContainers[i]) 219 } 220 221 return imageMap, errors 222 } 223 224 // newerImageAvailable returns true if there corresponding image on the remote 225 // registry is newer. 226 func newerImageAvailable(runtime *libpod.Runtime, img *image.Image, origName string) (bool, error) { 227 remoteRef, err := docker.ParseReference("//" + origName) 228 if err != nil { 229 return false, err 230 } 231 232 remoteImg, err := remoteRef.NewImage(context.Background(), runtime.SystemContext()) 233 if err != nil { 234 return false, err 235 } 236 237 rawManifest, _, err := remoteImg.Manifest(context.Background()) 238 if err != nil { 239 return false, err 240 } 241 242 remoteDigest, err := manifest.Digest(rawManifest) 243 if err != nil { 244 return false, err 245 } 246 247 return img.Digest().String() != remoteDigest.String(), nil 248 } 249 250 // updateImage pulls the specified image. 251 func updateImage(runtime *libpod.Runtime, name string) (*image.Image, error) { 252 sys := runtime.SystemContext() 253 registryOpts := image.DockerRegistryOptions{} 254 signaturePolicyPath := "" 255 authFilePath := "" 256 257 if sys != nil { 258 registryOpts.OSChoice = sys.OSChoice 259 registryOpts.ArchitectureChoice = sys.OSChoice 260 registryOpts.DockerCertPath = sys.DockerCertPath 261 262 signaturePolicyPath = sys.SignaturePolicyPath 263 authFilePath = sys.AuthFilePath 264 } 265 266 newImage, err := runtime.ImageRuntime().New(context.Background(), 267 docker.Transport.Name()+"://"+name, 268 signaturePolicyPath, 269 authFilePath, 270 os.Stderr, 271 ®istryOpts, 272 image.SigningOptions{}, 273 nil, 274 util.PullImageAlways, 275 ) 276 if err != nil { 277 return nil, err 278 } 279 return newImage, nil 280 }