diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..341d071 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +**/bin/ +**/obj/ +**/global.json +**/Dockerfile* +**/.dockerignore* +**/*.user + diff --git a/.gitignore b/.gitignore index b14f7ce..aa9d70d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /AccountOwnerServer/obj /AccountOwnerServer/bin +/AccountOwnerServer/Projects /Contracts/obj /Contracts/bin /Entities/obj @@ -8,3 +9,14 @@ /LoggerService/bin /Repository/obj /Repository/bin +/Tests/bin +/Tests/obj +publish +/.vs +/AccountOwnerServer/internallog.txt +/Integration/obj +/Integration/bin +/AccountOwnerServer/*.user +/Tests/*.user +/Integration/*.user +.vscode/settings.json diff --git a/AccountOwnerServer.sln b/AccountOwnerServer.sln index 7620715..fff8a13 100644 --- a/AccountOwnerServer.sln +++ b/AccountOwnerServer.sln @@ -1,7 +1,6 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26730.3 +VisualStudioVersion = 15.0.27130.2010 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AccountOwnerServer", "AccountOwnerServer\AccountOwnerServer.csproj", "{73FB08B8-4033-4116-B3AC-EC8253B024D4}" EndProject @@ -11,34 +10,106 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoggerService", "LoggerServ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Entities", "Entities\Entities.csproj", "{8CD97B3F-C616-469C-80A2-A1B366611487}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Repository", "Repository\Repository.csproj", "{CC0B1C81-CB8A-412C-BD48-BCBC3F1A1CD0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repository", "Repository\Repository.csproj", "{CC0B1C81-CB8A-412C-BD48-BCBC3F1A1CD0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{89CF5113-7F84-40B8-A81C-B918F00183AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integration", "Integration\Integration.csproj", "{35B4193F-04B2-4C75-BA9B-8221185D2F83}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {73FB08B8-4033-4116-B3AC-EC8253B024D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {73FB08B8-4033-4116-B3AC-EC8253B024D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73FB08B8-4033-4116-B3AC-EC8253B024D4}.Debug|x64.ActiveCfg = Debug|Any CPU + {73FB08B8-4033-4116-B3AC-EC8253B024D4}.Debug|x64.Build.0 = Debug|Any CPU + {73FB08B8-4033-4116-B3AC-EC8253B024D4}.Debug|x86.ActiveCfg = Debug|Any CPU + {73FB08B8-4033-4116-B3AC-EC8253B024D4}.Debug|x86.Build.0 = Debug|Any CPU {73FB08B8-4033-4116-B3AC-EC8253B024D4}.Release|Any CPU.ActiveCfg = Release|Any CPU {73FB08B8-4033-4116-B3AC-EC8253B024D4}.Release|Any CPU.Build.0 = Release|Any CPU + {73FB08B8-4033-4116-B3AC-EC8253B024D4}.Release|x64.ActiveCfg = Release|Any CPU + {73FB08B8-4033-4116-B3AC-EC8253B024D4}.Release|x64.Build.0 = Release|Any CPU + {73FB08B8-4033-4116-B3AC-EC8253B024D4}.Release|x86.ActiveCfg = Release|Any CPU + {73FB08B8-4033-4116-B3AC-EC8253B024D4}.Release|x86.Build.0 = Release|Any CPU {52725034-B791-4DF1-A227-02EDB5BF78E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {52725034-B791-4DF1-A227-02EDB5BF78E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52725034-B791-4DF1-A227-02EDB5BF78E0}.Debug|x64.ActiveCfg = Debug|Any CPU + {52725034-B791-4DF1-A227-02EDB5BF78E0}.Debug|x64.Build.0 = Debug|Any CPU + {52725034-B791-4DF1-A227-02EDB5BF78E0}.Debug|x86.ActiveCfg = Debug|Any CPU + {52725034-B791-4DF1-A227-02EDB5BF78E0}.Debug|x86.Build.0 = Debug|Any CPU {52725034-B791-4DF1-A227-02EDB5BF78E0}.Release|Any CPU.ActiveCfg = Release|Any CPU {52725034-B791-4DF1-A227-02EDB5BF78E0}.Release|Any CPU.Build.0 = Release|Any CPU + {52725034-B791-4DF1-A227-02EDB5BF78E0}.Release|x64.ActiveCfg = Release|Any CPU + {52725034-B791-4DF1-A227-02EDB5BF78E0}.Release|x64.Build.0 = Release|Any CPU + {52725034-B791-4DF1-A227-02EDB5BF78E0}.Release|x86.ActiveCfg = Release|Any CPU + {52725034-B791-4DF1-A227-02EDB5BF78E0}.Release|x86.Build.0 = Release|Any CPU {A190AF01-84DC-40A8-BEF3-C015825C8B30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A190AF01-84DC-40A8-BEF3-C015825C8B30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A190AF01-84DC-40A8-BEF3-C015825C8B30}.Debug|x64.ActiveCfg = Debug|Any CPU + {A190AF01-84DC-40A8-BEF3-C015825C8B30}.Debug|x64.Build.0 = Debug|Any CPU + {A190AF01-84DC-40A8-BEF3-C015825C8B30}.Debug|x86.ActiveCfg = Debug|Any CPU + {A190AF01-84DC-40A8-BEF3-C015825C8B30}.Debug|x86.Build.0 = Debug|Any CPU {A190AF01-84DC-40A8-BEF3-C015825C8B30}.Release|Any CPU.ActiveCfg = Release|Any CPU {A190AF01-84DC-40A8-BEF3-C015825C8B30}.Release|Any CPU.Build.0 = Release|Any CPU + {A190AF01-84DC-40A8-BEF3-C015825C8B30}.Release|x64.ActiveCfg = Release|Any CPU + {A190AF01-84DC-40A8-BEF3-C015825C8B30}.Release|x64.Build.0 = Release|Any CPU + {A190AF01-84DC-40A8-BEF3-C015825C8B30}.Release|x86.ActiveCfg = Release|Any CPU + {A190AF01-84DC-40A8-BEF3-C015825C8B30}.Release|x86.Build.0 = Release|Any CPU {8CD97B3F-C616-469C-80A2-A1B366611487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8CD97B3F-C616-469C-80A2-A1B366611487}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CD97B3F-C616-469C-80A2-A1B366611487}.Debug|x64.ActiveCfg = Debug|Any CPU + {8CD97B3F-C616-469C-80A2-A1B366611487}.Debug|x64.Build.0 = Debug|Any CPU + {8CD97B3F-C616-469C-80A2-A1B366611487}.Debug|x86.ActiveCfg = Debug|Any CPU + {8CD97B3F-C616-469C-80A2-A1B366611487}.Debug|x86.Build.0 = Debug|Any CPU {8CD97B3F-C616-469C-80A2-A1B366611487}.Release|Any CPU.ActiveCfg = Release|Any CPU {8CD97B3F-C616-469C-80A2-A1B366611487}.Release|Any CPU.Build.0 = Release|Any CPU + {8CD97B3F-C616-469C-80A2-A1B366611487}.Release|x64.ActiveCfg = Release|Any CPU + {8CD97B3F-C616-469C-80A2-A1B366611487}.Release|x64.Build.0 = Release|Any CPU + {8CD97B3F-C616-469C-80A2-A1B366611487}.Release|x86.ActiveCfg = Release|Any CPU + {8CD97B3F-C616-469C-80A2-A1B366611487}.Release|x86.Build.0 = Release|Any CPU {CC0B1C81-CB8A-412C-BD48-BCBC3F1A1CD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CC0B1C81-CB8A-412C-BD48-BCBC3F1A1CD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC0B1C81-CB8A-412C-BD48-BCBC3F1A1CD0}.Debug|x64.ActiveCfg = Debug|Any CPU + {CC0B1C81-CB8A-412C-BD48-BCBC3F1A1CD0}.Debug|x64.Build.0 = Debug|Any CPU + {CC0B1C81-CB8A-412C-BD48-BCBC3F1A1CD0}.Debug|x86.ActiveCfg = Debug|Any CPU + {CC0B1C81-CB8A-412C-BD48-BCBC3F1A1CD0}.Debug|x86.Build.0 = Debug|Any CPU {CC0B1C81-CB8A-412C-BD48-BCBC3F1A1CD0}.Release|Any CPU.ActiveCfg = Release|Any CPU {CC0B1C81-CB8A-412C-BD48-BCBC3F1A1CD0}.Release|Any CPU.Build.0 = Release|Any CPU + {CC0B1C81-CB8A-412C-BD48-BCBC3F1A1CD0}.Release|x64.ActiveCfg = Release|Any CPU + {CC0B1C81-CB8A-412C-BD48-BCBC3F1A1CD0}.Release|x64.Build.0 = Release|Any CPU + {CC0B1C81-CB8A-412C-BD48-BCBC3F1A1CD0}.Release|x86.ActiveCfg = Release|Any CPU + {CC0B1C81-CB8A-412C-BD48-BCBC3F1A1CD0}.Release|x86.Build.0 = Release|Any CPU + {89CF5113-7F84-40B8-A81C-B918F00183AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89CF5113-7F84-40B8-A81C-B918F00183AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89CF5113-7F84-40B8-A81C-B918F00183AD}.Debug|x64.ActiveCfg = Debug|Any CPU + {89CF5113-7F84-40B8-A81C-B918F00183AD}.Debug|x64.Build.0 = Debug|Any CPU + {89CF5113-7F84-40B8-A81C-B918F00183AD}.Debug|x86.ActiveCfg = Debug|Any CPU + {89CF5113-7F84-40B8-A81C-B918F00183AD}.Debug|x86.Build.0 = Debug|Any CPU + {89CF5113-7F84-40B8-A81C-B918F00183AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89CF5113-7F84-40B8-A81C-B918F00183AD}.Release|Any CPU.Build.0 = Release|Any CPU + {89CF5113-7F84-40B8-A81C-B918F00183AD}.Release|x64.ActiveCfg = Release|Any CPU + {89CF5113-7F84-40B8-A81C-B918F00183AD}.Release|x64.Build.0 = Release|Any CPU + {89CF5113-7F84-40B8-A81C-B918F00183AD}.Release|x86.ActiveCfg = Release|Any CPU + {89CF5113-7F84-40B8-A81C-B918F00183AD}.Release|x86.Build.0 = Release|Any CPU + {35B4193F-04B2-4C75-BA9B-8221185D2F83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35B4193F-04B2-4C75-BA9B-8221185D2F83}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35B4193F-04B2-4C75-BA9B-8221185D2F83}.Debug|x64.ActiveCfg = Debug|Any CPU + {35B4193F-04B2-4C75-BA9B-8221185D2F83}.Debug|x64.Build.0 = Debug|Any CPU + {35B4193F-04B2-4C75-BA9B-8221185D2F83}.Debug|x86.ActiveCfg = Debug|Any CPU + {35B4193F-04B2-4C75-BA9B-8221185D2F83}.Debug|x86.Build.0 = Debug|Any CPU + {35B4193F-04B2-4C75-BA9B-8221185D2F83}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35B4193F-04B2-4C75-BA9B-8221185D2F83}.Release|Any CPU.Build.0 = Release|Any CPU + {35B4193F-04B2-4C75-BA9B-8221185D2F83}.Release|x64.ActiveCfg = Release|Any CPU + {35B4193F-04B2-4C75-BA9B-8221185D2F83}.Release|x64.Build.0 = Release|Any CPU + {35B4193F-04B2-4C75-BA9B-8221185D2F83}.Release|x86.ActiveCfg = Release|Any CPU + {35B4193F-04B2-4C75-BA9B-8221185D2F83}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/AccountOwnerServer/AccountOwnerServer.csproj b/AccountOwnerServer/AccountOwnerServer.csproj index beffa6b..ec84190 100644 --- a/AccountOwnerServer/AccountOwnerServer.csproj +++ b/AccountOwnerServer/AccountOwnerServer.csproj @@ -9,8 +9,9 @@ - + + @@ -24,4 +25,13 @@ + + + PreserveNewest + + + PreserveNewest + + + diff --git a/AccountOwnerServer/Controllers/AccountController.cs b/AccountOwnerServer/Controllers/AccountController.cs new file mode 100644 index 0000000..4404b93 --- /dev/null +++ b/AccountOwnerServer/Controllers/AccountController.cs @@ -0,0 +1,67 @@ +using Contracts; +using Entities.Extensions; +using Entities.Models; +using Microsoft.AspNetCore.Mvc; +using System; + +namespace AccountOwnerServer.Controllers +{ + [Route("api/[controller]")] + public class AccountController : Controller + { + private ILoggerManager _logger; + private IRepositoryWrapper _repository; + + public AccountController(ILoggerManager logger, IRepositoryWrapper repository) + { + _logger = logger; + _repository = repository; + } + + [HttpGet] + public IActionResult GetAllAccounts() + { + try + { + var accounts = _repository.Account.GetAllAccounts(); + + _logger.LogInfo($"Returned all accounts from database."); + + return Ok(accounts); + } + catch (Exception ex) + { + _logger.LogError($"Something went wrong inside GetAllAccounts action: {ex}"); + return StatusCode(500, "Internal server error"); + } + } + + [HttpPost] + public IActionResult CreateOwner([FromBody]Account account) + { + try + { + if (account.IsObjectNull()) + { + _logger.LogError("Object sent from client is null."); + return BadRequest("Object is null"); + } + + if (!ModelState.IsValid) + { + _logger.LogError("Invalid object sent from client."); + return BadRequest("Invalid model object"); + } + + _repository.Account.CreateAccount(account); + + return CreatedAtRoute("AccountById", new { id = account.Id }, account); + } + catch (Exception ex) + { + _logger.LogError($"Something went wrong inside CreateAccount action: {ex}"); + return StatusCode(500, "Internal server error"); + } + } + } +} diff --git a/AccountOwnerServer/Controllers/OwnerController.cs b/AccountOwnerServer/Controllers/OwnerController.cs index dc2850c..e26f275 100644 --- a/AccountOwnerServer/Controllers/OwnerController.cs +++ b/AccountOwnerServer/Controllers/OwnerController.cs @@ -31,7 +31,7 @@ public IActionResult GetAllOwners() } catch (Exception ex) { - _logger.LogError($"Something went wrong inside GetAllOwners action: {ex.Message}"); + _logger.LogError($"Something went wrong inside GetAllOwners action: {ex}"); return StatusCode(500, "Internal server error"); } } @@ -62,7 +62,7 @@ public IActionResult GetOwnerById(Guid id) } [HttpGet("{id}/account")] - public IActionResult GetOwnerWithDetails(Guid id) + public IActionResult GetOwnerAccounts(Guid id) { try { diff --git a/AccountOwnerServer/Extensions/ServiceExtensions.cs b/AccountOwnerServer/Extensions/ServiceExtensions.cs index 89ac3a5..ca6eb09 100644 --- a/AccountOwnerServer/Extensions/ServiceExtensions.cs +++ b/AccountOwnerServer/Extensions/ServiceExtensions.cs @@ -6,10 +6,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Repository; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace AccountOwnerServer.Extensions { diff --git a/AccountOwnerServer/Startup.cs b/AccountOwnerServer/Startup.cs index 3a86fc4..fa74191 100644 --- a/AccountOwnerServer/Startup.cs +++ b/AccountOwnerServer/Startup.cs @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.HttpOverrides; using System.IO; using NLog.Extensions.Logging; -using Contracts; +using Swashbuckle.AspNetCore.Swagger; namespace AccountOwnerServer { @@ -39,6 +39,11 @@ public void ConfigureServices(IServiceCollection services) services.ConfigureRepositoryWrapper(); services.AddMvc(); + + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new Info { Title = "AccountOwner API", Version = "v1" }); + }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) @@ -47,7 +52,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); } - + app.UseCors("CorsPolicy"); app.UseForwardedHeaders(new ForwardedHeadersOptions @@ -70,6 +75,13 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) app.UseStaticFiles(); app.UseMvc(); + + app.UseSwagger(); + + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "AccountOwner API V1"); + }); } } } diff --git a/AccountOwnerServer/appsettings.json b/AccountOwnerServer/appsettings.json index 934ec46..5935f76 100644 --- a/AccountOwnerServer/appsettings.json +++ b/AccountOwnerServer/appsettings.json @@ -13,6 +13,6 @@ } }, "mysqlconnection": { - "connectionString": "server=localhost;userid=dbuser;password=pass;database=accountowner;" + "connectionString": "server=db;port=3306;userid=dbuser;password=dbuserpassword;database=accountowner;" } } diff --git a/AccountOwnerServer/nlog.config b/AccountOwnerServer/nlog.config index 91c2174..69efc94 100644 --- a/AccountOwnerServer/nlog.config +++ b/AccountOwnerServer/nlog.config @@ -3,7 +3,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true" internalLogLevel="Trace" - internalLogFile="d:Projects\Blog-AccountOwner\Project\internal_logs\internallog.txt"> + internalLogFile="internallog.txt"> @@ -11,7 +11,7 @@ diff --git a/Contracts/IAccountRepository.cs b/Contracts/IAccountRepository.cs index 825a4fb..945b7f5 100644 --- a/Contracts/IAccountRepository.cs +++ b/Contracts/IAccountRepository.cs @@ -1,8 +1,11 @@ using Entities.Models; +using System.Collections.Generic; namespace Contracts { public interface IAccountRepository { + IEnumerable GetAllAccounts(); + void CreateAccount(Account account); } } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..16e6204 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM microsoft/aspnetcore-build as build-image + +WORKDIR /home/app + +COPY ./*.sln ./ +COPY ./*/*.csproj ./ +RUN for file in $(ls *.csproj); do mkdir -p ./${file%.*}/ && mv $file ./${file%.*}/; done + +RUN dotnet restore + +COPY . . + +RUN dotnet test --verbosity=normal --results-directory /TestResults/ --logger "trx;LogFileName=test_results.xml" ./Tests/Tests.csproj + +RUN dotnet publish ./AccountOwnerServer/AccountOwnerServer.csproj -o /publish/ + +FROM microsoft/aspnetcore + +WORKDIR /publish + +COPY --from=build-image /publish . + +COPY --from=build-image /TestResults /TestResults + +ENV TEAMCITY_PROJECT_NAME = ${TEAMCITY_PROJECT_NAME} + +ENTRYPOINT ["dotnet", "AccountOwnerServer.dll"] diff --git a/Dockerfile.Integration b/Dockerfile.Integration new file mode 100644 index 0000000..88c3d4c --- /dev/null +++ b/Dockerfile.Integration @@ -0,0 +1,15 @@ +FROM microsoft/dotnet:2-sdk + +WORKDIR /home/app + +COPY ./*.sln ./ +COPY ./*/*.csproj ./ +RUN for file in $(ls *.csproj); do mkdir -p ./${file%.*}/ && mv $file ./${file%.*}/; done + +RUN dotnet restore + +COPY . . + +WORKDIR /home/app/Integration/ + +ENTRYPOINT ["dotnet", "test", "--verbosity=normal"] \ No newline at end of file diff --git a/Infrastructure/Registry/docker-compose.yml b/Infrastructure/Registry/docker-compose.yml new file mode 100644 index 0000000..12a05cb --- /dev/null +++ b/Infrastructure/Registry/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.0' + +services: + my-registry: + image: registry:latest + container_name: my-registry + volumes: + - registry:/var/lib/registry3 + ports: + - "50000:5000" + restart: unless-stopped +volumes: + registry: \ No newline at end of file diff --git a/Integration/Integration.csproj b/Integration/Integration.csproj new file mode 100644 index 0000000..a4bd719 --- /dev/null +++ b/Integration/Integration.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + + + + + + + + diff --git a/Integration/IntegrationTests.cs b/Integration/IntegrationTests.cs new file mode 100644 index 0000000..ce62209 --- /dev/null +++ b/Integration/IntegrationTests.cs @@ -0,0 +1,47 @@ +using AccountOwnerServer; +using Entities.Models; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace Integration +{ + public class IntegrationTests + { + private readonly TestContext _context; + + public IntegrationTests() + { + _context = new TestContext(); + } + + [Fact] + public async Task GetAllOwners_ReturnsOkResponse() + { + // Act + var response = await _context.Client.GetAsync("/api/owner"); + response.EnsureSuccessStatusCode(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task GetAllOwners_ReturnsAListOfOwners() + { + // Act + var response = await _context.Client.GetAsync("/api/owner"); + response.EnsureSuccessStatusCode(); + var responseString = await response.Content.ReadAsStringAsync(); + var owners = JsonConvert.DeserializeObject>(responseString); + + // Assert + Assert.NotEmpty(owners); + } + } +} diff --git a/Integration/TestContext.cs b/Integration/TestContext.cs new file mode 100644 index 0000000..33bc11b --- /dev/null +++ b/Integration/TestContext.cs @@ -0,0 +1,37 @@ +using AccountOwnerServer; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using System; +using System.Net.Http; + +namespace Integration +{ + public class TestContext: IDisposable + { + private TestServer _server; + public HttpClient Client { get; private set; } + + public TestContext() + { + SetUpClient(); + } + + private void SetUpClient() + { + _server = new TestServer(new WebHostBuilder() + .UseConfiguration(new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build()) + .UseStartup()); + + Client = _server.CreateClient(); + } + + public void Dispose() + { + _server?.Dispose(); + Client?.Dispose(); + } + } +} diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..74521ba --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,25 @@ +node('docker') { + + stage 'Checkout' + checkout scm + stage 'Build & UnitTest' + sh "docker build -t accountownerapp:B${BUILD_NUMBER} -f Dockerfile ." + sh "docker build -t accountownerapp:test-B${BUILD_NUMBER} -f Dockerfile.Integration ." + + stage 'Pusblish UT Reports' + + containerID = sh ( + script: "docker run -d accountownerapp:B${BUILD_NUMBER}", + returnStdout: true + ).trim() + echo "Container ID is ==> ${containerID}" + sh "docker cp ${containerID}:/TestResults/test_results.xml test_results.xml" + sh "docker stop ${containerID}" + sh "docker rm ${containerID}" + step([$class: 'MSTestPublisher', failOnError: false, testResultsFile: 'test_results.xml']) + + stage 'Integration Test' + //sh 'docker-compose -f docker-compose.integration.yml up' + sh "docker-compose -f docker-compose.integration.yml up --force-recreate --abort-on-container-exit" + sh "docker-compose -f docker-compose.integration.yml down -v" +} diff --git a/README.md b/README.md index 226a577..310e278 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ # Docker Series +https://code-maze.com/docker-series/ + +## Part 8 of the Docker Series on CodeMaze blog +https://code-maze.com/ci-jenkins-docker/ diff --git a/Repository/AccountRepository.cs b/Repository/AccountRepository.cs index 9dcb2d5..0cd0be0 100644 --- a/Repository/AccountRepository.cs +++ b/Repository/AccountRepository.cs @@ -1,6 +1,9 @@ using Contracts; using Entities; using Entities.Models; +using System; +using System.Collections.Generic; +using System.Linq; namespace Repository { @@ -10,5 +13,18 @@ public AccountRepository(RepositoryContext repositoryContext) :base(repositoryContext) { } + + public IEnumerable GetAllAccounts() + { + return FindAll() + .OrderBy(ac => ac.DateCreated); + } + + public void CreateAccount(Account account) + { + account.Id = Guid.NewGuid(); + Create(account); + Save(); + } } } diff --git a/Tests/OwnerRepositoryTests.cs b/Tests/OwnerRepositoryTests.cs new file mode 100644 index 0000000..81d3133 --- /dev/null +++ b/Tests/OwnerRepositoryTests.cs @@ -0,0 +1,42 @@ +using Contracts; +using Entities.Models; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Tests +{ + public class OwnerRepositoryTests + { + [Fact] + public void GetAllOwners_ReturnsListOfOwners_WithSingleOwner() + { + // Arrange + var mockRepo = new Mock(); + mockRepo.Setup(repo => (repo.GetAllOwners())).Returns(GetOwners()); + + // Act + var result = mockRepo.Object.GetAllOwners().ToList(); + + // Assert + Assert.IsType>(result); + Assert.Single(result); + } + + public List GetOwners() + { + return new List + { + new Owner + { + Id = Guid.NewGuid(), + Name = "John Keen", + DateOfBirth = new DateTime(1980, 12, 05), + Address = "61 Wellfield Road" + } + }; + } + } +} diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj new file mode 100644 index 0000000..114ad4b --- /dev/null +++ b/Tests/Tests.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + + + diff --git a/docker-compose-registry.yml b/docker-compose-registry.yml new file mode 100644 index 0000000..c1b218b --- /dev/null +++ b/docker-compose-registry.yml @@ -0,0 +1,8 @@ +version: '3.1' +services: + my-registry: + ports: + - '5000:5000' + restart: always + container_name: my-registry + image: 'registry:2' \ No newline at end of file diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml new file mode 100644 index 0000000..c84ac12 --- /dev/null +++ b/docker-compose.ci.yml @@ -0,0 +1,18 @@ +version: '3.1' +services: + jenkins: + container_name: jenkins + ports: + - '8080:8080' + - '50000:50000' + image: localhost:5000/jenkins + jenkins-slave: + container_name: jenkins-slave + restart: always + environment: + - 'JENKINS_URL=http://jenkins:8080' + image: localhost:5000/jenkins-slave + volumes: + - /var/run/docker.sock:/var/run/docker.sock + depends_on: + - jenkins \ No newline at end of file diff --git a/docker-compose.integration.yml b/docker-compose.integration.yml new file mode 100644 index 0000000..f91b0f6 --- /dev/null +++ b/docker-compose.integration.yml @@ -0,0 +1,32 @@ +version: '3.1' + +services: + db: + image: mysql:5.7 + environment: + MYSQL_RANDOM_ROOT_PASSWORD: 1 + MYSQL_DATABASE: accountowner + MYSQL_USER: dbuser + MYSQL_PASSWORD: dbuserpassword + DEBUG: 1 + volumes: + - dbdata:/var/lib/mysql + - ./_MySQL_Init_Script:/docker-entrypoint-initdb.d + restart: always + accountownerapp: + depends_on: + - db + image: "accountownerapp:B${BUILD_NUMBER}" + build: + context: . + integration: + depends_on: + - accountownerapp + image: "accountownerapp:test-B${BUILD_NUMBER}" + build: + context: . + dockerfile: Dockerfile.Integration + environment: + - TEAMCITY_PROJECT_NAME +volumes: + dbdata: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a591245 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.0' + +services: + db: + image: mysql:5.7 + environment: + MYSQL_RANDOM_ROOT_PASSWORD: 1 + MYSQL_DATABASE: accountowner + MYSQL_USER: dbuser + MYSQL_PASSWORD: dbuserpassword + volumes: + - dbdata:/var/lib/mysql + - ./_MySQL_Init_Script:/docker-entrypoint-initdb.d + restart: always + + accountownerapp: + depends_on: + - db + image: codemazeblog/accountownerapp + build: + context: . + ports: + - "8080:80" + +volumes: + dbdata: \ No newline at end of file diff --git a/jenkins-docker/master/Dockerfile b/jenkins-docker/master/Dockerfile new file mode 100644 index 0000000..a7ecd22 --- /dev/null +++ b/jenkins-docker/master/Dockerfile @@ -0,0 +1,14 @@ +FROM jenkins/jenkins:latest + +RUN /usr/local/bin/install-plugins.sh git mstest matrix-auth workflow-aggregator docker-workflow blueocean credentials-binding + +ENV JENKINS_USER admin +ENV JENKINS_PASS admin + +# Skip initial setup +ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false + +COPY executors.groovy /usr/share/jenkins/ref/init.groovy.d/ +COPY default-user.groovy /usr/share/jenkins/ref/init.groovy.d/ + +VOLUME /var/jenkins_home diff --git a/jenkins-docker/master/default-user.groovy b/jenkins-docker/master/default-user.groovy new file mode 100644 index 0000000..8d786fe --- /dev/null +++ b/jenkins-docker/master/default-user.groovy @@ -0,0 +1,14 @@ +import jenkins.model.* +import hudson.security.* + +def env = System.getenv() + +def jenkins = Jenkins.getInstance() +jenkins.setSecurityRealm(new HudsonPrivateSecurityRealm(false)) +jenkins.setAuthorizationStrategy(new GlobalMatrixAuthorizationStrategy()) + +def user = jenkins.getSecurityRealm().createAccount(env.JENKINS_USER, env.JENKINS_PASS) +user.save() + +jenkins.getAuthorizationStrategy().add(Jenkins.ADMINISTER, env.JENKINS_USER) +jenkins.save() diff --git a/jenkins-docker/master/executors.groovy b/jenkins-docker/master/executors.groovy new file mode 100644 index 0000000..0f6f064 --- /dev/null +++ b/jenkins-docker/master/executors.groovy @@ -0,0 +1,2 @@ +import jenkins.model.* +Jenkins.instance.setNumExecutors(0) diff --git a/jenkins-docker/slave/Dockerfile b/jenkins-docker/slave/Dockerfile new file mode 100644 index 0000000..ddcaccf --- /dev/null +++ b/jenkins-docker/slave/Dockerfile @@ -0,0 +1,33 @@ +FROM ubuntu:16.04 + +# Install Docker CLI +RUN apt-get update && apt-get install -y apt-transport-https ca-certificates +RUN apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D +RUN echo "deb [arch=amd64] https://download.docker.com/linux/ubuntu xenial stable" > /etc/apt/sources.list.d/docker.list +RUN apt-get update && apt-get install -y docker-ce --allow-unauthenticated + +RUN apt-get update && apt-get install -y openjdk-8-jre curl python python-pip git +RUN easy_install jenkins-webapi + +# Get docker-compose in the agent container + +RUN curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose +RUN mkdir -p /home/jenkins +RUN mkdir -p /var/lib/jenkins + +ADD slave.py /var/lib/jenkins/slave.py + +WORKDIR /home/jenkins + +ENV JENKINS_URL "http://jenkins" +ENV JENKINS_SLAVE_ADDRESS "" +ENV JENKINS_USER "admin" +ENV JENKINS_PASS "admin" +ENV SLAVE_NAME "" +ENV SLAVE_SECRET "" +ENV SLAVE_EXECUTORS "1" +ENV SLAVE_LABELS "docker" +ENV SLAVE_WORING_DIR "" +ENV CLEAN_WORKING_DIR "true" + +CMD [ "python", "-u", "/var/lib/jenkins/slave.py" ] diff --git a/jenkins-docker/slave/slave.py b/jenkins-docker/slave/slave.py new file mode 100644 index 0000000..855d9f2 --- /dev/null +++ b/jenkins-docker/slave/slave.py @@ -0,0 +1,90 @@ +from jenkins import Jenkins, JenkinsError, NodeLaunchMethod +import os +import signal +import sys +import urllib +import subprocess +import shutil +import requests +import time + +slave_jar = '/var/lib/jenkins/slave.jar' +slave_name = os.environ['SLAVE_NAME'] if os.environ['SLAVE_NAME'] != '' else 'docker-slave-' + os.environ['HOSTNAME'] +jnlp_url = os.environ['JENKINS_URL'] + '/computer/' + slave_name + '/slave-agent.jnlp' +slave_jar_url = os.environ['JENKINS_URL'] + '/jnlpJars/slave.jar' +print(slave_jar_url) +process = None + +def clean_dir(dir): + for root, dirs, files in os.walk(dir): + for f in files: + os.unlink(os.path.join(root, f)) + for d in dirs: + shutil.rmtree(os.path.join(root, d)) + +def slave_create(node_name, working_dir, executors, labels): + j = Jenkins(os.environ['JENKINS_URL'], os.environ['JENKINS_USER'], os.environ['JENKINS_PASS']) + j.node_create(node_name, working_dir, num_executors = int(executors), labels = labels, launcher = NodeLaunchMethod.JNLP) + +def slave_delete(node_name): + j = Jenkins(os.environ['JENKINS_URL'], os.environ['JENKINS_USER'], os.environ['JENKINS_PASS']) + j.node_delete(node_name) + +def slave_download(target): + if os.path.isfile(slave_jar): + os.remove(slave_jar) + + loader = urllib.URLopener() + loader.retrieve(os.environ['JENKINS_URL'] + '/jnlpJars/slave.jar', '/var/lib/jenkins/slave.jar') + +def slave_run(slave_jar, jnlp_url): + params = [ 'java', '-jar', slave_jar, '-jnlpUrl', jnlp_url ] + if os.environ['JENKINS_SLAVE_ADDRESS'] != '': + params.extend([ '-connectTo', os.environ['JENKINS_SLAVE_ADDRESS' ] ]) + + if os.environ['SLAVE_SECRET'] == '': + params.extend([ '-jnlpCredentials', os.environ['JENKINS_USER'] + ':' + os.environ['JENKINS_PASS'] ]) + else: + params.extend([ '-secret', os.environ['SLAVE_SECRET'] ]) + return subprocess.Popen(params, stdout=subprocess.PIPE) + +def signal_handler(sig, frame): + if process != None: + process.send_signal(signal.SIGINT) + +signal.signal(signal.SIGINT, signal_handler) +signal.signal(signal.SIGTERM, signal_handler) + +def master_ready(url): + try: + r = requests.head(url, verify=False, timeout=None) + return r.status_code == requests.codes.ok + except: + return False + +while not master_ready(slave_jar_url): + print("Master not ready yet, sleeping for 10sec!") + time.sleep(10) + +slave_download(slave_jar) +print 'Downloaded Jenkins slave jar.' + +if os.environ['SLAVE_WORING_DIR']: + os.setcwd(os.environ['SLAVE_WORING_DIR']) + +if os.environ['CLEAN_WORKING_DIR'] == 'true': + clean_dir(os.getcwd()) + print "Cleaned up working directory." + +if os.environ['SLAVE_NAME'] == '': + slave_create(slave_name, os.getcwd(), os.environ['SLAVE_EXECUTORS'], os.environ['SLAVE_LABELS']) + print 'Created temporary Jenkins slave.' + +process = slave_run(slave_jar, jnlp_url) +print 'Started Jenkins slave with name "' + slave_name + '" and labels [' + os.environ['SLAVE_LABELS'] + '].' +process.wait() + +print 'Jenkins slave stopped.' +if os.environ['SLAVE_NAME'] == '': + slave_delete(slave_name) + print 'Removed temporary Jenkins slave.'