Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: ImageFromDockerfileBuilder cannot build Dockerfile with intermediate layer (regression works with 3.3.0) #972

Closed
jasdefer opened this issue Aug 11, 2023 · 24 comments · Fixed by #979
Assignees
Labels
bug Something isn't working

Comments

@jasdefer
Copy link

Problem

I cannot give the docker directory to the ImageFromDockerfileBuilder, only the dockerfile is possible:

public ImageFromDockerfileBuilder WithDockerfile(string dockerfile)
    {
      return Merge(DockerResourceConfiguration, new ImageFromDockerfileConfiguration(dockerfile: dockerfile));
    }

I need to set the root to a certain directory so that the Copy instructions in my Dockerfile work.

Solution

I am not sure if this is enough, but consider this:

Add another method accepting the dockerfile and the directory.

public ImageFromDockerfileBuilder WithDockerfile(string dockerfile, string dockerfileDirectory)
    {
      return Merge(DockerResourceConfiguration, new ImageFromDockerfileConfiguration(dockerfile: dockerfile,
                                                                                     dockerfileDirectory: dockerfileDirectory));
    }

Benefit

This change could enable my use case for using the ImageFromDockerfileBuilder.

Alternatives

I am not sure if there are other alternatives, I don't understand Docker well enough yet.

Would you like to help contributing this enhancement?

Yes

@jasdefer jasdefer added the enhancement New feature or request label Aug 11, 2023
@HofmeisterAn
Copy link
Collaborator

You can pass the build context via WithDockerfileDirectory to the builder configuration. You will find further information in our docs.

@HofmeisterAn HofmeisterAn self-assigned this Aug 11, 2023
@HofmeisterAn HofmeisterAn added question Have you tried our Slack workspace (https://testcontainers.slack.com)? and removed enhancement New feature or request labels Aug 11, 2023
@jasdefer
Copy link
Author

jasdefer commented Aug 11, 2023

Yes, but building the Dockerfile will fail that way. I think, that WithDockerFileDirectory defines the directory from where the docker build command is executed. But from some root directory I need to run docker build --tag mytag -f "dir/dir/Dockerfile" for the build command to work with the Copy instructions. But maybe I misunderstood something?

@HofmeisterAn
Copy link
Collaborator

WithDockerfileDirectory gets the root directory that contains all files that are part of the Dockerfile and the final image. The Dockerfile is a part of that directory.

_ = new ImageFromDockerfileBuilder()
    .WithDockerfileDirectory("/Users/testcontainers/example")
    .WithDockerfile("dir/dir/Dockerfile"); // Corresponds to /Users/testcontainers/example/dir/dir/Dockerfile

Does that help?

@jasdefer
Copy link
Author

Thank you for your help :)
But no, that does not work, but running the docker build command works:

PS C:\Users\User\testcontainers\example> docker build --tag SomeTag -f "dir/dir/Dockerfile" .

And

IFutureDockerImage futureImage = new ImageFromDockerfileBuilder()
          .WithDockerfileDirectory("/Users/testcontainers/example")
          .WithDockerfile("dir/dir/Dockerfile")
          .Build();

        await futureImage.CreateAsync()
          .ConfigureAwait(false);

