Repository: ansible/ansible-builder

Issue:
Building the image with Docker Engine 27+ gives some warnings
## Description

Docker Engine 27+ uses new BuildKit 0.14+ to build images by default, and BuildKit 0.14.x has new feature that validates if syntax in Dockerfile meet best practices.

> Default Dockerfile frontend has been updated to v1.8.0
> https://github.com/moby/buildkit/releases/tag/v0.14.0

> Many new validation rules have been added to verify that your Dockerfile is using best practices. These rules are validated during build and new "lint" subrequest can be used to only trigger validation without completing the whole build.
> https://github.com/moby/buildkit/releases/tag/dockerfile/1.8.0

Refer to the `Build checks` section of Docker Docs to see which rules are validated: https://docs.docker.com/reference/build-checks/

The problem is, the `Dockerfile` that generated by current ansible-builder does not follow this rule; `FROM` is uppercased but `as` is lowercased: https://docs.docker.com/reference/build-checks/from-as-casing/

```dockerfile
# Base build stage
FROM $EE_BASE_IMAGE as base
...
# Galaxy build stage
FROM base as galaxy
...
# Builder build stage
FROM base as builder
...
# Final build stage
FROM base as final
```

This will result in a successful build, but some warnings to be displayed:

```bash
$ ansible-builder build --tag registry.example.com/ansible/ee:2.17-minimal --container-runtime docker --verbosity 3
Ansible Builder is generating your execution environment build context.
...
Ansible Builder is building your execution environment image. Tags: registry.example.com/ansible/ee:2.17-minimal
Running command:
  docker build -f context/Dockerfile -t registry.example.com/ansible/ee:2.17-minimal context
#0 building with "default" instance using docker driver

#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 2.96kB done
#1 WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 11) ✅
#1 WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 31) ✅
#1 WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 50) ✅
#1 WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 70) ✅
...

#31 [final 9/9] RUN rm -rf /output
#31 DONE 0.1s

#32 exporting to image
#32 exporting layers
#32 exporting layers 0.6s done
#32 writing image sha256:73227f703dbf35eb822caa325f0db8593fb10719bbb64d21615ea7ddd50df04e done
#32 naming to registry.example.com/ansible/ee:2.17-minimal done
#32 DONE 0.6s

 4 warnings found (use --debug to expand): ✅
 - FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 11) ✅
 - FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 31) ✅
 - FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 50) ✅
 - FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 70) ✅

Complete! The build context can be found at: /home/********/builder/context
```

## Environment

```bash
$ docker version
Client: Docker Engine - Community
 Version:           27.0.1-rc.1
 API version:       1.46
 Go version:        go1.21.11
 Git commit:        0d415ad
 Built:             Thu Jun 20 15:59:18 2024
 OS/Arch:           linux/amd64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          27.0.1-rc.1
  API version:      1.46 (minimum version 1.24)
  Go version:       go1.21.11
  Git commit:       018d93d
  Built:            Thu Jun 20 15:57:34 2024
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.7.18
  GitCommit:        ae71819c4f5e67bb4d5ae76a6b735f29cc25774e
 runc:
  Version:          1.7.18
  GitCommit:        v1.1.13-0-g58aa920
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

$ docker buildx ls
NAME/NODE     DRIVER/ENDPOINT   STATUS    BUILDKIT   PLATFORMS
default*      docker                                 
 \_ default    \_ default       running   v0.14.1    linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386
```

## Steps to reproduce

1. Install docker engine 27+
2. Build EE with following yaml with `--container-runtime docker`

   ```yaml
   ---
   version: 3
   images:
     base_image:
       name: quay.io/centos/centos:stream9-minimal
   
   options:
     package_manager_path: /usr/bin/microdnf
   
   dependencies:
     python_interpreter:
       package_system: python3.12
       python_path: /usr/bin/python3.12
     ansible_core:
       package_pip: ansible-core~=2.17
     ansible_runner:
       package_pip: ansible-runner~=2.4
     galaxy:
       collections:
         - name: community.general
           version: 9.1.0
           source: https://galaxy.ansible.com
   
   additional_build_steps:
     append_base:
       - RUN alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 0
   ```

Discussion:
kurokobo: Oh, I missed that there are unit tests. Will update this PR to fix tests.
kurokobo: Updated.

