github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/pkg/api/handlers/compat/images.go (about) 1 package compat 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "os" 9 "strings" 10 11 "github.com/containers/buildah" 12 "github.com/containers/common/libimage" 13 "github.com/containers/common/pkg/config" 14 "github.com/containers/common/pkg/filters" 15 "github.com/containers/image/v5/manifest" 16 "github.com/containers/image/v5/types" 17 "github.com/hanks177/podman/v4/libpod" 18 "github.com/hanks177/podman/v4/pkg/api/handlers" 19 "github.com/hanks177/podman/v4/pkg/api/handlers/utils" 20 api "github.com/hanks177/podman/v4/pkg/api/types" 21 "github.com/hanks177/podman/v4/pkg/auth" 22 "github.com/hanks177/podman/v4/pkg/domain/entities" 23 "github.com/hanks177/podman/v4/pkg/domain/infra/abi" 24 "github.com/containers/storage" 25 "github.com/gorilla/schema" 26 "github.com/opencontainers/go-digest" 27 "github.com/pkg/errors" 28 "github.com/sirupsen/logrus" 29 ) 30 31 // mergeNameAndTagOrDigest creates an image reference as string from the 32 // provided image name and tagOrDigest which can be a tag, a digest or empty. 33 func mergeNameAndTagOrDigest(name, tagOrDigest string) string { 34 if len(tagOrDigest) == 0 { 35 return name 36 } 37 38 separator := ":" // default to tag 39 if _, err := digest.Parse(tagOrDigest); err == nil { 40 // We have a digest, so let's change the separator. 41 separator = "@" 42 } 43 return fmt.Sprintf("%s%s%s", name, separator, tagOrDigest) 44 } 45 46 func ExportImage(w http.ResponseWriter, r *http.Request) { 47 // 200 ok 48 // 500 server 49 runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) 50 51 tmpfile, err := ioutil.TempFile("", "api.tar") 52 if err != nil { 53 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) 54 return 55 } 56 defer os.Remove(tmpfile.Name()) 57 58 name := utils.GetName(r) 59 possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, name) 60 if err != nil { 61 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "error normalizing image")) 62 return 63 } 64 65 imageEngine := abi.ImageEngine{Libpod: runtime} 66 67 saveOptions := entities.ImageSaveOptions{ 68 Format: "docker-archive", 69 Output: tmpfile.Name(), 70 } 71 72 if err := imageEngine.Save(r.Context(), possiblyNormalizedName, nil, saveOptions); err != nil { 73 if errors.Cause(err) == storage.ErrImageUnknown { 74 utils.ImageNotFound(w, name, errors.Wrapf(err, "failed to find image %s", name)) 75 return 76 } 77 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) 78 return 79 } 80 81 if err := tmpfile.Close(); err != nil { 82 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile")) 83 return 84 } 85 86 rdr, err := os.Open(tmpfile.Name()) 87 if err != nil { 88 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to read the exported tarfile")) 89 return 90 } 91 defer rdr.Close() 92 utils.WriteResponse(w, http.StatusOK, rdr) 93 } 94 95 func CommitContainer(w http.ResponseWriter, r *http.Request) { 96 decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) 97 runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) 98 99 query := struct { 100 Author string `schema:"author"` 101 Changes []string `schema:"changes"` 102 Comment string `schema:"comment"` 103 Container string `schema:"container"` 104 Pause bool `schema:"pause"` 105 Squash bool `schema:"squash"` 106 Repo string `schema:"repo"` 107 Tag string `schema:"tag"` 108 // fromSrc string # fromSrc is currently unused 109 }{ 110 Tag: "latest", 111 } 112 113 if err := decoder.Decode(&query, r.URL.Query()); err != nil { 114 utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) 115 return 116 } 117 rtc, err := runtime.GetConfig() 118 if err != nil { 119 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "Decode()")) 120 return 121 } 122 sc := runtime.SystemContext() 123 options := libpod.ContainerCommitOptions{ 124 Pause: true, 125 } 126 options.CommitOptions = buildah.CommitOptions{ 127 SignaturePolicyPath: rtc.Engine.SignaturePolicyPath, 128 ReportWriter: os.Stderr, 129 SystemContext: sc, 130 PreferredManifestType: manifest.DockerV2Schema2MediaType, 131 } 132 133 input := handlers.CreateContainerConfig{} 134 if err := json.NewDecoder(r.Body).Decode(&input); err != nil { 135 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "Decode()")) 136 return 137 } 138 139 options.Message = query.Comment 140 options.Author = query.Author 141 options.Pause = query.Pause 142 options.Squash = query.Squash 143 for _, change := range query.Changes { 144 options.Changes = append(options.Changes, strings.Split(change, "\n")...) 145 } 146 ctr, err := runtime.LookupContainer(query.Container) 147 if err != nil { 148 utils.Error(w, http.StatusNotFound, err) 149 return 150 } 151 152 var destImage string 153 if len(query.Repo) > 1 { 154 destImage = fmt.Sprintf("%s:%s", query.Repo, query.Tag) 155 possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, destImage) 156 if err != nil { 157 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "error normalizing image")) 158 return 159 } 160 destImage = possiblyNormalizedName 161 } 162 163 commitImage, err := ctr.Commit(r.Context(), destImage, options) 164 if err != nil && !strings.Contains(err.Error(), "is not running") { 165 utils.Error(w, http.StatusInternalServerError, errors.Wrapf(err, "CommitFailure")) 166 return 167 } 168 utils.WriteResponse(w, http.StatusCreated, entities.IDResponse{ID: commitImage.ID()}) // nolint 169 } 170 171 func CreateImageFromSrc(w http.ResponseWriter, r *http.Request) { 172 // 200 no error 173 // 404 repo does not exist or no read access 174 // 500 internal 175 decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) 176 runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) 177 178 query := struct { 179 Changes []string `schema:"changes"` 180 FromSrc string `schema:"fromSrc"` 181 Message string `schema:"message"` 182 Platform string `schema:"platform"` 183 Repo string `shchema:"repo"` 184 }{ 185 // This is where you can override the golang default value for one of fields 186 } 187 188 if err := decoder.Decode(&query, r.URL.Query()); err != nil { 189 utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) 190 return 191 } 192 // fromSrc – Source to import. The value may be a URL from which the image can be retrieved or - to read the image from the request body. This parameter may only be used when importing an image. 193 source := query.FromSrc 194 if source == "-" { 195 f, err := ioutil.TempFile("", "api_load.tar") 196 if err != nil { 197 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to create tempfile")) 198 return 199 } 200 201 source = f.Name() 202 if err := SaveFromBody(f, r); err != nil { 203 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to write temporary file")) 204 } 205 } 206 207 reference := query.Repo 208 if query.Repo != "" { 209 possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, reference) 210 if err != nil { 211 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "error normalizing image")) 212 return 213 } 214 reference = possiblyNormalizedName 215 } 216 217 platformSpecs := strings.Split(query.Platform, "/") 218 opts := entities.ImageImportOptions{ 219 Source: source, 220 Changes: query.Changes, 221 Message: query.Message, 222 Reference: reference, 223 OS: platformSpecs[0], 224 } 225 if len(platformSpecs) > 1 { 226 opts.Architecture = platformSpecs[1] 227 } 228 229 imageEngine := abi.ImageEngine{Libpod: runtime} 230 report, err := imageEngine.Import(r.Context(), opts) 231 if err != nil { 232 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to import tarball")) 233 return 234 } 235 // Success 236 utils.WriteResponse(w, http.StatusOK, struct { 237 Status string `json:"status"` 238 Progress string `json:"progress"` 239 ProgressDetail map[string]string `json:"progressDetail"` 240 Id string `json:"id"` // nolint 241 }{ 242 Status: report.Id, 243 ProgressDetail: map[string]string{}, 244 Id: report.Id, 245 }) 246 } 247 248 type pullResult struct { 249 images []*libimage.Image 250 err error 251 } 252 253 func CreateImageFromImage(w http.ResponseWriter, r *http.Request) { 254 // 200 no error 255 // 404 repo does not exist or no read access 256 // 500 internal 257 decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) 258 runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) 259 260 query := struct { 261 FromImage string `schema:"fromImage"` 262 Tag string `schema:"tag"` 263 Platform string `schema:"platform"` 264 }{ 265 // This is where you can override the golang default value for one of fields 266 } 267 if err := decoder.Decode(&query, r.URL.Query()); err != nil { 268 utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) 269 return 270 } 271 272 possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, mergeNameAndTagOrDigest(query.FromImage, query.Tag)) 273 if err != nil { 274 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "error normalizing image")) 275 return 276 } 277 278 authConf, authfile, err := auth.GetCredentials(r) 279 if err != nil { 280 utils.Error(w, http.StatusBadRequest, err) 281 return 282 } 283 defer auth.RemoveAuthfile(authfile) 284 285 pullOptions := &libimage.PullOptions{} 286 pullOptions.AuthFilePath = authfile 287 if authConf != nil { 288 pullOptions.Username = authConf.Username 289 pullOptions.Password = authConf.Password 290 pullOptions.IdentityToken = authConf.IdentityToken 291 } 292 pullOptions.Writer = os.Stderr // allows for debugging on the server 293 294 // Handle the platform. 295 platformSpecs := strings.Split(query.Platform, "/") 296 pullOptions.OS = platformSpecs[0] // may be empty 297 if len(platformSpecs) > 1 { 298 pullOptions.Architecture = platformSpecs[1] 299 if len(platformSpecs) > 2 { 300 pullOptions.Variant = platformSpecs[2] 301 } 302 } 303 304 progress := make(chan types.ProgressProperties) 305 pullOptions.Progress = progress 306 307 pullResChan := make(chan pullResult) 308 go func() { 309 pulledImages, err := runtime.LibimageRuntime().Pull(r.Context(), possiblyNormalizedName, config.PullPolicyAlways, pullOptions) 310 pullResChan <- pullResult{images: pulledImages, err: err} 311 }() 312 313 flush := func() { 314 if flusher, ok := w.(http.Flusher); ok { 315 flusher.Flush() 316 } 317 } 318 319 w.WriteHeader(http.StatusOK) 320 w.Header().Set("Content-Type", "application/json") 321 flush() 322 323 enc := json.NewEncoder(w) 324 enc.SetEscapeHTML(true) 325 326 loop: // break out of for/select infinite loop 327 for { 328 var report struct { 329 Stream string `json:"stream,omitempty"` 330 Status string `json:"status,omitempty"` 331 Progress struct { 332 Current uint64 `json:"current,omitempty"` 333 Total int64 `json:"total,omitempty"` 334 } `json:"progressDetail,omitempty"` 335 Error string `json:"error,omitempty"` 336 Id string `json:"id,omitempty"` // nolint 337 } 338 select { 339 case e := <-progress: 340 switch e.Event { 341 case types.ProgressEventNewArtifact: 342 report.Status = "Pulling fs layer" 343 case types.ProgressEventRead: 344 report.Status = "Downloading" 345 report.Progress.Current = e.Offset 346 report.Progress.Total = e.Artifact.Size 347 case types.ProgressEventSkipped: 348 report.Status = "Already exists" 349 case types.ProgressEventDone: 350 report.Status = "Download complete" 351 } 352 report.Id = e.Artifact.Digest.Encoded()[0:12] 353 if err := enc.Encode(report); err != nil { 354 logrus.Warnf("Failed to json encode error %q", err.Error()) 355 } 356 flush() 357 case pullRes := <-pullResChan: 358 err := pullRes.err 359 pulledImages := pullRes.images 360 if err != nil { 361 report.Error = err.Error() 362 } else { 363 if len(pulledImages) > 0 { 364 img := pulledImages[0].ID() 365 if utils.IsLibpodRequest(r) { 366 report.Status = "Pull complete" 367 } else { 368 report.Status = "Download complete" 369 } 370 report.Id = img[0:12] 371 } else { 372 report.Error = "internal error: no images pulled" 373 } 374 } 375 if err := enc.Encode(report); err != nil { 376 logrus.Warnf("Failed to json encode error %q", err.Error()) 377 } 378 flush() 379 break loop // break out of for/select infinite loop 380 } 381 } 382 } 383 384 func GetImage(w http.ResponseWriter, r *http.Request) { 385 // 200 no error 386 // 404 no such 387 // 500 internal 388 name := utils.GetName(r) 389 possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, name) 390 if err != nil { 391 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "error normalizing image")) 392 return 393 } 394 395 newImage, err := utils.GetImage(r, possiblyNormalizedName) 396 if err != nil { 397 // Here we need to fiddle with the error message because docker-py is looking for "No 398 // such image" to determine on how to raise the correct exception. 399 errMsg := strings.ReplaceAll(err.Error(), "image not known", "No such image") 400 utils.Error(w, http.StatusNotFound, errors.Errorf("failed to find image %s: %s", name, errMsg)) 401 return 402 } 403 inspect, err := handlers.ImageDataToImageInspect(r.Context(), newImage) 404 if err != nil { 405 utils.Error(w, http.StatusInternalServerError, errors.Wrapf(err, "failed to convert ImageData to ImageInspect '%s'", inspect.ID)) 406 return 407 } 408 utils.WriteResponse(w, http.StatusOK, inspect) 409 } 410 411 func GetImages(w http.ResponseWriter, r *http.Request) { 412 decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) 413 runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) 414 query := struct { 415 All bool 416 Digests bool 417 Filter string // Docker 1.24 compatibility 418 }{ 419 // This is where you can override the golang default value for one of fields 420 } 421 422 if err := decoder.Decode(&query, r.URL.Query()); err != nil { 423 utils.Error(w, http.StatusBadRequest, 424 errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) 425 return 426 } 427 if _, found := r.URL.Query()["digests"]; found && query.Digests { 428 utils.UnSupportedParameter("digests") 429 return 430 } 431 432 filterList, err := filters.FiltersFromRequest(r) 433 if err != nil { 434 utils.Error(w, http.StatusInternalServerError, err) 435 return 436 } 437 if !utils.IsLibpodRequest(r) { 438 if len(query.Filter) > 0 { // Docker 1.24 compatibility 439 filterList = append(filterList, "reference="+query.Filter) 440 } 441 filterList = append(filterList, "manifest=false") 442 } 443 444 imageEngine := abi.ImageEngine{Libpod: runtime} 445 446 listOptions := entities.ImageListOptions{All: query.All, Filter: filterList} 447 summaries, err := imageEngine.List(r.Context(), listOptions) 448 if err != nil { 449 utils.Error(w, http.StatusInternalServerError, err) 450 return 451 } 452 453 if !utils.IsLibpodRequest(r) { 454 // docker adds sha256: in front of the ID 455 for _, s := range summaries { 456 s.ID = "sha256:" + s.ID 457 } 458 } 459 utils.WriteResponse(w, http.StatusOK, summaries) 460 } 461 462 func LoadImages(w http.ResponseWriter, r *http.Request) { 463 decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) 464 runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) 465 466 query := struct { 467 Changes map[string]string `json:"changes"` // Ignored 468 Message string `json:"message"` // Ignored 469 Quiet bool `json:"quiet"` // Ignored 470 }{ 471 // This is where you can override the golang default value for one of fields 472 } 473 474 if err := decoder.Decode(&query, r.URL.Query()); err != nil { 475 utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) 476 return 477 } 478 479 // First write the body to a temporary file that we can later attempt 480 // to load. 481 f, err := ioutil.TempFile("", "api_load.tar") 482 if err != nil { 483 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to create tempfile")) 484 return 485 } 486 defer func() { 487 err := os.Remove(f.Name()) 488 if err != nil { 489 logrus.Errorf("Failed to remove temporary file: %v.", err) 490 } 491 }() 492 if err := SaveFromBody(f, r); err != nil { 493 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to write temporary file")) 494 return 495 } 496 497 imageEngine := abi.ImageEngine{Libpod: runtime} 498 499 loadOptions := entities.ImageLoadOptions{Input: f.Name()} 500 loadReport, err := imageEngine.Load(r.Context(), loadOptions) 501 if err != nil { 502 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to load image")) 503 return 504 } 505 506 if len(loadReport.Names) < 1 { 507 utils.Error(w, http.StatusInternalServerError, errors.Errorf("one or more images are required")) 508 return 509 } 510 511 utils.WriteResponse(w, http.StatusOK, struct { 512 Stream string `json:"stream"` 513 }{ 514 Stream: fmt.Sprintf("Loaded image: %s", strings.Join(loadReport.Names, ",")), 515 }) 516 } 517 518 func ExportImages(w http.ResponseWriter, r *http.Request) { 519 // 200 OK 520 // 500 Error 521 decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) 522 runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) 523 524 query := struct { 525 Names []string `schema:"names"` 526 }{ 527 // This is where you can override the golang default value for one of fields 528 } 529 if err := decoder.Decode(&query, r.URL.Query()); err != nil { 530 utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) 531 return 532 } 533 if len(query.Names) == 0 { 534 utils.Error(w, http.StatusBadRequest, fmt.Errorf("no images to download")) 535 return 536 } 537 538 images := make([]string, len(query.Names)) 539 for i, img := range query.Names { 540 possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, img) 541 if err != nil { 542 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "error normalizing image")) 543 return 544 } 545 images[i] = possiblyNormalizedName 546 } 547 548 tmpfile, err := ioutil.TempFile("", "api.tar") 549 if err != nil { 550 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) 551 return 552 } 553 defer os.Remove(tmpfile.Name()) 554 if err := tmpfile.Close(); err != nil { 555 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile")) 556 return 557 } 558 559 imageEngine := abi.ImageEngine{Libpod: runtime} 560 561 saveOptions := entities.ImageSaveOptions{Format: "docker-archive", Output: tmpfile.Name(), MultiImageArchive: true} 562 if err := imageEngine.Save(r.Context(), images[0], images[1:], saveOptions); err != nil { 563 utils.InternalServerError(w, err) 564 return 565 } 566 567 rdr, err := os.Open(tmpfile.Name()) 568 if err != nil { 569 utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to read the exported tarfile")) 570 return 571 } 572 defer rdr.Close() 573 utils.WriteResponse(w, http.StatusOK, rdr) 574 }