Throws the exception (when creating the futureImage:

Docker.DotNet.DockerApiException : Docker API responded with status code=NotFound, response={"message":"pull access denied for build, repository does not exist or may require 'docker login': denied: requested access to the resource is denied"}

The Dockerfile is a regular Dockerfile for running an ASPNET Web Api. It copies the files, restores, builds and publishes the application. So I am confused by the exception about pulling.

@jasdefer jasdefer changed the title [Enhancement]: [Enhancement]: Issue with Creating Docker Image from Dockerfile Aug 14, 2023
@HofmeisterAn
Copy link
Collaborator

pull access denied for build, repository does not exist or may require 'docker login': denied: requested access to the resource is denied

This sounds like Docker cannot pull the dependent images from in the Dockerfile. Are your dependent images stored in a private registry (FROM statement)? Which version do you use? Can you try 3.3.0 and 3.4.0?

@jasdefer
Copy link
Author

I don't point to private registries and only use mcr.microsoft.com/dotnet/aspnet:7.0 and mcr.microsoft.com/dotnet/sdk:7.0.

It works with version 3.3.0 but not with 3.4.0.. That helps, thank you! Is this a bug in the library, or did I set it up incorrectly?

For my background: I have a solution with a web app and a web api. The web app calls the api. For some integration tests I want to start both apps in containers and make requests to the web app. This way, I want to test, if the the web app can call the api and if everything fits together.

@HofmeisterAn
Copy link
Collaborator

HofmeisterAn commented Aug 14, 2023

It works with version 3.3.0 but not with 3.4.0.. That helps, thank you! Is this a bug in the library, or did I set it up incorrectly?

This sounds odd. It might be a regression in version 3.4.0 (#951), but I am unable to reproduce it. When I run the WeatherForecast example with version 3.4.0 (similar configuration to yours), it runs without any issues. The necessary images are pulled as specified in the Dockerfile.

Can you share the Dockerfile configuration and the log messages (run the tests in debug configuration in your IDE)?

@jasdefer
Copy link
Author

Sure, here is the Dockerfile:

FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["MySoftware/Source/MyApi/MyApi/MyApi.csproj", "MySoftware/Source/MyApi/MyApi/"]
RUN dotnet restore "MySoftware/Source/MyApi/MyApi/MyApi.csproj"
COPY . .
WORKDIR "/src/MySoftware/Source/MyApi/MyApi"
RUN dotnet build "MyApi.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "MyApi.csproj" -c Release -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyApi.dll"]

I am using Visual Studio and XUnit. What kind of log messages do you mean?

Here is the test output (when using testcontainers version 3.4.0):

 MyUi.Application.IntegrationTests.MyTest.Test
   Source: MyTest.cs line 9
   Duration: 2.8 sec

  Message: 
Docker.DotNet.DockerApiException : Docker API responded with status code=NotFound, response={"message":"pull access denied for build, repository does not exist or may require 'docker login': denied: requested access to the resource is denied"}


  Stack Trace: 
DockerClient.HandleIfErrorResponseAsync(HttpStatusCode statusCode, HttpResponseMessage response)
DockerClient.MakeRequestForRawResponseAsync(HttpMethod method, String path, IQueryString queryString, IRequestContent body, IDictionary`2 headers, CancellationToken token)
StreamUtil.MonitorResponseForMessagesAsync[T](Task`1 responseTask, DockerClient client, CancellationToken cancel, IProgress`1 progress)
DockerImageOperations.CreateAsync(IImage image, IDockerRegistryAuthenticationConfiguration dockerRegistryAuthConfig, CancellationToken ct)
TestcontainersClient.PullImageAsync(IImage image, CancellationToken ct)
TestcontainersClient.BuildAsync(IImageFromDockerfileConfiguration configuration, CancellationToken ct)
FutureDockerImage.UnsafeCreateAsync(CancellationToken ct)
FutureDockerImage.CreateAsync(CancellationToken ct)
MyTest.Test() line 18
--- End of stack trace from previous location ---

@HofmeisterAn
Copy link
Collaborator

Thank you, that really helped me understand the problem much better. Testcontainers for .NET, tries to download each image from the Dockerfile in advance (this is necessary when the image is stored in a private registry). However, in your case, Testcontainers for .NET attempts to download images for the lines FROM build AS publish and FROM base AS final. These lines represent intermediate layers, not actual images that Testcontainers can download. As a result, an exception is thrown. If you remove these intermediate layers (similar to the WeatherForecast example), you should be able to use the latest version (3.4.0).

I am curious about how we can identify these intermediate build layers. @eddumelendez , do you have any idea how Java does it? Do we need to skip pulling images when we find the image name after the AS statement?

@HofmeisterAn HofmeisterAn added bug Something isn't working and removed question Have you tried our Slack workspace (https://testcontainers.slack.com)? labels Aug 14, 2023
@jasdefer
Copy link
Author

Ah, I understand the problem now. Thank you for that explanation. Can I help in another way?

I don't really want to remove those intermediate layers, because they improve performance, and separate concerns, if I understand them correctly (Visual Studio automatically adds them when creating the Dockerfile). Would you still remove those layers or just work with the version 3.3.0?

@HofmeisterAn
Copy link
Collaborator

I don't really want to remove those intermediate layers

I intended that as a workaround, not as a final solution 😄. Of course, the upcoming version of Testcontainers for .NET should address the issue with the intermediate layers appropriately.

Can I help in another way?

I am considering a way to figure out the names of the intermediate layers, then we can skip those layers (images) here.

@jasdefer
Copy link
Author

I am not an expert with regular expressions, nor with the Dockerfile syntax. But can you just ignore lines with as ignoring the case? Note the space before and after as. Also you might need to ensure that this is not the end of the line. But I am not sure, if this simple approach is enough.

@HofmeisterAn
Copy link
Collaborator

I do not think we can simply ignore the lines that contains AS. We need to ignore those that matches an image, like:

FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
FROM base AS final

@jasdefer
Copy link
Author

Ah okay, so you need to store the labels (base for example) and ignore them?

@FredericVaugeoisFlo
Copy link

I have the same problem! :)

@HofmeisterAn HofmeisterAn changed the title [Enhancement]: Issue with Creating Docker Image from Dockerfile [Bug]: ImageFromDockerfileBuilder cannot build Dockerfile with intermediate layer (regression works with 3.3.0) Aug 14, 2023
@HofmeisterAn
Copy link
Collaborator

HofmeisterAn commented Aug 14, 2023

Ah okay, so you need to store the labels (base for example) and ignore them?

Yes, kind of.

I need to think about it again, but I believe the following will work. I am not sure if there is a more efficient solution.

public sealed class GitHub : List<string>
{
    private static readonly Regex FromLinePattern = new Regex("FROM (?<arg>--[^\\s]+\\s)*(?<image>[^\\s]+).*", RegexOptions.None, TimeSpan.FromSeconds(1));

    public GitHub()
    {
        Add("FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build1");
        Add("FROM mcr.microsoft.com/dotnet/sdk:7.0 as build2");
        Add("FROM mcr.microsoft.com/dotnet/sdk:7.0");
        Add("FROM mcr.microsoft.com/dotnet/sdk:6.0");
        Add("FROM build1 AS final1");
        Add("FROM build1 AS final2");
        Add("FROM mcr.microsoft.com/as/AS:5.0");
    }

    [Fact]
    public void Issue972()
    {
        // TODO: Only process lines that match FromLinePattern.
        IEnumerable<string> stages = this
            .Select(line => line.Split(new[] { " as ", " AS " }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
            .Where(substrings => substrings.Length > 1)
            .Select(substrings => substrings[substrings.Length - 1])
            .Distinct()
            .ToList();

        IEnumerable<string> images = this
            .Select(line => FromLinePattern.Match(line))
            .Where(match => match.Success)
            .Select(match => match.Groups["image"])
            .Select(group => group.Value)
            .Where(value => !stages.Any(stage => stage.EndsWith(value)))
            .Distinct()
            .ToList();

        Assert.Equal(4, stages.Count());
        Assert.Equal(4, images.Count());
    }
}

@jasdefer
Copy link
Author

jasdefer commented Aug 15, 2023

Maybe add " As " and " aS ", too? Looks good for me!

@ThumbGen
Copy link

ThumbGen commented Aug 16, 2023

I am also observing a problem with 3.4.0

ARG RUNTIME_VERSION=3.17
ARG BUILD_VERSION=7.0-bullseye-slim

FROM alpine:$RUNTIME_VERSION AS base

The BuildAsync fails now with "invalid tag name" as the $RUNTIME_VERSION is used as a tag instead of being resolved.

image

I suspect this change to be the problem: https://github.com/testcontainers/testcontainers-dotnet/pull/951/files

@HofmeisterAn
Copy link
Collaborator

Thank you, that is another case we need to address. I might have merged the feature to pull images from private registries to fast 😬.

@jasdefer
Copy link
Author

No worries, it's a great library and you responded very fast to the problems :) We can use 3.3.0 until those bugs are fixed.

@HofmeisterAn
Copy link
Collaborator

Much appreciated. I will address the issue later this week. In the first fix, I will not include the resolution of Dockerfile ARGs (variables). I will likely ignore those lines, similar to the previous versions, and add support for ARGs (variables) afterward. I will focus on:

pull access denied for build, repository does not exist or may require 'docker login': denied: requested access to the resource is denied

@HofmeisterAn HofmeisterAn pinned this issue Aug 16, 2023
@HofmeisterAn
Copy link
Collaborator

I have prepared a fix in #979. If anyone would like to review it, your feedback would be appreciated.

@HofmeisterAn HofmeisterAn unpinned this issue Sep 7, 2023
@goldsam
Copy link

goldsam commented Sep 26, 2023

This is still not working with 3.5.0. Getting the same results as OP. Works fine with 3.3.0.

My Dockerfile looks like this:

ARG DOTNET_RELEASE=7.0
FROM mcr.microsoft.com/dotnet/aspnet:$DOTNET_RELEASE AS base

WORKDIR /app

ENTRYPOINT ["dotnet", "MyApp.dll"]

FROM base AS final

ARG DOTNET_RELEASE=7.0
ARG BUILD_CONFIGURATION=Release
ARG PublishProtocol=publish
COPY bin/$BUILD_CONFIGURATION/net$DOTNET_RELEASE/$PublishProtocol .

# Use unprivileged ports
EXPOSE 9080
ENV ASPNETCORE_URLS=http://+:9080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl --fail http://localhost:9080/health || exit 1

# Use non-root user to run service
RUN groupadd -r service && useradd --no-log-init -r -g service service
RUN chown -R service:service /app
RUN chmod 755 /app
USER service:service

@HofmeisterAn
Copy link
Collaborator

This is still not working with 3.5.0. Getting the same results as OP. Works fine with 3.3.0.

It is working as described. ARGs are out of scope (this issue) and not supported yet. You find the related feature here: #980.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
5 participants