Source file docker_hub.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
type fetch_errors = [
| `Api_error of Http_lwt_client.response * string option
| `Malformed_json of string
| `Msg of string
]
let fmt = Printf.sprintf
let ( >>= ) = Result.bind
let ( >|= ) x f = Lwt.map f x
let rec cmps = function
| [] -> 0
| f::fl ->
match f () with
| 0 -> cmps fl
| n -> n
let hurl ~meth ~ url =
Http_lwt_client.request
~config:(`HTTP_1_1 Httpaf.Config.default)
~meth
~headers
url
(fun _ acc body -> Lwt.return (Some (Option.value ~default:"" acc ^ body)))
None
>|= function
| Ok ({Http_lwt_client.status = `OK; _}, Some body) -> Ok body
| Ok (resp, body) -> Error (`Api_error (resp, body))
| Error e -> Error e
module Json : sig
type t
val parse : string -> (t, [> `Malformed_json of string]) result
val get : string -> t -> (t, [> `Malformed_json of string]) result
val get_string : string -> t -> (string, [> `Malformed_json of string]) result
val get_list : string -> t -> (t list, [> `Malformed_json of string]) result
val get_string_list : string -> t -> (string list, [> `Malformed_json of string]) result
val get_string_opt : string -> t -> string option
val map :
(t -> ('a, [> `Malformed_json of string] as 'b) result) ->
t list ->
('a list, [> `Malformed_json of string] as 'b) result
val pp : Format.formatter -> t -> unit
end = struct
type t = Yojson.Safe.t
let parse json =
match Yojson.Safe.from_string json with
| json -> Ok json
| exception (Yojson.Json_error msg) -> Error (`Malformed_json msg)
let get field = function
| `Assoc l ->
begin match List.find_opt (fun (k, _) -> String.equal k field) l with
| Some (_, v) -> Ok v
| None -> Error (`Malformed_json field)
end
| _ -> Error (`Malformed_json field)
let get_opt field json =
match get field json with
| Ok x -> Some x
| Error _ -> None
let get_string field json =
get field json >>= function
| `String str -> Ok str
| _ -> Error (`Malformed_json field)
let get_string_opt field json =
match get_opt field json with
| Some (`String str) -> Some str
| _ -> None
let get_list field json =
get field json >>= function
| `List l -> Ok l
| _ -> Error (`Malformed_json field)
let map f l =
List.fold_left (fun acc x ->
acc >>= fun acc ->
f x >>= fun x ->
Ok (acc @ [x])
) (Ok []) l
let get_string_list field json =
get field json >>= function
| `List l ->
map (function
| `String x -> Ok x
| _ -> Error (`Malformed_json "not a list of string")
) l
| _ -> Error (`Malformed_json field)
let pp fmt json =
Yojson.Safe.pretty_print fmt json
end
module Platform = struct
type t = {
os : string;
arch : string;
variant : string option;
}
let compare {os; arch; variant} x =
cmps [
(fun () -> String.compare os x.os);
(fun () -> String.compare arch x.arch);
(fun () -> Option.compare String.compare variant x.variant);
]
let equal x y = compare x y = 0
end
module Image = struct
type name = {name : string}
type tag = {tag : string}
type digest = {digest : string}
let fail ~func_name = raise (Invalid_argument func_name)
let make_name ~func_name name =
let aux = function
| 'a'..'z' | '0'..'9' | '.' | '_' | '-' -> true
| _ -> false
in
match String.split_on_char '/' name with
| [""; ""] -> fail ~func_name
| [x; y] when String.for_all aux x && String.for_all aux y -> {name}
| _ -> fail ~func_name
let make_tag ~func_name tag =
let aux = function
| 'a'..'z' | 'A'..'Z' | '0'..'9' | '.' | '_' | '-' -> true
| _ -> false
in
match tag with
| "" -> fail ~func_name
| tag when String.for_all aux tag -> {tag}
| _ -> fail ~func_name
let make_digest ~func_name digest =
let aux = function
| 'a'..'z' | 'A'..'Z' | '0'..'9' | '.' | '_' | '-' | '+' -> true
| _ -> false
in
match String.split_on_char ':' digest with
| [""; ""] -> fail ~func_name
| [x; y] when String.for_all aux x && String.for_all aux y -> {digest}
| _ -> fail ~func_name
let parse ~func_name image =
let parse_name_and_tag image =
let parse_name image =
match String.split_on_char '/' image with
| [_; _] -> make_name ~func_name image
| [_] -> make_name ~func_name ("library/"^image)
| _ -> fail ~func_name
in
match String.split_on_char ':' image with
| [image; tag] -> (parse_name image, make_tag ~func_name tag)
| [image] -> (parse_name image, make_tag ~func_name "latest")
| _ -> fail ~func_name
in
match String.split_on_char '@' image with
| [image; digest] -> (parse_name_and_tag image, Some (make_digest ~func_name digest))
| [image] -> (parse_name_and_tag image, None)
| _ -> fail ~func_name
let from_string image =
let func_name = "Docker_hub.Image.from_string" in
let (name, tag), digest = parse ~func_name image in
(name, tag, digest)
let with_digest image =
let func_name = "Docker_hub.Image.with_digest" in
match parse ~func_name image with
| (_, None) -> raise (Invalid_argument func_name)
| ((name, tag), Some digest) -> (name, tag, digest)
let without_digest image =
let func_name = "Docker_hub.Image.without_digest" in
match parse ~func_name image with
| (_, Some _) -> raise (Invalid_argument func_name)
| ((name, tag), None) -> (name, tag)
let ignore_digest image =
let func_name = "Docker_hub.Image.ignore_digest" in
Stdlib.fst (parse ~func_name image)
let to_string {name} {tag} = function
| None -> fmt "%s:%s" name tag
| Some {digest} -> fmt "%s:%s@%s" name tag digest
let name_to_string {name} = name
let tag_to_string {tag} = tag
let digest_to_string {digest} = digest
end
module Token = struct
type t = {
json : Json.t;
token : string;
name : string;
}
let fetch {Image.name} =
hurl ~meth:`GET
~headers:[]
(fmt "https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull" name)
>|= function
| Ok json ->
Json.parse json >>= fun json ->
Json.get_string "token" json >>= fun token ->
Ok {json; token; name}
| Error e -> Error e
let pp fmt {json; token = _; name = _} =
Json.pp fmt json
end
let check_media_type media_type media_type' =
if String.equal media_type media_type' then
Ok ()
else
Error (`Malformed_json (fmt "mediaType: %s != %s" media_type media_type'))
module Manifest = struct
type t = {
json : Json.t;
config_digest : string;
rootfs_digest : string;
}
let media_type = "application/vnd.docker.distribution.manifest.v2+json"
let config_media_type = "application/vnd.docker.container.image.v1+json"
let rootfs_media_type = "application/vnd.docker.image.rootfs.diff.tar.gzip"
let fetch {Image.digest} {Token.token; name; _} =
hurl ~meth:`GET
~headers:[("Accept", media_type); ("Authorization", fmt "Bearer %s" token)]
(fmt "https://registry-1.docker.io/v2/%s/manifests/%s" name digest)
>|= function
| Ok json ->
Json.parse json >>= fun json ->
begin
Json.get "config" json >>= fun config ->
Json.get_string "mediaType" config >>= fun config_media_type' ->
check_media_type config_media_type config_media_type' >>= fun () ->
Json.get_string "digest" config
end >>= fun config_digest ->
begin
Json.get_list "layers" json >>= function
| [] | _::_::_ -> Error (`Msg "Does not support multiple layers yet")
| [layer] ->
Json.get_string "mediaType" layer >>= fun rootfs_media_type' ->
check_media_type rootfs_media_type rootfs_media_type' >>= fun () ->
Json.get_string "digest" layer
end >>= fun rootfs_digest ->
Ok {json; config_digest; rootfs_digest}
| Error e -> Error e
let pp fmt {json; config_digest = _; rootfs_digest = _} =
Json.pp fmt json
end
let parse_platform json =
Json.get_string "os" json >>= fun os ->
Json.get_string "architecture" json >>= fun arch ->
let variant = Json.get_string_opt "variant" json in
Ok {Platform.os; arch; variant}
module Manifests = struct
type elt = {
platform : Platform.t;
digest : Image.digest;
}
type t = {
json : Json.t;
elements : elt list;
}
let get_elt json =
Json.get_string "digest" json >>= fun digest ->
Json.get "platform" json >>= fun platform ->
parse_platform platform >>= fun platform ->
Ok {platform; digest = {Image.digest}}
let media_type = "application/vnd.docker.distribution.manifest.list.v2+json"
let fetch {Image.tag} {Token.token; name; _} =
hurl ~meth:`GET
~headers:[("Accept", media_type); ("Authorization", fmt "Bearer %s" token)]
(fmt "https://registry-1.docker.io/v2/%s/manifests/%s" name tag)
>|= function
| Ok json ->
Json.parse json >>= fun json ->
Json.get_list "manifests" json >>= fun elements ->
Json.map get_elt elements >>= fun elements ->
Ok {json; elements}
| Error e -> Error e
let elements {elements; _} = elements
let pp fmt {json; elements = _} =
Json.pp fmt json
end
module Config = struct
type t = {
json : Json.t;
env : string list;
platform : Platform.t;
}
let fetch {Manifest.config_digest; _} {Token.token; name; _} =
hurl ~meth:`GET
~headers:[("Accept", Manifest.config_media_type); ("Authorization", fmt "Bearer %s" token)]
(fmt "https://registry-1.docker.io/v2/%s/blobs/%s" name config_digest)
>|= function
| Ok json ->
Json.parse json >>= fun json ->
(Json.get "config" json >>= Json.get_string_list "Env") >>= fun env ->
parse_platform json >>= fun platform ->
Ok {json; env; platform}
| Error e -> Error e
let env {env; _} = env
let platform {platform; _} = platform
let pp fmt {json; env = _; platform = _} =
Json.pp fmt json
end
let fetch_rootfs ~output_file {Manifest.rootfs_digest; _} {Token.token; name; _} =
let ( >>= ) = Lwt.bind in
hurl ~meth:`GET
~headers:[("Accept", Manifest.rootfs_media_type); ("Authorization", fmt "Bearer %s" token)]
(fmt "https://registry-1.docker.io/v2/%s/blobs/%s" name rootfs_digest)
>>= function
| Ok x ->
Lwt_io.with_file ~mode:Lwt_io.Output (Fpath.to_string output_file) begin fun ch ->
Lwt_io.write ch x >|= fun () ->
Ok ()
end
| Error e -> Lwt.return (Error e)