Docker.DotNet icon indicating copy to clipboard operation
Docker.DotNet copied to clipboard

Is it possible to use this package on MacOS with M1?

Open chrissainty opened this issue 3 years ago • 12 comments

Apologies if this is my lack of brain power but after two days of trying to get this to work I thought I'd just check it's actually possible. I've had a search through the issues and there seem to be a few from people running on MacOS, but I'm at a loss as to how to get things to work.

Right now I have the following class:

public class DockerSetup : IAsyncLifetime
{
    private readonly DockerClient _dockerClient;

    private string? _containerId;
    private const string ContainerImageUri = "mcr.microsoft.com/azure-sql-edge";

    public DockerSetup()
    {
        _dockerClient = new DockerClientConfiguration(DockerApiUri()).CreateClient();
    }

    public async Task InitializeAsync()
    {
        await PullImage();
        await StartContainer();
    }

    private Uri DockerApiUri()
    {
        var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
        if (isWindows)
        {
            return new Uri("npipe://./pipe/docker_engine");
        }

        var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
        var isMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
        if (isLinux || isMacOs)
        {
            return new Uri("unix:/var/run/docker.sock");
        }

        throw new Exception(
            "Was unable to determine what OS this is running on, does not appear to be Windows or Linux!?");
    }

    private async Task PullImage()
    {
        await _dockerClient.Images
            .CreateImageAsync(new ImagesCreateParameters { FromImage = ContainerImageUri, Tag = "latest" },
                new AuthConfig(),
                new Progress<JSONMessage>());
    }

    private async Task StartContainer()
    {
        var response = await _dockerClient.Containers.CreateContainerAsync(new CreateContainerParameters
        {
            Image = ContainerImageUri, Env = new[] { "ACCEPT_EULA=y", "MSSQL_SA_PASSWORD=IntegrationTestingFTW!" }, HostConfig = new HostConfig { PortBindings = new Dictionary<string, IList<PortBinding>> { { "1433/tcp", new PortBinding[] { new PortBinding { HostPort = "14333" } } } } }
        });

        _containerId = response.ID;
        await _dockerClient.Containers.StartContainerAsync(_containerId, null);
    }

    public async Task DisposeAsync()
    {
        if (_containerId != null)
        {
            await _dockerClient.Containers.KillContainerAsync(_containerId, new ContainerKillParameters());
        }
    }
}

But whenever I try to use it in a test I get an error System.Net.Http.HttpRequestException: Connection failed. Here is the info from dotnet --info

.NET SDK:
 Version:   7.0.100-rc.2.22477.23
 Commit:    0a5360315a

Runtime Environment:
 OS Name:     Mac OS X
 OS Version:  12.6
 OS Platform: Darwin
 RID:         osx.12-arm64
 Base Path:   /usr/local/share/dotnet/sdk/7.0.100-rc.2.22477.23/

Host:
  Version:      7.0.0-rc.2.22472.3
  Architecture: arm64
  Commit:       550605cc93

