How to find the creation date of an image in a (private) Docker registry (API v2)?
Asked Answered
T

6

21

I would like to find out the latest timestamp for an image in a private Docker registry using the v2 API without first pulling the image to my local host.

Thereupon answered 16/9, 2015 at 10:6 Comment(0)
T
36

So after some hacking around, I got the following to work using the curl and jq tools:

curl -X GET http://registry:5000/v2/<IMAGE>/manifests/<TAG> \
    | jq -r '.history[].v1Compatibility' \
    | jq '.created' \
    | sort \
    | tail -n1

This seems to work but I don't really know how the v1 Compatibility representation is to be interpreted so I don't know if I am really getting the correct number out of this.

Comments on this are welcome!

Thereupon answered 16/9, 2015 at 10:6 Comment(6)
You have to accept your answer, I googled the whole day today and didn't find a better approach :(Aberdeen
Thanks! As an excuse to learn more jq, I condensed the parsing into a single command: jq -r '[.history[]]|map(.v1Compatibility|fromjson|.created)|sort|.[0]Gilles
Oops, forgot the reverse: jq -r '[.history[]]|map(.v1Compatibility|fromjson|.created)|sort|reverse|.[0]Gilles
Doing some checking on this, isn't always the fist Object in the "history" the latest one?? I'm getting the same results using jq -r '[.history[0]]|map(.v1Compatibility|fromjson|.created)Lenna
If your manifest schema version is v2, you may not see the history and v1Compatibility fields. If this is the case, you may need to force schema v1 using an Accept header. So your command would then be curl -X GET -H 'Accept: application/vnd.docker.distribution.manifest.v1+json' ...Nannie
it doesn't work for me: hub.docker.com/v2/redis/manifests/latest - 404 - what is the problem?Breughel
C
6

With the V2 image manifest, schema version 2, the response from http://registry:5000/v2/<IMAGE>/manifests/<TAG> does not contain the 'history' field, and I haven't found a way to retrieve the image creation date in one request. However one can use another request to obtain it, based on the image manifest config's digest.

First, get the config's digest (notice the header which specifies the schema version):

digest=$(curl -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
 http://registry:5000/v2/<IMAGE>/manifests/<TAG> \
 | jq -r '.config.digest')

The value of digest will be something similar to sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736.

Then, get the blob of the object identified by the digest, i.e. the configuration object for a container, and retrieve the creation date from there:

curl -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
  http://registry:5000/v2/<IMAGE>/blobs/$digest | jq -r '.created'

This should produce:

2021-10-27T12:18:24.105617451Z

Bonus: Getting the creation dates using skopeo

I'm not sure if the above is the only or the best way, but by the way, it is apparently more or less what skopeo does under the hood when one runs:

skopeo inspect docker://registry:5000/<IMAGE>:<TAG>

The above command returns information about the image tag, among them the creation date:

{
  "Name": "registry:5000/<IMAGE>",
  "Digest": "sha256:655721ff613ee766a4126cb5e0d5ae81598e1b0c3bcf7017c36c4d72cb092fe9",
  "RepoTags": [...],
  "Created": "2021-10-27T12:18:24.105617451Z",
  ...
}
Caslon answered 11/11, 2021 at 17:11 Comment(0)
L
3

A minor improvement to the answer from @snth : print the date more user friendly by using date:

date --date=$(curl -s -X GET http://$REGISTRY:5000/v2/$IMAGE/manifests/$TAG | \
  jq -r '.history[].v1Compatibility' | jq -r '.created' | sort | tail -n 1 )

Prints something like:

Fr 12. Okt 15:26:03 CEST 2018
Ludlow answered 15/10, 2018 at 12:26 Comment(2)
Thanks, the date helps! To make the others' life a bit easier - inside $(...), it's like the command in accepted answer, but the "jq -r '.created'" bit has '-r' flagCarpal
This should really be a comment, not an answer.Allmon
H
3

Compiling the comments and answers above here is a complete solution

With Authentication

# Setup variables to make this more usalbe
USERNAME=docker_user
PASSWORD=docker_pass
DOCKER_REGISTRY=http://my-registry.myorg.org:9080
REPO=myrepo
TAG=atag

# Query Registry and pipe results to jq

