github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/pkg/autoupdate/autoupdate.go (about) 1 package autoupdate 2 3 import ( 4 "context" 5 "os" 6 "sort" 7 8 "github.com/containers/common/libimage" 9 "github.com/containers/common/pkg/config" 10 "github.com/containers/image/v5/docker" 11 "github.com/containers/image/v5/docker/reference" 12 "github.com/containers/image/v5/transports/alltransports" 13 "github.com/hanks177/podman/v4/libpod" 14 "github.com/hanks177/podman/v4/libpod/define" 15 "github.com/hanks177/podman/v4/libpod/events" 16 "github.com/hanks177/podman/v4/pkg/domain/entities" 17 "github.com/hanks177/podman/v4/pkg/systemd" 18 systemdDefine "github.com/hanks177/podman/v4/pkg/systemd/define" 19 "github.com/coreos/go-systemd/v22/dbus" 20 "github.com/pkg/errors" 21 "github.com/sirupsen/logrus" 22 ) 23 24 // Label denotes the container/pod label key to specify auto-update policies in 25 // container labels. 26 const Label = "io.containers.autoupdate" 27 28 // Label denotes the container label key to specify authfile in 29 // container labels. 30 const AuthfileLabel = "io.containers.autoupdate.authfile" 31 32 // Policy represents an auto-update policy. 33 type Policy string 34 35 const ( 36 // PolicyDefault is the default policy denoting no auto updates. 37 PolicyDefault Policy = "disabled" 38 // PolicyRegistryImage is the policy to update as soon as there's a new image found. 39 PolicyRegistryImage = "registry" 40 // PolicyLocalImage is the policy to run auto-update based on a local image 41 PolicyLocalImage = "local" 42 ) 43 44 // Map for easy lookups of supported policies. 45 var supportedPolicies = map[string]Policy{ 46 "": PolicyDefault, 47 "disabled": PolicyDefault, 48 "image": PolicyRegistryImage, 49 "registry": PolicyRegistryImage, 50 "local": PolicyLocalImage, 51 } 52 53 // policyMapper is used for tying a container to it's autoupdate policy 54 type policyMapper map[Policy][]*libpod.Container 55 56 // LookupPolicy looks up the corresponding Policy for the specified 57 // string. If none is found, an errors is returned including the list of 58 // supported policies. 59 // 60 // Note that an empty string resolved to PolicyDefault. 61 func LookupPolicy(s string) (Policy, error) { 62 policy, exists := supportedPolicies[s] 63 if exists { 64 return policy, nil 65 } 66 67 // Sort the keys first as maps are non-deterministic. 68 keys := []string{} 69 for k := range supportedPolicies { 70 if k != "" { 71 keys = append(keys, k) 72 } 73 } 74 sort.Strings(keys) 75 76 return "", errors.Errorf("invalid auto-update policy %q: valid policies are %+q", s, keys) 77 } 78 79 // ValidateImageReference checks if the specified imageName is a fully-qualified 80 // image reference to the docker transport (without digest). Such a reference 81 // includes a domain, name and tag (e.g., quay.io/podman/stable:latest). The 82 // reference may also be prefixed with "docker://" explicitly indicating that 83 // it's a reference to the docker transport. 84 func ValidateImageReference(imageName string) error { 85 // Make sure the input image is a docker. 86 imageRef, err := alltransports.ParseImageName(imageName) 87 if err == nil && imageRef.Transport().Name() != docker.Transport.Name() { 88 return errors.Errorf("auto updates require the docker image transport but image is of transport %q", imageRef.Transport().Name()) 89 } else if err != nil { 90 repo, err := reference.Parse(imageName) 91 if err != nil { 92 return errors.Wrap(err, "enforcing fully-qualified docker transport reference for auto updates") 93 } 94 if _, ok := repo.(reference.NamedTagged); !ok { 95 return errors.Errorf("auto updates require fully-qualified image references (no tag): %q", imageName) 96 } 97 if _, ok := repo.(reference.Digested); ok { 98 return errors.Errorf("auto updates require fully-qualified image references without digest: %q", imageName) 99 } 100 } 101 return nil 102 } 103 104 // AutoUpdate looks up containers with a specified auto-update policy and acts 105 // accordingly. 106 // 107 // If the policy is set to PolicyRegistryImage, it checks if the image 108 // on the remote registry is different than the local one. If the image digests 109 // differ, it pulls the remote image and restarts the systemd unit running the 110 // container. 111 // 112 // If the policy is set to PolicyLocalImage, it checks if the image 113 // of a running container is different than the local one. If the image digests 114 // differ, it restarts the systemd unit with the new image. 115 // 116 // It returns a slice of successfully restarted systemd units and a slice of 117 // errors encountered during auto update. 118 func AutoUpdate(ctx context.Context, runtime *libpod.Runtime, options entities.AutoUpdateOptions) ([]*entities.AutoUpdateReport, []error) { 119 // Create a map from `image ID -> []*Container`. 120 containerMap, errs := imageContainersMap(runtime) 121 if len(containerMap) == 0 { 122 return nil, errs 123 } 124 125 // Create a map from `image ID -> *libimage.Image` for image lookups. 126 listOptions := &libimage.ListImagesOptions{ 127 Filters: []string{"readonly=false"}, 128 } 129 imagesSlice, err := runtime.LibimageRuntime().ListImages(ctx, nil, listOptions) 130 if err != nil { 131 return nil, []error{err} 132 } 133 imageMap := make(map[string]*libimage.Image) 134 for i := range imagesSlice { 135 imageMap[imagesSlice[i].ID()] = imagesSlice[i] 136 } 137 138 // Connect to DBUS. 139 conn, err := systemd.ConnectToDBUS() 140 if err != nil { 141 logrus.Errorf(err.Error()) 142 return nil, []error{err} 143 } 144 defer conn.Close() 145 146 runtime.NewSystemEvent(events.AutoUpdate) 147 148 // Update all images/container according to their auto-update policy. 149 var allReports []*entities.AutoUpdateReport 150 updatedRawImages := make(map[string]bool) 151 for imageID, policyMapper := range containerMap { 152 image, exists := imageMap[imageID] 153 if !exists { 154 errs = append(errs, errors.Errorf("container image ID %q not found in local storage", imageID)) 155 return nil, errs 156 } 157 158 for _, ctr := range policyMapper[PolicyRegistryImage] { 159 report, err := autoUpdateRegistry(ctx, image, ctr, updatedRawImages, &options, conn, runtime) 160 if err != nil { 161 errs = append(errs, err) 162 } 163 if report != nil { 164 allReports = append(allReports, report) 165 } 166 } 167 168 for _, ctr := range policyMapper[PolicyLocalImage] { 169 report, err := autoUpdateLocally(ctx, image, ctr, &options, conn, runtime) 170 if err != nil { 171 errs = append(errs, err) 172 } 173 if report != nil { 174 allReports = append(allReports, report) 175 } 176 } 177 } 178 179 return allReports, errs 180 } 181 182 // autoUpdateRegistry updates the image/container according to the "registry" policy. 183 func autoUpdateRegistry(ctx context.Context, image *libimage.Image, ctr *libpod.Container, updatedRawImages map[string]bool, options *entities.AutoUpdateOptions, conn *dbus.Conn, runtime *libpod.Runtime) (*entities.AutoUpdateReport, error) { 184 cid := ctr.ID() 185 rawImageName := ctr.RawImageName() 186 if rawImageName == "" { 187 return nil, errors.Errorf("registry auto-updating container %q: raw-image name is empty", cid) 188 } 189 190 labels := ctr.Labels() 191 unit, exists := labels[systemdDefine.EnvVariable] 192 if !exists { 193 return nil, errors.Errorf("auto-updating container %q: no %s label found", ctr.ID(), systemdDefine.EnvVariable) 194 } 195 196 report := &entities.AutoUpdateReport{ 197 ContainerID: cid, 198 ContainerName: ctr.Name(), 199 ImageName: rawImageName, 200 Policy: PolicyRegistryImage, 201 SystemdUnit: unit, 202 Updated: "failed", 203 } 204 205 if _, updated := updatedRawImages[rawImageName]; updated { 206 logrus.Infof("Auto-updating container %q using registry image %q", cid, rawImageName) 207 if err := restartSystemdUnit(ctx, ctr, unit, conn); err != nil { 208 return report, err 209 } 210 report.Updated = "true" 211 return report, nil 212 } 213 214 authfile := getAuthfilePath(ctr, options) 215 needsUpdate, err := newerRemoteImageAvailable(ctx, image, rawImageName, authfile) 216 if err != nil { 217 return report, errors.Wrapf(err, "registry auto-updating container %q: image check for %q failed", cid, rawImageName) 218 } 219 220 if !needsUpdate { 221 report.Updated = "false" 222 return report, nil 223 } 224 225 if options.DryRun { 226 report.Updated = "pending" 227 return report, nil 228 } 229 230 if _, err := updateImage(ctx, runtime, rawImageName, authfile); err != nil { 231 return report, errors.Wrapf(err, "registry auto-updating container %q: image update for %q failed", cid, rawImageName) 232 } 233 updatedRawImages[rawImageName] = true 234 235 logrus.Infof("Auto-updating container %q using registry image %q", cid, rawImageName) 236 updateErr := restartSystemdUnit(ctx, ctr, unit, conn) 237 if updateErr == nil { 238 report.Updated = "true" 239 return report, nil 240 } 241 242 if !options.Rollback { 243 return report, updateErr 244 } 245 246 // To fallback, simply retag the old image and restart the service. 247 if err := image.Tag(rawImageName); err != nil { 248 return report, errors.Wrap(err, "falling back to previous image") 249 } 250 if err := restartSystemdUnit(ctx, ctr, unit, conn); err != nil { 251 return report, errors.Wrap(err, "restarting unit with old image during fallback") 252 } 253 254 report.Updated = "rolled back" 255 return report, nil 256 } 257 258 // autoUpdateRegistry updates the image/container according to the "local" policy. 259 func autoUpdateLocally(ctx context.Context, image *libimage.Image, ctr *libpod.Container, options *entities.AutoUpdateOptions, conn *dbus.Conn, runtime *libpod.Runtime) (*entities.AutoUpdateReport, error) { 260 cid := ctr.ID() 261 rawImageName := ctr.RawImageName() 262 if rawImageName == "" { 263 return nil, errors.Errorf("locally auto-updating container %q: raw-image name is empty", cid) 264 } 265 266 labels := ctr.Labels() 267 unit, exists := labels[systemdDefine.EnvVariable] 268 if !exists { 269 return nil, errors.Errorf("auto-updating container %q: no %s label found", ctr.ID(), systemdDefine.EnvVariable) 270 } 271 272 report := &entities.AutoUpdateReport{ 273 ContainerID: cid, 274 ContainerName: ctr.Name(), 275 ImageName: rawImageName, 276 Policy: PolicyLocalImage, 277 SystemdUnit: unit, 278 Updated: "failed", 279 } 280 281 needsUpdate, err := newerLocalImageAvailable(runtime, image, rawImageName) 282 if err != nil { 283 return report, errors.Wrapf(err, "locally auto-updating container %q: image check for %q failed", cid, rawImageName) 284 } 285 286 if !needsUpdate { 287 report.Updated = "false" 288 return report, nil 289 } 290 291 if options.DryRun { 292 report.Updated = "pending" 293 return report, nil 294 } 295 296 logrus.Infof("Auto-updating container %q using local image %q", cid, rawImageName) 297 updateErr := restartSystemdUnit(ctx, ctr, unit, conn) 298 if updateErr == nil { 299 report.Updated = "true" 300 return report, nil 301 } 302 303 if !options.Rollback { 304 return report, updateErr 305 } 306 307 // To fallback, simply retag the old image and restart the service. 308 if err := image.Tag(rawImageName); err != nil { 309 return report, errors.Wrap(err, "falling back to previous image") 310 } 311 if err := restartSystemdUnit(ctx, ctr, unit, conn); err != nil { 312 return report, errors.Wrap(err, "restarting unit with old image during fallback") 313 } 314 315 report.Updated = "rolled back" 316 return report, nil 317 } 318 319 // restartSystemdUnit restarts the systemd unit the container is running in. 320 func restartSystemdUnit(ctx context.Context, ctr *libpod.Container, unit string, conn *dbus.Conn) error { 321 restartChan := make(chan string) 322 if _, err := conn.RestartUnitContext(ctx, unit, "replace", restartChan); err != nil { 323 return errors.Wrapf(err, "auto-updating container %q: restarting systemd unit %q failed", ctr.ID(), unit) 324 } 325 326 // Wait for the restart to finish and actually check if it was 327 // successful or not. 328 result := <-restartChan 329 330 switch result { 331 case "done": 332 logrus.Infof("Successfully restarted systemd unit %q of container %q", unit, ctr.ID()) 333 return nil 334 335 default: 336 return errors.Errorf("auto-updating container %q: restarting systemd unit %q failed: expected %q but received %q", ctr.ID(), unit, "done", result) 337 } 338 } 339 340 // imageContainersMap generates a map[image ID] -> [containers using the image] 341 // of all containers with a valid auto-update policy. 342 func imageContainersMap(runtime *libpod.Runtime) (map[string]policyMapper, []error) { 343 allContainers, err := runtime.GetAllContainers() 344 if err != nil { 345 return nil, []error{err} 346 } 347 348 errors := []error{} 349 containerMap := make(map[string]policyMapper) 350 for _, ctr := range allContainers { 351 state, err := ctr.State() 352 if err != nil { 353 errors = append(errors, err) 354 continue 355 } 356 // Only update running containers. 357 if state != define.ContainerStateRunning { 358 continue 359 } 360 361 // Only update containers with the specific label/policy set. 362 labels := ctr.Labels() 363 value, exists := labels[Label] 364 if !exists { 365 continue 366 } 367 368 policy, err := LookupPolicy(value) 369 if err != nil { 370 errors = append(errors, err) 371 continue 372 } 373 374 // Skip labels not related to autoupdate 375 if policy == PolicyDefault { 376 continue 377 } else { 378 id, _ := ctr.Image() 379 policyMap, exists := containerMap[id] 380 if !exists { 381 policyMap = make(map[Policy][]*libpod.Container) 382 } 383 policyMap[policy] = append(policyMap[policy], ctr) 384 containerMap[id] = policyMap 385 // Now we know that `ctr` is configured for auto updates. 386 } 387 } 388 389 return containerMap, errors 390 } 391 392 // getAuthfilePath returns an authfile path, if set. The authfile label in the 393 // container, if set, as precedence over the one set in the options. 394 func getAuthfilePath(ctr *libpod.Container, options *entities.AutoUpdateOptions) string { 395 labels := ctr.Labels() 396 authFilePath, exists := labels[AuthfileLabel] 397 if exists { 398 return authFilePath 399 } 400 return options.Authfile 401 } 402 403 // newerRemoteImageAvailable returns true if there corresponding image on the remote 404 // registry is newer. 405 func newerRemoteImageAvailable(ctx context.Context, img *libimage.Image, origName string, authfile string) (bool, error) { 406 remoteRef, err := docker.ParseReference("//" + origName) 407 if err != nil { 408 return false, err 409 } 410 options := &libimage.HasDifferentDigestOptions{AuthFilePath: authfile} 411 return img.HasDifferentDigest(ctx, remoteRef, options) 412 } 413 414 // newerLocalImageAvailable returns true if the container and local image have different digests 415 func newerLocalImageAvailable(runtime *libpod.Runtime, img *libimage.Image, rawImageName string) (bool, error) { 416 localImg, _, err := runtime.LibimageRuntime().LookupImage(rawImageName, nil) 417 if err != nil { 418 return false, err 419 } 420 return localImg.Digest().String() != img.Digest().String(), nil 421 } 422 423 // updateImage pulls the specified image. 424 func updateImage(ctx context.Context, runtime *libpod.Runtime, name, authfile string) (*libimage.Image, error) { 425 pullOptions := &libimage.PullOptions{} 426 pullOptions.AuthFilePath = authfile 427 pullOptions.Writer = os.Stderr 428 429 pulledImages, err := runtime.LibimageRuntime().Pull(ctx, name, config.PullPolicyAlways, pullOptions) 430 if err != nil { 431 return nil, err 432 } 433 return pulledImages[0], nil 434 }