.NET SDKs installed:
  6.0.402 [/usr/local/share/dotnet/sdk]
  7.0.100-rc.2.22477.23 [/usr/local/share/dotnet/sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 6.0.10 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 7.0.0-rc.2.22476.2 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 6.0.10 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.0-rc.2.22472.3 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

And I'm running MacOS Monterey 12.6 with Docker 4.13.0.

chrissainty avatar Oct 24 '22 21:10 chrissainty

Your example looks good, it runs on my Windows machine. I am expecting an M1 soon, then I can test it there too. Can you add the full stack trace of your error?

Is this a fresh installation of Docker Desktop (4.13.0)? Can you install 4.12.0 first and then do an update? We noticed some issues with a fresh 4.13.0.

HofmeisterAn avatar Oct 25 '22 05:10 HofmeisterAn

I've removed DOcker 4.13.0 and done a fresh install of 4.12.0 and ran the code again and I'm getting the same error. Here is a full stack trace:

ystem.Net.Http.HttpRequestException: Connection failed
 ---> System.Net.Sockets.SocketException (49): Can't assign requested address
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.CreateException(SocketError error, Boolean forAsyncThrow)
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ConnectAsync(Socket socket)
   at System.Net.Sockets.Socket.ConnectAsync(EndPoint remoteEP, CancellationToken cancellationToken)
   at System.Net.Sockets.Socket.ConnectAsync(EndPoint remoteEP)
   at System.Net.Sockets.SocketTaskExtensions.ConnectAsync(Socket socket, EndPoint remoteEP)
   at Docker.DotNet.DockerClient.<>c__DisplayClass6_0.<<-ctor>b__1>d.MoveNext()
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at Docker.DotNet.DockerClient.<>c__DisplayClass6_0.<.ctor>b__1(String host, Int32 port, CancellationToken cancellationToken)
   at Microsoft.Net.Http.Client.ManagedHandler.ProcessRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at Microsoft.Net.Http.Client.ManagedHandler.ProcessRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at Microsoft.Net.Http.Client.ManagedHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at Microsoft.Net.Http.Client.ManagedHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpMessageInvoker.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
   at System.Net.Http.HttpClient.SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
   at Docker.DotNet.DockerClient.PrivateMakeRequestAsync(TimeSpan timeout, HttpCompletionOption completionOption, HttpMethod method, String path, IQueryString queryString, IDictionary`2 headers, IRequestContent data, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at Docker.DotNet.DockerClient.PrivateMakeRequestAsync(TimeSpan timeout, HttpCompletionOption completionOption, HttpMethod method, String path, IQueryString queryString, IDictionary`2 headers, IRequestContent data, CancellationToken cancellationToken)
   at Docker.DotNet.DockerClient.MakeRequestForRawResponseAsync(HttpMethod method, String path, IQueryString queryString, IRequestContent body, IDictionary`2 headers, CancellationToken token)
   at Docker.DotNet.ImageOperations.CreateImageAsync(ImagesCreateParameters parameters, Stream imageStream, AuthConfig authConfig, IDictionary`2 headers, IProgress`1 progress, CancellationToken cancellationToken)
   at Docker.DotNet.ImageOperations.CreateImageAsync(ImagesCreateParameters parameters, Stream imageStream, AuthConfig authConfig, IProgress`1 progress, CancellationToken cancellationToken)
   at Docker.DotNet.ImageOperations.CreateImageAsync(ImagesCreateParameters parameters, AuthConfig authConfig, IProgress`1 progress, CancellationToken cancellationToken)
   at WorkAuthor.Web.IntegrationTests.Setup.DockerSetup.PullImage() in /Users/chrissainty/Git/Deployed/WorkAuthor/src/WorkAuthor.Api/tests/WorkAuthor.Api.Tests.Integration/src/Setup/DockerSetup.cs:line 47

The HttpClient is using http://docker.sock as its base address, which I believe is correct. However, I can't connect to that address using cURL (not sure if I should be able to, but I'm guessing so).

chrissainty avatar Oct 25 '22 07:10 chrissainty

I know some developers using Testcontainers for .NET on an M1. Usually, they have issues with running the containers (compatibility), but not with pulling the images. Unfortunately, I do not have an M1 here, I will test it again soon as it arrives.

HofmeisterAn avatar Oct 25 '22 07:10 HofmeisterAn

We currently run Azure sql edge on M1 Macs and regular sql on x86/x64 (for double the compatibility), I managed to get it to work using the TestContainers package that wraps Docker.DotNet with TestContainersBuilder with MSSqlTestContainer, with a couple of changes. This used to work on DockerDesktop 4.9.1, we migrated to Rancher Desktop after that, and it continues to work there, with one gotcha, in that Rancher desktop doesn't have a loopback for localhost so the connection string needed replacing with 127.0.0.1 to work, this shouldn't be needed with Docker desktop though. Hope this code helps you:

   public SqlTestContainer Make()
   {
      var configuration = new MsSqlTestcontainerConfiguration { Password = this.sqlConnectionStringBuilder.Password };

      var architecture = RuntimeInformation.ProcessArchitecture;
      TestcontainersSettings.Logger ??= this.logger;
      MsSqlTestcontainer testContainer;
      switch(architecture)
      {
         case Architecture.X86:
         case Architecture.X64:
            testContainer = new TestcontainersBuilder<MsSqlTestcontainer>()
                            .WithDatabase(configuration)
                            .Build();
            break;
         case Architecture.Arm64:
            const string started = "Recovery is complete. This is an informational message only. No user action is required.";
            var stdout = new MemoryStream();
            var stderr = new MemoryStream();
            var consumer = Consume.RedirectStdoutAndStderrToStream(stdout, stderr);
            testContainer = new TestcontainersBuilder<MsSqlTestcontainer>()
                            .WithDatabase(configuration)
                            .WithImage("mcr.microsoft.com/azure-sql-edge")
                            .WithOutputConsumer(consumer)
                            .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged(consumer.Stdout, started))
                            .Build();
            break;
         case Architecture.Arm:
         case Architecture.Wasm:
         case Architecture.S390x:
         default:
            throw new InvalidOperationException($"No container tested with architecture '{architecture}'");
      }

      return new SqlTestContainer(testContainer, this.sqlConnectionStringBuilder);
   }

rossmasday avatar Oct 25 '22 13:10 rossmasday

Thanks for the info @rossmasday. I think the issue is with connecting with the Docker API. The client is trying to connection to http://docker.sock but that doesn't appear to be responding to anything. I can't connect to it with cURL either. Is there some setting to enable the API or should it be available by default?

chrissainty avatar Oct 25 '22 15:10 chrissainty

@chrissainty Check out the Docker release notes:

By default Docker will not create the /var/run/docker.sock symlink on the host and use the docker-desktop CLI context instead.

Maybe your are running into that?

HofmeisterAn avatar Oct 26 '22 12:10 HofmeisterAn

@HofmeisterAn This looks to be the issue. So updating the DockerApiUrl method's code to this works:

var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
var isMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
if (isLinux || isMacOs)
{
    return new Uri("unix:///Users/chrissainty/.docker/run/docker.sock");
}

I guess this could be added to user secrets so each dev can specify their own location. I've tried updating the docker context to use default but that still doesn't make the usual unix:///var/run/docker.sock URL work. I'm a bit out of my depth here with Docker... I just want to write some integration tests 😭

chrissainty avatar Oct 26 '22 13:10 chrissainty

So after sinking more hours into this, I'm ready to admit defeat--for now. We're going to setup Docker manually and I'll maybe revisit this in the future. Thanks for the help @HofmeisterAn, it's been much appreciated.

chrissainty avatar Oct 27 '22 07:10 chrissainty

sudo ln -s $HOME/.docker/run/docker.sock /var/run/docker.sock does not help? Maybe this discussion helps: https://github.com/testcontainers/testcontainers-java/discussions/6045.

HofmeisterAn avatar Oct 27 '22 08:10 HofmeisterAn

I'm facing the same issues using Testcontainers on a freshly installed M2, whereas I had it running on my previous M1 @HofmeisterAn.

Update: I downloaded the latest dmg from here and now it is working as expected: https://docs.docker.com/desktop/release-notes/#4170

riezebosch avatar Apr 03 '23 10:04 riezebosch

@riezebosch We got a similar response in Slack a few days ago. According to the Docker release notes nothing has changed. Right now, we guess that depending on the permissions, Docker Desktop for Mac creates the symlink or not.

Is Docker activated and enabled in the macOS "Login Items" (Settings / General / Login Items / Allow in the Background)? Maybe it creats the symlink on startup. Otherwise, you can set the DOCKER_HOST env variable to the user daemon socket.

If I download the latest dmg from here it is working as expected: https://docs.docker.com/desktop/release-notes/#4170

Now, I am a bit confused. What did you use before? BTW, we are thinking to resolve the user Docker daemon automatically too.

HofmeisterAn avatar Apr 03 '23 12:04 HofmeisterAn

@riezebosch We got a similar response in Slack a few days ago. According to the Docker release notes nothing has changed. Right now, we guess that depending on the permissions, Docker Desktop for Mac creates the symlink or not.

Is Docker activated and enabled in the macOS "Login Items" (Settings / General / Login Items / Allow in the Background)? Maybe it creats the symlink on startup. Otherwise, you can set the DOCKER_HOST env variable to the user daemon socket.

If I download the latest dmg from here it is working as expected: https://docs.docker.com/desktop/release-notes/#4170

Now, I am a bit confused. What did you use before? BTW, we are thinking to resolve the user Docker daemon automatically too.

I have no idea 🤷🏻 Looking back in my browser history I used the generic download url on 16th of March: https://desktop.docker.com/mac/main/arm64/Docker.dmg

That's probably the exact same version I downloaded today. So I did go from 4.17.0 to 4.16.3 to 4.17.0 again.

Meanwhile I had disabled it in the "Login Items".

riezebosch avatar Apr 03 '23 14:04 riezebosch