Patch:
diff --git a/Containerfile b/Containerfile
index 555f369..0502aca 100644
--- a/Containerfile
+++ b/Containerfile
@@ -1,6 +1,6 @@
 ARG BASE_IMAGE=quay.io/centos/centos:stream9
 
-FROM $BASE_IMAGE as builder
+FROM $BASE_IMAGE AS builder
 # build this library (meaning ansible-builder)
 COPY . /tmp/src
 COPY ./src/ansible_builder/_target_scripts/* /output/scripts/
diff --git a/src/ansible_builder/containerfile.py b/src/ansible_builder/containerfile.py
index 6a516d5..55befca 100644
--- a/src/ansible_builder/containerfile.py
+++ b/src/ansible_builder/containerfile.py
@@ -75,7 +75,7 @@ class Containerfile:
         # 'base' (possibly customized) will be used by future build stages
         self.steps.extend([
             "# Base build stage",
-            "FROM $EE_BASE_IMAGE as base",
+            "FROM $EE_BASE_IMAGE AS base",
             "USER root",
             "ENV PIP_BREAK_SYSTEM_PACKAGES=1",
         ])
@@ -108,7 +108,7 @@ class Containerfile:
             self.steps.extend([
                 "",
                 "# Galaxy build stage",
-                "FROM base as galaxy",
+                "FROM base AS galaxy",
             ])
 
             self._insert_global_args()
@@ -137,7 +137,7 @@ class Containerfile:
         self.steps.extend([
             "",
             "# Builder build stage",
-            f"FROM {image} as builder",
+            f"FROM {image} AS builder",
             "ENV PIP_BREAK_SYSTEM_PACKAGES=1",
             "WORKDIR /build",
         ])
@@ -164,7 +164,7 @@ class Containerfile:
         self.steps.extend([
             "",
             "# Final build stage",
-            "FROM base as final",
+            "FROM base AS final",
             "ENV PIP_BREAK_SYSTEM_PACKAGES=1",
         ])
 


Test patch:
diff --git a/test/unit/test_containerfile.py b/test/unit/test_containerfile.py
index aeaae75..c803607 100644
--- a/test/unit/test_containerfile.py
+++ b/test/unit/test_containerfile.py
@@ -145,7 +145,7 @@ def test_v2_custom_builder_image(build_dir_and_ee_yml):
     c.prepare()
 
     assert 'ARG EE_BUILDER_IMAGE="quay.io/user/mycustombuilderimage:latest"' in c.steps
-    assert "FROM $EE_BUILDER_IMAGE as builder" in c.steps
+    assert "FROM $EE_BUILDER_IMAGE AS builder" in c.steps
 
 
 def test_v3_various(build_dir_and_ee_yml):
@@ -295,7 +295,7 @@ def test_v1_builder_image(build_dir_and_ee_yml):
     tmpdir, ee_path = build_dir_and_ee_yml(ee_data)
     c = make_containerfile(tmpdir, ee_path, run_validate=True)
     c.prepare()
-    assert "FROM $EE_BUILDER_IMAGE as builder" in c.steps
+    assert "FROM $EE_BUILDER_IMAGE AS builder" in c.steps
     assert "COPY _build/scripts/pip_install /output/scripts/pip_install" in c.steps
     assert "RUN /output/scripts/pip_install $PYCMD" in c.steps
 
@@ -315,7 +315,7 @@ def test_v2_builder_image(build_dir_and_ee_yml):
     tmpdir, ee_path = build_dir_and_ee_yml(ee_data)
     c = make_containerfile(tmpdir, ee_path, run_validate=True)
     c.prepare()
-    assert "FROM $EE_BUILDER_IMAGE as builder" in c.steps
+    assert "FROM $EE_BUILDER_IMAGE AS builder" in c.steps
     assert "COPY _build/scripts/pip_install /output/scripts/pip_install" in c.steps
     assert "RUN /output/scripts/pip_install $PYCMD" in c.steps
 
@@ -333,7 +333,7 @@ def test_v2_builder_image_default(build_dir_and_ee_yml):
     tmpdir, ee_path = build_dir_and_ee_yml(ee_data)
     c = make_containerfile(tmpdir, ee_path, run_validate=True)
     c.prepare()
-    assert "FROM base as builder" in c.steps
+    assert "FROM base AS builder" in c.steps
     assert "COPY _build/scripts/pip_install /output/scripts/pip_install" not in c.steps