public static async Task <int> Launch(MarvelMicroserviceConfig config, LaunchOptions launchOptions) { var registratorRunResultTask = EnsureRegistratorContainerIsRunning(launchOptions.EurekaServer, launchOptions.LocalIpAddress); var buildImageUriTask = EcrResources.DotnetMicroserviceBuildImageUrl.EnsureImageIsPulled(); var registratorRunResult = await registratorRunResultTask; if (registratorRunResult != 0) { return(registratorRunResult); } var buildImageUri = await buildImageUriTask; if (!buildImageUri.WasSuccessful) { return(1); } // # TODO include a hash of the tools version in the container name to ensure containers are recreated after tools update(?) var containerName = config.DevContainerName; var dockerTaskScriptPath = ContainerPaths.DockerTaskScriptPath; // Build arguments list for `docker create` call so we can hash them and ensure an existing container is compatible. Argument pairs are combined into one argument for readability. var hostname = launchOptions.Host ?? await NetworkUtility.GetFriendlyHostName(); var labelArgs = LabelUtilities.FormatLabelsAsArguments(await new LabelBuilder(config).GetLabelsForLocalDev(hostname)); var dockerCreateArgs = labelArgs.Concat(new List <string> { "-p 5000:5000", "--dns-search=agilesports.local", $"-e \"{ProjectNameEnvVariable}={config.ProjectName}\"", $"-e \"{SolutionNameEnvVariable}={config.SolutionName}\"", $"-e \"{WebappDirEnvVariable}={ContainerPaths.GetWebappDirectory(config)}\"", $"-e \"{BrowserAppDirEnvVariable}={ContainerPaths.GetBrowserAppDirectory(config)}\"", // This could probably live in the image $"-e ASPNETCORE_ENVIRONMENT=Development", $"-v {config.BaseDirectory}:{ContainerPaths.MountedSourceDirectory}", }); if (launchOptions.UseSharedCache) { dockerCreateArgs = dockerCreateArgs.Concat(GetCacheVolumeArgs()); } var createContainerResult = await DockerCommands.GetOrCreateContainerWithName( containerName, buildImageUri.Value, dockerCreateArgs.ToList(), $"/bin/bash {dockerTaskScriptPath} hang"); var container = createContainerResult.Container; var isNewContainer = createContainerResult.IsNewContainer; if (!container.IsRunning) { Output.Info($"Starting container {containerName}"); var result = await CommandUtilities.RunCommandAsync("docker", $"start {container.ContainerId}", throwOnErrorExitCode : false); // Output.Info("StdErr=" + string.Join("\n", result.StandardError)); // Output.Info("StdOut=" + string.Join("\n", result.StandardOutput)); if (result.ExitCode != 0) { var stdErr = string.Join("\n", result.StandardError); // Message is `Bind for 0.0.0.0:5000 failed: port is already allocated`. trimming the port portion in case the port changes. if (stdErr.Contains("failed: port is already allocated")) { Output.Info("Webapp port is already in use. Attempting to stop other container using port."); // Find other containers using a partial match on suffix. This corresponds to the naming scheme defined in MarvelMicroserviceConfig. var containers = await DockerCommands.GetContainersByName("marvel-dev"); var otherContainer = containers.FirstOrDefault(c => c.IsRunning); if (otherContainer == null) { Output.Error("Unable to find running container using same port."); Output.Error($"Failed to start container {containerName}. StandardError={stdErr}"); return(1); } Output.Info($"Stopping container {otherContainer.ContainerId}"); await DockerCommands.StopContainer(otherContainer); Output.Info($"Starting container {containerName} again"); var restartResult = await ProcessEx.RunAsync("docker", $"start {container.ContainerId}"); if (restartResult.ExitCode != 0) { Output.Error($"Failed to restart container {containerName}. StandardError={stdErr}"); return(result.ExitCode); } } else { Output.Error($"Failed to start container {containerName}. StandardError={stdErr}"); return(result.ExitCode); } } // TODO only perform this check after failed `docker exec` commands, for better reporting? // Ensure the container doesn't immediately exit. Thread.Sleep(10); var runningContainer = await DockerCommands.GetContainerByName(containerName); if (!runningContainer.IsRunning) { Output.Error($"Container {containerName} stopped unexpectedly. Check container logs."); return(1); } } if (isNewContainer) { Output.Info($"Attaching to container {containerName} to run first time launch command on new container"); var code = CommandUtilities.ExecuteCommand("docker", $"exec -i {container.ContainerId} /bin/bash {dockerTaskScriptPath} firstTimeLaunch"); if (code != 0) { Output.Info($"First time startup command failed. Removing container."); await DockerCommands.RemoveContainer(container); return(code); } } else { Output.Info($"Attaching to container {containerName} to run relaunch command on existing container"); var code = CommandUtilities.ExecuteCommand("docker", $"exec -i {container.ContainerId} /bin/bash {dockerTaskScriptPath} relaunch"); if (code != 0) { return(code); } } Output.Info($"Container {containerName} launched and ready to run"); Output.Info($"Using hostname: {hostname}"); Output.Info($"If debugging from VS Code, switch to the Debug Console (Cmd+Shift+Y / Ctrl+Shift+Y) for app and watch process logs"); return(0); }
public static async Task <int> Build(MarvelMicroserviceConfig config, string publishedRuntimeImageName, BuildConfig buildConfig, IStaticAssetProcessor staticAssetProcessor) { var buildImageUri = await EcrResources.DotnetMicroserviceBuildImageUrl.EnsureImageIsPulled(); if (!buildImageUri.WasSuccessful) { return(1); } var taskTimer = Stopwatch.StartNew(); // Clear the old temp dir to ensure a fresh publish if running locally var outputPath = config.DotnetPublishOutputPath; if (Directory.Exists(outputPath)) { Directory.Delete(outputPath, true); } Directory.CreateDirectory(outputPath); var dockerRunOptions = new List <string> { $"--rm", $"-e \"{LaunchCommand.ProjectNameEnvVariable}={config.ProjectName}\"", $"-e \"{LaunchCommand.SolutionNameEnvVariable}={config.SolutionName}\"", $"-e \"{WebappDirEnvVariable}={ContainerPaths.GetWebappDirectory(config)}\"", $"-e \"{BrowserAppDirEnvVariable}={ContainerPaths.GetBrowserAppDirectory(config)}\"", $"-v {config.BaseDirectory}:{ContainerPaths.MountedSourceDirectory}", $"-v {outputPath}:{ContainerPaths.BuildOutputDirectory}", }; var runtimeImageLabelsTask = new LabelBuilder(config).GetLabels(buildConfig); var exitCode = 0; var dotnetBuildTimer = Stopwatch.StartNew(); Output.Info($"Building dotnet webapp."); // Run container to build app and copy published resources to mounted output directory exitCode = CommandUtilities.ExecuteCommand("docker", $"run {string.Join(" ", dockerRunOptions)} {buildImageUri.Value} /bin/bash {ContainerPaths.DockerTaskScriptPath} buildWithoutCompose"); if (exitCode != 0) { return(exitCode); } Output.Info($"dotnet webapp build completed {dotnetBuildTimer.Elapsed}"); // Run static asset build from output directory await staticAssetProcessor.ProcessStaticAssets(Path.Combine(config.DotnetPublishOutputPath, "wwwroot")); // Build the image from the source output var dockerBuildTimer = Stopwatch.StartNew(); Output.Info($"Building docker image {publishedRuntimeImageName}."); var labelArgs = LabelUtilities.FormatLabelsAsArguments(await runtimeImageLabelsTask); var buildArgs = labelArgs.Concat(new List <string> { $"-t {publishedRuntimeImageName}", $"--build-arg webappAssemblyPath={config.PublishedWebappAssemblyPath}", config.DotnetPublishOutputPath, }); exitCode = CommandUtilities.ExecuteCommand("docker", $"build {string.Join(" ", buildArgs)}"); if (exitCode != 0) { return(exitCode); } Output.Info($"Docker build completed {dockerBuildTimer.Elapsed}"); Output.Info($"Build time elapsed {taskTimer.Elapsed}"); return(exitCode); }