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