]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/import/import-dkr.c
treewide: fix multiple typos
[thirdparty/systemd.git] / src / import / import-dkr.c
1 /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
2
3 /***
4 This file is part of systemd.
5
6 Copyright 2014 Lennart Poettering
7
8 systemd is free software; you can redistribute it and/or modify it
9 under the terms of the GNU Lesser General Public License as published by
10 the Free Software Foundation; either version 2.1 of the License, or
11 (at your option) any later version.
12
13 systemd is distributed in the hope that it will be useful, but
14 WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
17
18 You should have received a copy of the GNU Lesser General Public License
19 along with systemd; If not, see <http://www.gnu.org/licenses/>.
20 ***/
21
22 #include <curl/curl.h>
23 #include <sys/prctl.h>
24
25 #include "sd-daemon.h"
26 #include "json.h"
27 #include "strv.h"
28 #include "btrfs-util.h"
29 #include "utf8.h"
30 #include "mkdir.h"
31 #include "import-util.h"
32 #include "curl-util.h"
33 #include "aufs-util.h"
34 #include "import-job.h"
35 #include "import-common.h"
36 #include "import-dkr.h"
37
38 typedef enum DkrProgress {
39 DKR_SEARCHING,
40 DKR_RESOLVING,
41 DKR_METADATA,
42 DKR_DOWNLOADING,
43 DKR_COPYING,
44 } DkrProgress;
45
46 struct DkrImport {
47 sd_event *event;
48 CurlGlue *glue;
49
50 char *index_url;
51 char *image_root;
52
53 ImportJob *images_job;
54 ImportJob *tags_job;
55 ImportJob *ancestry_job;
56 ImportJob *json_job;
57 ImportJob *layer_job;
58
59 char *name;
60 char *tag;
61 char *id;
62
63 char *response_token;
64 char **response_registries;
65
66 char **ancestry;
67 unsigned n_ancestry;
68 unsigned current_ancestry;
69
70 DkrImportFinished on_finished;
71 void *userdata;
72
73 char *local;
74 bool force_local;
75
76 char *temp_path;
77 char *final_path;
78
79 pid_t tar_pid;
80 };
81
82 #define PROTOCOL_PREFIX "https://"
83
84 #define HEADER_TOKEN "X-Do" /* the HTTP header for the auth token */ "cker-Token:"
85 #define HEADER_REGISTRY "X-Do" /*the HTTP header for the registry */ "cker-Endpoints:"
86
87 #define LAYERS_MAX 2048
88
89 static void dkr_import_job_on_finished(ImportJob *j);
90
91 DkrImport* dkr_import_unref(DkrImport *i) {
92 if (!i)
93 return NULL;
94
95 if (i->tar_pid > 1) {
96 (void) kill_and_sigcont(i->tar_pid, SIGKILL);
97 (void) wait_for_terminate(i->tar_pid, NULL);
98 }
99
100 import_job_unref(i->images_job);
101 import_job_unref(i->tags_job);
102 import_job_unref(i->ancestry_job);
103 import_job_unref(i->json_job);
104 import_job_unref(i->layer_job);
105
106 curl_glue_unref(i->glue);
107 sd_event_unref(i->event);
108
109 if (i->temp_path) {
110 (void) btrfs_subvol_remove(i->temp_path);
111 (void) rm_rf_dangerous(i->temp_path, false, true, false);
112 free(i->temp_path);
113 }
114
115 free(i->name);
116 free(i->tag);
117 free(i->id);
118 free(i->response_token);
119 free(i->response_registries);
120 strv_free(i->ancestry);
121 free(i->final_path);
122 free(i->index_url);
123 free(i->image_root);
124 free(i->local);
125 free(i);
126
127 return NULL;
128 }
129
130 int dkr_import_new(
131 DkrImport **ret,
132 sd_event *event,
133 const char *index_url,
134 const char *image_root,
135 DkrImportFinished on_finished,
136 void *userdata) {
137
138 _cleanup_(dkr_import_unrefp) DkrImport *i = NULL;
139 char *e;
140 int r;
141
142 assert(ret);
143 assert(index_url);
144
145 if (!http_url_is_valid(index_url))
146 return -EINVAL;
147
148 i = new0(DkrImport, 1);
149 if (!i)
150 return -ENOMEM;
151
152 i->on_finished = on_finished;
153 i->userdata = userdata;
154
155 i->image_root = strdup(image_root ?: "/var/lib/machines");
156 if (!i->image_root)
157 return -ENOMEM;
158
159 i->index_url = strdup(index_url);
160 if (!i->index_url)
161 return -ENOMEM;
162
163 e = endswith(i->index_url, "/");
164 if (e)
165 *e = 0;
166
167 if (event)
168 i->event = sd_event_ref(event);
169 else {
170 r = sd_event_default(&i->event);
171 if (r < 0)
172 return r;
173 }
174
175 r = curl_glue_new(&i->glue, i->event);
176 if (r < 0)
177 return r;
178
179 i->glue->on_finished = import_job_curl_on_finished;
180 i->glue->userdata = i;
181
182 *ret = i;
183 i = NULL;
184
185 return 0;
186 }
187
188 static void dkr_import_report_progress(DkrImport *i, DkrProgress p) {
189 unsigned percent;
190
191 assert(i);
192
193 switch (p) {
194
195 case DKR_SEARCHING:
196 percent = 0;
197 if (i->images_job)
198 percent += i->images_job->progress_percent * 5 / 100;
199 break;
200
201 case DKR_RESOLVING:
202 percent = 5;
203 if (i->tags_job)
204 percent += i->tags_job->progress_percent * 5 / 100;
205 break;
206
207 case DKR_METADATA:
208 percent = 10;
209 if (i->ancestry_job)
210 percent += i->ancestry_job->progress_percent * 5 / 100;
211 if (i->json_job)
212 percent += i->json_job->progress_percent * 5 / 100;
213 break;
214
215 case DKR_DOWNLOADING:
216 percent = 20;
217 percent += 75 * i->current_ancestry / MAX(1U, i->n_ancestry);
218 if (i->layer_job)
219 percent += i->layer_job->progress_percent * 75 / MAX(1U, i->n_ancestry) / 100;
220
221 break;
222
223 case DKR_COPYING:
224 percent = 95;
225 break;
226
227 default:
228 assert_not_reached("Unknown progress state");
229 }
230
231 sd_notifyf(false, "X_IMPORT_PROGRESS=%u", percent);
232 log_debug("Combined progress %u%%", percent);
233 }
234
235 static int parse_id(const void *payload, size_t size, char **ret) {
236 _cleanup_free_ char *buf = NULL, *id = NULL, *other = NULL;
237 union json_value v = {};
238 void *json_state = NULL;
239 const char *p;
240 int t;
241
242 assert(payload);
243 assert(ret);
244
245 if (size <= 0)
246 return -EBADMSG;
247
248 if (memchr(payload, 0, size))
249 return -EBADMSG;
250
251 buf = strndup(payload, size);
252 if (!buf)
253 return -ENOMEM;
254
255 p = buf;
256 t = json_tokenize(&p, &id, &v, &json_state, NULL);
257 if (t < 0)
258 return t;
259 if (t != JSON_STRING)
260 return -EBADMSG;
261
262 t = json_tokenize(&p, &other, &v, &json_state, NULL);
263 if (t < 0)
264 return t;
265 if (t != JSON_END)
266 return -EBADMSG;
267
268 if (!dkr_id_is_valid(id))
269 return -EBADMSG;
270
271 *ret = id;
272 id = NULL;
273
274 return 0;
275 }
276
277 static int parse_ancestry(const void *payload, size_t size, char ***ret) {
278 _cleanup_free_ char *buf = NULL;
279 void *json_state = NULL;
280 const char *p;
281 enum {
282 STATE_BEGIN,
283 STATE_ITEM,
284 STATE_COMMA,
285 STATE_END,
286 } state = STATE_BEGIN;
287 _cleanup_strv_free_ char **l = NULL;
288 size_t n = 0, allocated = 0;
289
290 if (size <= 0)
291 return -EBADMSG;
292
293 if (memchr(payload, 0, size))
294 return -EBADMSG;
295
296 buf = strndup(payload, size);
297 if (!buf)
298 return -ENOMEM;
299
300 p = buf;
301 for (;;) {
302 _cleanup_free_ char *str;
303 union json_value v = {};
304 int t;
305
306 t = json_tokenize(&p, &str, &v, &json_state, NULL);
307 if (t < 0)
308 return t;
309
310 switch (state) {
311
312 case STATE_BEGIN:
313 if (t == JSON_ARRAY_OPEN)
314 state = STATE_ITEM;
315 else
316 return -EBADMSG;
317
318 break;
319
320 case STATE_ITEM:
321 if (t == JSON_STRING) {
322 if (!dkr_id_is_valid(str))
323 return -EBADMSG;
324
325 if (n+1 > LAYERS_MAX)
326 return -EFBIG;
327
328 if (!GREEDY_REALLOC(l, allocated, n + 2))
329 return -ENOMEM;
330
331 l[n++] = str;
332 str = NULL;
333 l[n] = NULL;
334
335 state = STATE_COMMA;
336
337 } else if (t == JSON_ARRAY_CLOSE)
338 state = STATE_END;
339 else
340 return -EBADMSG;
341
342 break;
343
344 case STATE_COMMA:
345 if (t == JSON_COMMA)
346 state = STATE_ITEM;
347 else if (t == JSON_ARRAY_CLOSE)
348 state = STATE_END;
349 else
350 return -EBADMSG;
351 break;
352
353 case STATE_END:
354 if (t == JSON_END) {
355
356 if (strv_isempty(l))
357 return -EBADMSG;
358
359 if (!strv_is_uniq(l))
360 return -EBADMSG;
361
362 l = strv_reverse(l);
363
364 *ret = l;
365 l = NULL;
366 return 0;
367 } else
368 return -EBADMSG;
369 }
370
371 }
372 }
373
374 static const char *dkr_import_current_layer(DkrImport *i) {
375 assert(i);
376
377 if (strv_isempty(i->ancestry))
378 return NULL;
379
380 return i->ancestry[i->current_ancestry];
381 }
382
383 static const char *dkr_import_current_base_layer(DkrImport *i) {
384 assert(i);
385
386 if (strv_isempty(i->ancestry))
387 return NULL;
388
389 if (i->current_ancestry <= 0)
390 return NULL;
391
392 return i->ancestry[i->current_ancestry-1];
393 }
394
395 static int dkr_import_add_token(DkrImport *i, ImportJob *j) {
396 const char *t;
397
398 assert(i);
399 assert(j);
400
401 if (i->response_token)
402 t = strappenda("Authorization: Token ", i->response_token);
403 else
404 t = HEADER_TOKEN " true";
405
406 j->request_header = curl_slist_new("Accept: application/json", t, NULL);
407 if (!j->request_header)
408 return -ENOMEM;
409
410 return 0;
411 }
412
413 static bool dkr_import_is_done(DkrImport *i) {
414 assert(i);
415 assert(i->images_job);
416
417 if (i->images_job->state != IMPORT_JOB_DONE)
418 return false;
419
420 if (!i->tags_job || i->tags_job->state != IMPORT_JOB_DONE)
421 return false;
422
423 if (!i->ancestry_job || i->ancestry_job->state != IMPORT_JOB_DONE)
424 return false;
425
426 if (!i->json_job || i->json_job->state != IMPORT_JOB_DONE)
427 return false;
428
429 if (i->layer_job && i->layer_job->state != IMPORT_JOB_DONE)
430 return false;
431
432 if (dkr_import_current_layer(i))
433 return false;
434
435 return true;
436 }
437
438 static int dkr_import_make_local_copy(DkrImport *i) {
439 int r;
440
441 assert(i);
442
443 if (!i->local)
444 return 0;
445
446 if (!i->final_path) {
447 i->final_path = strjoin(i->image_root, "/.dkr-", i->id, NULL);
448 if (!i->final_path)
449 return log_oom();
450 }
451
452 r = import_make_local_copy(i->final_path, i->image_root, i->local, i->force_local);
453 if (r < 0)
454 return r;
455
456 return 0;
457 }
458
459 static int dkr_import_job_on_open_disk(ImportJob *j) {
460 const char *base;
461 DkrImport *i;
462 int r;
463
464 assert(j);
465 assert(j->userdata);
466
467 i = j->userdata;
468 assert(i->layer_job == j);
469 assert(i->final_path);
470 assert(!i->temp_path);
471 assert(i->tar_pid <= 0);
472
473 r = tempfn_random(i->final_path, &i->temp_path);
474 if (r < 0)
475 return log_oom();
476
477 mkdir_parents_label(i->temp_path, 0700);
478
479 base = dkr_import_current_base_layer(i);
480 if (base) {
481 const char *base_path;
482
483 base_path = strappenda(i->image_root, "/.dkr-", base);
484 r = btrfs_subvol_snapshot(base_path, i->temp_path, false, true);
485 } else
486 r = btrfs_subvol_make(i->temp_path);
487 if (r < 0)
488 return log_error_errno(r, "Failed to make btrfs subvolume %s: %m", i->temp_path);
489
490 j->disk_fd = import_fork_tar(i->temp_path, &i->tar_pid);
491 if (j->disk_fd < 0)
492 return j->disk_fd;
493
494 return 0;
495 }
496
497 static void dkr_import_job_on_progress(ImportJob *j) {
498 DkrImport *i;
499
500 assert(j);
501 assert(j->userdata);
502
503 i = j->userdata;
504
505 dkr_import_report_progress(
506 i,
507 j == i->images_job ? DKR_SEARCHING :
508 j == i->tags_job ? DKR_RESOLVING :
509 j == i->ancestry_job || j == i->json_job ? DKR_METADATA :
510 DKR_DOWNLOADING);
511 }
512
513 static int dkr_import_pull_layer(DkrImport *i) {
514 _cleanup_free_ char *path = NULL;
515 const char *url, *layer = NULL;
516 int r;
517
518 assert(i);
519 assert(!i->layer_job);
520 assert(!i->temp_path);
521 assert(!i->final_path);
522
523 for (;;) {
524 layer = dkr_import_current_layer(i);
525 if (!layer)
526 return 0; /* no more layers */
527
528 path = strjoin(i->image_root, "/.dkr-", layer, NULL);
529 if (!path)
530 return log_oom();
531
532 if (laccess(path, F_OK) < 0) {
533 if (errno == ENOENT)
534 break;
535
536 return log_error_errno(errno, "Failed to check for container: %m");
537 }
538
539 log_info("Layer %s already exists, skipping.", layer);
540
541 i->current_ancestry++;
542
543 free(path);
544 path = NULL;
545 }
546
547 log_info("Pulling layer %s...", layer);
548
549 i->final_path = path;
550 path = NULL;
551
552 url = strappenda(PROTOCOL_PREFIX, i->response_registries[0], "/v1/images/", layer, "/layer");
553 r = import_job_new(&i->layer_job, url, i->glue, i);
554 if (r < 0)
555 return log_error_errno(r, "Failed to allocate layer job: %m");
556
557 r = dkr_import_add_token(i, i->layer_job);
558 if (r < 0)
559 return log_oom();
560
561 i->layer_job->on_finished = dkr_import_job_on_finished;
562 i->layer_job->on_open_disk = dkr_import_job_on_open_disk;
563 i->layer_job->on_progress = dkr_import_job_on_progress;
564
565 r = import_job_begin(i->layer_job);
566 if (r < 0)
567 return log_error_errno(r, "Failed to start layer job: %m");
568
569 return 0;
570 }
571
572 static void dkr_import_job_on_finished(ImportJob *j) {
573 DkrImport *i;
574 int r;
575
576 assert(j);
577 assert(j->userdata);
578
579 i = j->userdata;
580 if (j->error != 0) {
581 if (j == i->images_job)
582 log_error_errno(j->error, "Failed to retrieve images list. (Wrong index URL?)");
583 else if (j == i->tags_job)
584 log_error_errno(j->error, "Failed to retrieve tags list.");
585 else if (j == i->ancestry_job)
586 log_error_errno(j->error, "Failed to retrieve ancestry list.");
587 else if (j == i->json_job)
588 log_error_errno(j->error, "Failed to retrieve json data.");
589 else
590 log_error_errno(j->error, "Failed to retrieve layer data.");
591
592 r = j->error;
593 goto finish;
594 }
595
596 if (i->images_job == j) {
597 const char *url;
598
599 assert(!i->tags_job);
600 assert(!i->ancestry_job);
601 assert(!i->json_job);
602 assert(!i->layer_job);
603
604 if (strv_isempty(i->response_registries)) {
605 r = -EBADMSG;
606 log_error("Didn't get registry information.");
607 goto finish;
608 }
609
610 log_info("Index lookup succeeded, directed to registry %s.", i->response_registries[0]);
611 dkr_import_report_progress(i, DKR_RESOLVING);
612
613 url = strappenda(PROTOCOL_PREFIX, i->response_registries[0], "/v1/repositories/", i->name, "/tags/", i->tag);
614 r = import_job_new(&i->tags_job, url, i->glue, i);
615 if (r < 0) {
616 log_error_errno(r, "Failed to allocate tags job: %m");
617 goto finish;
618 }
619
620 r = dkr_import_add_token(i, i->tags_job);
621 if (r < 0) {
622 log_oom();
623 goto finish;
624 }
625
626 i->tags_job->on_finished = dkr_import_job_on_finished;
627 i->tags_job->on_progress = dkr_import_job_on_progress;
628
629 r = import_job_begin(i->tags_job);
630 if (r < 0) {
631 log_error_errno(r, "Failed to start tags job: %m");
632 goto finish;
633 }
634
635 } else if (i->tags_job == j) {
636 const char *url;
637 char *id = NULL;
638
639 assert(!i->ancestry_job);
640 assert(!i->json_job);
641 assert(!i->layer_job);
642
643 r = parse_id(j->payload, j->payload_size, &id);
644 if (r < 0) {
645 log_error_errno(r, "Failed to parse JSON id.");
646 goto finish;
647 }
648
649 free(i->id);
650 i->id = id;
651
652 log_info("Tag lookup succeeded, resolved to layer %s.", i->id);
653 dkr_import_report_progress(i, DKR_METADATA);
654
655 url = strappenda(PROTOCOL_PREFIX, i->response_registries[0], "/v1/images/", i->id, "/ancestry");
656 r = import_job_new(&i->ancestry_job, url, i->glue, i);
657 if (r < 0) {
658 log_error_errno(r, "Failed to allocate ancestry job: %m");
659 goto finish;
660 }
661
662 r = dkr_import_add_token(i, i->ancestry_job);
663 if (r < 0) {
664 log_oom();
665 goto finish;
666 }
667
668 i->ancestry_job->on_finished = dkr_import_job_on_finished;
669 i->ancestry_job->on_progress = dkr_import_job_on_progress;
670
671 url = strappenda(PROTOCOL_PREFIX, i->response_registries[0], "/v1/images/", i->id, "/json");
672 r = import_job_new(&i->json_job, url, i->glue, i);
673 if (r < 0) {
674 log_error_errno(r, "Failed to allocate json job: %m");
675 goto finish;
676 }
677
678 r = dkr_import_add_token(i, i->json_job);
679 if (r < 0) {
680 log_oom();
681 goto finish;
682 }
683
684 i->json_job->on_finished = dkr_import_job_on_finished;
685 i->json_job->on_progress = dkr_import_job_on_progress;
686
687 r = import_job_begin(i->ancestry_job);
688 if (r < 0) {
689 log_error_errno(r, "Failed to start ancestry job: %m");
690 goto finish;
691 }
692
693 r = import_job_begin(i->json_job);
694 if (r < 0) {
695 log_error_errno(r, "Failed to start json job: %m");
696 goto finish;
697 }
698
699 } else if (i->ancestry_job == j) {
700 char **ancestry = NULL, **k;
701 unsigned n;
702
703 assert(!i->layer_job);
704
705 r = parse_ancestry(j->payload, j->payload_size, &ancestry);
706 if (r < 0) {
707 log_error_errno(r, "Failed to parse JSON id.");
708 goto finish;
709 }
710
711 n = strv_length(ancestry);
712 if (n <= 0 || !streq(ancestry[n-1], i->id)) {
713 log_error("Ancestry doesn't end in main layer.");
714 strv_free(ancestry);
715 r = -EBADMSG;
716 goto finish;
717 }
718
719 log_info("Ancestor lookup succeeded, requires layers:\n");
720 STRV_FOREACH(k, ancestry)
721 log_info("\t%s", *k);
722
723 strv_free(i->ancestry);
724 i->ancestry = ancestry;
725 i->n_ancestry = n;
726 i->current_ancestry = 0;
727
728 dkr_import_report_progress(i, DKR_DOWNLOADING);
729
730 r = dkr_import_pull_layer(i);
731 if (r < 0)
732 goto finish;
733
734 } else if (i->layer_job == j) {
735 assert(i->temp_path);
736 assert(i->final_path);
737
738 j->disk_fd = safe_close(j->disk_fd);
739
740 if (i->tar_pid > 0) {
741 r = wait_for_terminate_and_warn("tar", i->tar_pid, true);
742 i->tar_pid = 0;
743 if (r < 0)
744 goto finish;
745 }
746
747 r = aufs_resolve(i->temp_path);
748 if (r < 0) {
749 log_error_errno(r, "Failed to resolve aufs whiteouts: %m");
750 goto finish;
751 }
752
753 r = btrfs_subvol_set_read_only(i->temp_path, true);
754 if (r < 0) {
755 log_error_errno(r, "Failed to mark snapshot read-only: %m");
756 goto finish;
757 }
758
759 if (rename(i->temp_path, i->final_path) < 0) {
760 log_error_errno(errno, "Failed to rename snaphsot: %m");
761 goto finish;
762 }
763
764 log_info("Completed writing to layer %s.", i->final_path);
765
766 i->layer_job = import_job_unref(i->layer_job);
767 free(i->temp_path);
768 i->temp_path = NULL;
769 free(i->final_path);
770 i->final_path = NULL;
771
772 i->current_ancestry ++;
773 r = dkr_import_pull_layer(i);
774 if (r < 0)
775 goto finish;
776
777 } else if (i->json_job != j)
778 assert_not_reached("Got finished event for unknown curl object");
779
780 if (!dkr_import_is_done(i))
781 return;
782
783 dkr_import_report_progress(i, DKR_COPYING);
784
785 r = dkr_import_make_local_copy(i);
786 if (r < 0)
787 goto finish;
788
789 r = 0;
790
791 finish:
792 if (i->on_finished)
793 i->on_finished(i, r, i->userdata);
794 else
795 sd_event_exit(i->event, r);
796 }
797
798 static int dkr_import_job_on_header(ImportJob *j, const char *header, size_t sz) {
799 _cleanup_free_ char *registry = NULL;
800 char *token;
801 DkrImport *i;
802 int r;
803
804 assert(j);
805 assert(j->userdata);
806
807 i = j->userdata;
808
809 r = curl_header_strdup(header, sz, HEADER_TOKEN, &token);
810 if (r < 0)
811 return log_oom();
812 if (r > 0) {
813 free(i->response_token);
814 i->response_token = token;
815 return 0;
816 }
817
818 r = curl_header_strdup(header, sz, HEADER_REGISTRY, &registry);
819 if (r < 0)
820 return log_oom();
821 if (r > 0) {
822 char **l, **k;
823
824 l = strv_split(registry, ",");
825 if (!l)
826 return log_oom();
827
828 STRV_FOREACH(k, l) {
829 if (!hostname_is_valid(*k)) {
830 log_error("Registry hostname is not valid.");
831 strv_free(l);
832 return -EBADMSG;
833 }
834 }
835
836 strv_free(i->response_registries);
837 i->response_registries = l;
838 }
839
840 return 0;
841 }
842
843 int dkr_import_pull(DkrImport *i, const char *name, const char *tag, const char *local, bool force_local) {
844 const char *url;
845 int r;
846
847 assert(i);
848
849 if (!dkr_name_is_valid(name))
850 return -EINVAL;
851
852 if (tag && !dkr_tag_is_valid(tag))
853 return -EINVAL;
854
855 if (local && !machine_name_is_valid(local))
856 return -EINVAL;
857
858 if (i->images_job)
859 return -EBUSY;
860
861 if (!tag)
862 tag = "latest";
863
864 r = free_and_strdup(&i->local, local);
865 if (r < 0)
866 return r;
867 i->force_local = force_local;
868
869 r = free_and_strdup(&i->name, name);
870 if (r < 0)
871 return r;
872 r = free_and_strdup(&i->tag, tag);
873 if (r < 0)
874 return r;
875
876 url = strappenda(i->index_url, "/v1/repositories/", name, "/images");
877
878 r = import_job_new(&i->images_job, url, i->glue, i);
879 if (r < 0)
880 return r;
881
882 r = dkr_import_add_token(i, i->images_job);
883 if (r < 0)
884 return r;
885
886 i->images_job->on_finished = dkr_import_job_on_finished;
887 i->images_job->on_header = dkr_import_job_on_header;
888 i->images_job->on_progress = dkr_import_job_on_progress;
889
890 return import_job_begin(i->images_job);
891 }