DOCKER_DATE=$(curl -s -u $USERNAME:$PASSWORD -H 'Accept: application/vnd.docker.distribution.manifest.v1+json' -X GET http://$REGISTRY_URL/v2/circle-lb/manifests/master | jq -r '[.history[]]|map(.v1Compatibility|fromjson|.created)|sort|reverse|.[0]')
echo "Date for $REPO:$TAG is $DOCKER_DATE"

Without Authentication

DOCKER_DATE=$(curl -s -H 'Accept: application/vnd.docker.distribution.manifest.v1+json' -X GET http://$REGISTRY_URL/v2/circle-lb/manifests/master | jq -r '[.history[]]|map(.v1Compatibility|fromjson|.created)|sort|reverse|.[0]')

And lastly if you want to parse with the date command (gnu date)

on linux:

date -d $DOCKER_DATE

on mac:

gdate -d $DOCKER_DATE
Hollyanne answered 21/5, 2020 at 20:38 Comment(0)
L
0

Here is dotnet core (C#) implementation in case anyone is interested:

    public class DockerHub{
    public DockerRegistryToken GetRegistryToken(string image){
        Console.WriteLine("authenticateing with dockerhub");
        using HttpClient client = new ();   
        var url = string.Format($"https://auth.docker.io/token?service=registry.docker.io&scope=repository:{image}:pull");
        //var response = client.Send(new HttpRequestMessage(HttpMethod.Get, url));
        var result = client.GetStringAsync(url);            
        var drt = JsonSerializer.Deserialize<DockerRegistryToken>(result.Result);        
        return drt;
    }

    public DateTime GetRemoteImageDate(string image, string tag, DockerRegistryToken token){

        using HttpClient client = new ();           
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.docker.distribution.manifest.list.v2+json"));
        
    
        var manifestResult = client.GetStringAsync(string.Format($"https://registry-1.docker.io/v2/{image}/manifests/{tag}"));  
        var json = JsonDocument.Parse(manifestResult.Result);

        var created = json.RootElement.GetProperty("history").EnumerateArray()
                    .Select(m => JsonDocument.Parse(m.GetProperty("v1Compatibility").ToString()).RootElement.GetProperty("created"))
                    .Select(m => DateTime.Parse(m.GetString()))
                    .OrderByDescending(m => m)
                    .FirstOrDefault(); // I challange you to improve this
        Console.WriteLine("Date recieved: {0}",created);
        return created.ToUniversalTime();

    }   
}

public class DockerRegistryToken{
    [JsonPropertyName("token")]
    public string Token { get; set; }

    /// always null
    [JsonPropertyName("access_token")]
    public string AccessToken  {get; set; }

    [JsonPropertyName("expires_in")]
    public int ExpiresInSeconds { get; set; }

    [JsonPropertyName("issued_at")]
    public DateTime IssuedAt { get; set; }

}

To use

        var client = new DockerHub();
        var tok = client.GetRegistryToken("your/reponame");
        var remoteImageDate = client.GetRemoteImageDate("your/reponame","latest",tok);
Larimer answered 5/5, 2021 at 23:11 Comment(0)
C
0

Some idea for C#:

using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;

namespace DockerApi;

public class Manifests
{
    [JsonPropertyName("schemaVersion")]
    public long SchemaVersion { get; set; }

    [JsonPropertyName("name")]
    public string Name { get; set; }

    [JsonPropertyName("tag")]
    public string Tag { get; set; }

    [JsonPropertyName("architecture")]
    public string Architecture { get; set; }

    [JsonPropertyName("fsLayers")]
    public FsLayer[] FsLayers { get; set; }

    [JsonPropertyName("history")]
    public History[] History { get; set; }

    [JsonPropertyName("signatures")]
    public Signature[] Signatures { get; set; }

    public V1Compatibility V1Compatibility
    {
        get
        {
            var jsonNode = JsonNode.Parse(History.Where(m => m.V1Compatibility.Contains("architecture")).First().V1Compatibility);
            return JsonSerializer.Deserialize<V1Compatibility>(jsonNode);
        }
    }
}
public partial class FsLayer
{
    [JsonPropertyName("blobSum")]
    public string BlobSum { get; set; }
}

public partial class History
{
    [JsonPropertyName("v1Compatibility")]
    public string V1Compatibility { get; set; }
}

public partial class Signature
{
    [JsonPropertyName("header")]
    public Header Header { get; set; }

    [JsonPropertyName("signature")]
    public string SignatureSignature { get; set; }

    [JsonPropertyName("protected")]
    public string Protected { get; set; }
}

public partial class Header
{
    [JsonPropertyName("jwk")]
    public Jwk Jwk { get; set; }

    [JsonPropertyName("alg")]
    public string Alg { get; set; }
}

public partial class Jwk
{
    [JsonPropertyName("crv")]
    public string Crv { get; set; }

    [JsonPropertyName("kid")]
    public string Kid { get; set; }

    [JsonPropertyName("kty")]
    public string Kty { get; set; }

    [JsonPropertyName("x")]
    public string X { get; set; }

    [JsonPropertyName("y")]
    public string Y { get; set; }
}
public partial class V1Compatibility
{
    [JsonPropertyName("architecture")]
    public string Architecture { get; set; }

    [JsonPropertyName("config")]
    public Config Config { get; set; }

    [JsonPropertyName("created")]
    public DateTimeOffset Created { get; set; }

    [JsonPropertyName("id")]
    public string Id { get; set; }

    [JsonPropertyName("os")]
    public string Os { get; set; }

    [JsonPropertyName("parent")]
    public string Parent { get; set; }

    [JsonPropertyName("throwaway")]
    public bool Throwaway { get; set; }
}

public partial class Config
{
    [JsonPropertyName("Env")]
    public string[] Env { get; set; }

    [JsonPropertyName("Cmd")]
    public string[] Cmd { get; set; }

    [JsonPropertyName("WorkingDir")]
    public string WorkingDir { get; set; }

    [JsonPropertyName("ArgsEscaped")]
    public bool ArgsEscaped { get; set; }

    [JsonPropertyName("OnBuild")]
    public object OnBuild { get; set; }
}
Constabulary answered 17/2, 2022 at 3:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.