Repository: moby/moby (language: go)

Issue:
"Created" in image inspect response is not a valid date/time if "created" is absent in the image config
### Description

The `created` field in an image config is optional according to the OCI Image Format Specification (https://github.com/opencontainers/image-spec/blob/main/config.md#properties).

The `Created` field in a image inspect api response (`/images/{name}/json`) is a date and time in RFC 3339 format
(https://github.com/moby/moby/blob/master/docs/api/v1.44.yaml#L1742-L1745).

If the `created` field is now absent in an image's config - which is valid as described above - (for example in the `hivemq/hivemq-swarm:4.25.0` image), the `Created` field in the image inspect response is not a valid date and time but an empty string since version 25.x.
- Docker version 24.0.7: `"Created":"0001-01-01T00:00:00Z"`
- Docker version 25.0.3: `"Created":""`

Is this change intentional?
Instead of an empty string, I would have expected the `Created` field to be absent in an image inspect response in this case.
It was unexpected that a docker update results in the testcontainers software breaking with a `DateTimeParseException`: https://github.com/testcontainers/testcontainers-java/pull/8302

Furthermore, the creation time now seems to be more inconsistent between the image list and image inspect api responses.
- Docker version 24.0.7: `-62135596800` == `"0001-01-01T00:00:00Z"`
- Docker version 25.0.3: `0` vs `""`

### Reproduce

1. Docker version 24.0.7:
   1. `docker image inspect hivemq/hivemq-swarm:4.25.0`
      => `"Created": "0001-01-01T00:00:00Z"`
   2. `curl --unix-socket /var/run/docker.sock http://v1.41/images/hivemq/hivemq-swarm:4.25.0/json`
      => `"Created":"0001-01-01T00:00:00Z"`
   3. `docker image list`
      => `CREATED` = `N/A`
   4. `curl --unix-socket /var/run/docker.sock http://v1.41/images/json`
      => `"Created":-62135596800`
3. Docker version 25.0.3:
   1. `docker image inspect hivemq/hivemq-swarm:4.25.0`
      => `"Created": ""`
   2. `curl --unix-socket /var/run/docker.sock http://v1.41/images/hivemq/hivemq-swarm:4.25.0/json`
      => `"Created":""`
   3. `docker image list`
      => `CREATED` = `54 years ago`
   4. `curl --unix-socket /var/run/docker.sock http://v1.41/images/json`
      => `"Created":0`

### Expected behavior

The behavior of the `Created` fields in the api responses should be defined for the case when the `created` field is absent in the image config.
While the previous behavior was not ideal (negative timestamp), it was at least consistent and did not violate the date/time format.

### docker version

```bash
Client: Docker Engine - Community
 Version:           25.0.3
 API version:       1.44
 Go version:        go1.21.6
 Git commit:        4debf41
 Built:             Tue Feb  6 21:13:09 2024
 OS/Arch:           linux/amd64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          25.0.3
  API version:      1.44 (minimum version 1.24)
  Go version:       go1.21.6
  Git commit:       f417435
  Built:            Tue Feb  6 21:13:09 2024
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.28
  GitCommit:        ae07eda36dd25f8a1b98dfbf587313b99c0190bb
 runc:
  Version:          1.1.12
  GitCommit:        v1.1.12-0-g51d5e94
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
```


### docker info

```bash
Client: Docker Engine - Community
 Version:    25.0.3
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.12.1
    Path:     /usr/libexec/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v2.24.5
    Path:     /usr/libexec/docker/cli-plugins/docker-compose
  scan: Docker Scan (Docker Inc.)
    Version:  v0.23.0
    Path:     /usr/libexec/docker/cli-plugins/docker-scan

Server:
 Containers: 0
  Running: 0
  Paused: 0
  Stopped: 0
 Images: 1
 Server Version: 25.0.3
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Using metacopy: false
  Native Overlay Diff: true
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: systemd
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: ae07eda36dd25f8a1b98dfbf587313b99c0190bb
 runc version: v1.1.12-0-g51d5e94
 init version: de40ad0
 Security Options:
  apparmor
  seccomp
   Profile: builtin
  cgroupns
 Kernel Version: 5.15.0-60-generic
 Operating System: Ubuntu 22.04.3 LTS
 OSType: linux
 Architecture: x86_64
 CPUs: 4
 Total Memory: 15.63GiB
 Name: gradle-agent-1677767724950
 ID: 9299a426-4db0-424c-a2e8-4543fd9005ea
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Live Restore Enabled: false
```


### Additional Info

_No response_


Discussion / hints:
I think this may be related to;

- https://github.com/moby/moby/pull/45504

Which had a comment about some of this;

> The most notable change here is that the OCI's type uses a pointer for `Created`, which we probably should've been too, so most of these changes are accounting for that (and embedding our `Equal` implementation in the one single place it was used).

The tricky bit here is that, while some parts may be similar, the image inspect response is _not_ an OCI image struct; it predates the OCI, and it's a struct that was defined for the image inspect response, which showed the "low-level" information as is stored in the Docker Engine's storage for an image; https://github.com/moby/moby/blob/a60546b084eef87910345876e32c3db46e7c3bbc/api/types/types.go#L32-L34

It looks like we should at least update the description to mention that the field may be empty if the associated (OCI) image did not have a `Created` date set https://github.com/moby/moby/blob/a60546b084eef87910345876e32c3db46e7c3bbc/api/types/types.go#L73-L75


We may consider to also make it a pointer and/or omit-empty, but should look if that won't cause compatibility issues on its own 🤔 



cc @tianon @vvoland @corhere 

Hmm, this is a tricky one -- IMO, it's more _correct_ for the API field to be the empty string than to be `0001-01-01T00:00:00Z` in this case (and indeed, omitted entirely might even be more correct).

Maybe we need to add an API version constraint such that API versions older than 25.x's return the older zero `time.Time` value for this case instead?
To be clear, I am not suggesting that `0001-01-01T00:00:00Z` was better and imho the new response would be fine if this case was defined/documented. For example the api spec should say that an empty string should be interpreted as an absent field.
Agreed!  What I had in mind was something like this to address the backwards compat break (and scope it to the 25.x API version):

```diff
diff --git a/api/server/router/image/image_routes.go b/api/server/router/image/image_routes.go
index 4a9f494a3b..f98c8452a1 100644
--- a/api/server/router/image/image_routes.go
+++ b/api/server/router/image/image_routes.go
@@ -299,6 +299,10 @@ func (ir *imageRouter) getImagesByName(ctx context.Context, w http.ResponseWrite
 	version := httputils.VersionFromContext(ctx)
 	if versions.LessThan(version, "1.44") {
 		imageInspect.VirtualSize = imageInspect.Size //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.44.
+
+		if imageInspect.Created == "" {
+			imageInspect.Created = time.Time{}.Format(time.RFC3339Nano)
+		}
 	}
 	return httputils.WriteJSON(w, http.StatusOK, imageInspect)
 }
```

Coupled with an update to the API docs for API 1.44+ to make it clear that empty `Created` means it wasn't set in the original object (which previously couldn't differentiate between unset or `0001-01-01T00:00:00Z` explicitly, but I doubt there are many actual images in the wild with that set, so in practice it's a narrow gain :smile:).

Fix patch:
diff --git a/api/server/router/image/image_routes.go b/api/server/router/image/image_routes.go
index 7921d49575de4..fa5eaf2f6075a 100644
--- a/api/server/router/image/image_routes.go
+++ b/api/server/router/image/image_routes.go
@@ -298,6 +298,12 @@ func (ir *imageRouter) getImagesByName(ctx context.Context, w http.ResponseWrite
 	version := httputils.VersionFromContext(ctx)
 	if versions.LessThan(version, "1.44") {
 		imageInspect.VirtualSize = imageInspect.Size //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.44.
+
+		if imageInspect.Created == "" {
+			// backwards compatibility for Created not existing returning "0001-01-01T00:00:00Z"
+			// https://github.com/moby/moby/issues/47368
+			imageInspect.Created = time.Time{}.Format(time.RFC3339Nano)
+		}
 	}
 	return httputils.WriteJSON(w, http.StatusOK, imageInspect)
 }
diff --git a/api/swagger.yaml b/api/swagger.yaml
index e55a76fc63c38..5e448edad65ef 100644
--- a/api/swagger.yaml
+++ b/api/swagger.yaml
@@ -1742,7 +1742,7 @@ definitions:
       Created:
         description: |
           Date and time at which the image was created, formatted in
-          [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.
+          [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds, or empty if the field was not set in the image config.
         type: "string"
         x-nullable: false
         example: "2022-02-04T21:20:12.497794809Z"
diff --git a/docs/api/v1.44.yaml b/docs/api/v1.44.yaml
index e55a76fc63c38..5e448edad65ef 100644
--- a/docs/api/v1.44.yaml
+++ b/docs/api/v1.44.yaml
@@ -1742,7 +1742,7 @@ definitions:
       Created:
         description: |
           Date and time at which the image was created, formatted in
-          [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.
+          [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds, or empty if the field was not set in the image config.
         type: "string"
         x-nullable: false
         example: "2022-02-04T21:20:12.497794809Z"
diff --git a/docs/api/version-history.md b/docs/api/version-history.md
index 2cc332be2602a..fe04753249537 100644
--- a/docs/api/version-history.md
+++ b/docs/api/version-history.md
@@ -79,6 +79,9 @@ keywords: "API, Docker, rcli, REST, documentation"
   `SecondaryIPv6Addresses` available in `NetworkSettings` when calling `GET /containers/{id}/json` are
   deprecated and will be removed in a future release. You should instead look for the default network in
   `NetworkSettings.Networks`.
+* `GET /images/{id}/json` now responds with an empty `Created` field
+  (previously it was `0001-01-01T00:00:00Z`) if the `Created` field is missing
+  from the image config.
 
 ## v1.43 API changes
 


Test patch: