* Migrating to vs 2019

* Support for dockerfile on dotnet core 3.0 preview8

* Dockerfile working with external nuget package, using the container registry SDK

* Pagination support added

* Link header readded

* Readme update

* Error message added to VerifyCredential method
This commit is contained in:
ggonzalere 2019-08-21 17:24:02 -07:00 коммит произвёл GitHub
Родитель 11558e887a
Коммит 4c7512dd72
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
58 изменённых файлов: 14311 добавлений и 868 удалений

2
.gitignore поставляемый
Просмотреть файл

@ -9,3 +9,5 @@ src/**/project.lock.json
src/.vs/
*.swp
*~
*.js
*.js.map

Просмотреть файл

@ -11,21 +11,21 @@ $ docker run -it -p 5000:80 azurecr/web-manager
## Build instructions
Make sure you have installed [dotnet core 3.0](https://dotnet.microsoft.com/download/dotnet-core/3.0)
```bash
$ cd src/ACRManager
$ cd src/WebManager/app
$ npm install
$ npm install -g gulp webpack
$ cd ..
$ dotnet restore
$ dotnet run
```
## Buiding the Docker image:
- Follow previous instructions up to `dotnet run`
## Buiding and running the Docker image:
```bash
$ dotnet publish -c Release
$ docker build -t acrmanager bin/Release/netcoreapp1.0/publish
$ cd src/
$ docker build -t acrmanager -f WebManager/Dockerfile .
$ docker run -p 5000:80 -it --rm acrmanager
```

20
src/.dockerignore Normal file
Просмотреть файл

@ -0,0 +1,20 @@
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.vs
**/.vscode
**/*.*proj.user
**/azds.yaml
**/charts
**/bin
**/obj
**/Dockerfile
**/Dockerfile.develop
**/docker-compose.yml
**/docker-compose.*.yml
**/*.dbmdl
**/*.jfm
**/secrets.dev.yaml
**/values.dev.yaml
**/.toolstarget

Двоичный файл не отображается.

Просмотреть файл

@ -1,9 +1,9 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
# Visual Studio Version 16
VisualStudioVersion = 16.0.28922.388
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "WebManager", "WebManager\WebManager.xproj", "{3CC6F00A-DA65-4CCD-96C1-80054DA8D7DD}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebManager", "WebManager\WebManager.csproj", "{2ECAB0F5-EC44-43A2-BFB4-A02DDFADB731}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -11,12 +11,15 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3CC6F00A-DA65-4CCD-96C1-80054DA8D7DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3CC6F00A-DA65-4CCD-96C1-80054DA8D7DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3CC6F00A-DA65-4CCD-96C1-80054DA8D7DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3CC6F00A-DA65-4CCD-96C1-80054DA8D7DD}.Release|Any CPU.Build.0 = Release|Any CPU
{2ECAB0F5-EC44-43A2-BFB4-A02DDFADB731}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2ECAB0F5-EC44-43A2-BFB4-A02DDFADB731}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2ECAB0F5-EC44-43A2-BFB4-A02DDFADB731}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2ECAB0F5-EC44-43A2-BFB4-A02DDFADB731}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B423CAEA-07FC-486A-9CFA-0D918639E54E}
EndGlobalSection
EndGlobal

Просмотреть файл

@ -1,2 +0,0 @@
docker-compose.yml
Dockerfile

232
src/WebManager/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,232 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
bin/
Bin/
obj/
Obj/
# Visual Studio 2015 cache/options directory
.vs/
/wwwroot/dist/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Microsoft Azure ApplicationInsights config file
ApplicationInsights.config
# Windows Store app package directory
AppPackages/
BundleArtifacts/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
orleans.codegen.cs
/node_modules
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
# FAKE - F# Make
.fake/

Просмотреть файл

@ -1,13 +0,0 @@
sudo: required
dist: trusty
language: node_js
node_js:
- '6'
before_install:
- sudo sh -c 'echo "deb [arch=amd64] https://apt-mo.trafficmanager.net/repos/dotnet-release/ trusty main" > /etc/apt/sources.list.d/dotnetdev.list'
- sudo apt-key adv --keyserver apt-mo.trafficmanager.net --recv-keys 417A0893
- sudo apt-get update
- sudo apt-get install dotnet-dev-1.0.0-preview2-003121
script:
- npm run lint
- npm run test

Просмотреть файл

@ -1,168 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using WebManager.Services;
using WebManager.Utility;
namespace WebManager
{
public class ApiController : Controller
{
private DockerApiService _service;
public ApiController(DockerApiService service)
{
_service = service;
}
public RegistryCredential GetDockerCredential()
{
if (!Request.Headers.ContainsKey("Authorization"))
{
return null;
}
var header = Request.Headers["Authorization"];
if (header.Count < 1)
{
return null;
}
if (!header[0].StartsWith("Basic "))
{
return null;
}
string basicAuth = header[0].Substring("Basic ".Length);
if (!Request.Headers.ContainsKey("Registry"))
{
return null;
}
return new RegistryCredential() { BasicAuth = basicAuth, Registry = Request.Headers["Registry"] };
}
/// <summary>
/// The client should set the following headers:
/// Authorization: Basic
/// Registry: (the name of the registry to access)
/// </summary>
[HttpGet]
public async Task<IActionResult> Catalog()
{
RegistryCredential cred = GetDockerCredential();
if (cred == null)
{
return new UnauthorizedResult();
}
var resp = await _service.Catalog(cred,
Request.QueryString.HasValue ? Request.QueryString.Value : "");
if (resp == null)
{
return new UnauthorizedResult();
}
if (resp.Item3 != null)
{
Response.Headers.Add("Link", resp.Item3);
}
return new ContentResult()
{
Content = resp.Item1,
ContentType = "application/json",
StatusCode = (int) resp.Item2
};
}
[HttpGet]
public async Task<IActionResult> Manifest(string repo, string tag)
{
RegistryCredential cred = GetDockerCredential();
if (cred == null)
{
return new UnauthorizedResult();
}
var resp = await _service.Manifest(cred, repo, tag);
if (resp == null)
{
return new UnauthorizedResult();
}
return new ContentResult()
{
Content = resp.Item1,
ContentType = "application/json",
StatusCode = (int) resp.Item2
};
}
/// <summary>
/// The client should set the following headers:
/// Authorization: Basic
/// Registry: (the name of the registry to access)
/// </summary>
[HttpGet]
public async Task<IActionResult> ListTags(string name)
{
RegistryCredential cred = GetDockerCredential();
if (cred == null)
{
return new UnauthorizedResult();
}
var resp = await _service.ListTags(cred, name,
Request.QueryString.HasValue ? Request.QueryString.Value : "");
if (resp == null)
{
return new UnauthorizedResult();
}
if (resp.Item3 != null)
{
Response.Headers.Add("Link", resp.Item3);
}
return new ContentResult()
{
Content = resp.Item1,
ContentType = "application/json",
StatusCode = (int) resp.Item2
};
}
[HttpGet]
public async Task<IActionResult> VerifyCredential()
{
RegistryCredential cred = GetDockerCredential();
if (cred == null)
{
return new UnauthorizedResult();
}
if (cred.Registry == null)
{
return new UnauthorizedResult();
}
// must have a TLD
if (!cred.Registry.Contains("."))
{
return new UnauthorizedResult();
}
if (await _service.TestCredentials(cred.Registry, cred.BasicAuth))
{
return new OkResult();
}
return new UnauthorizedResult();
}
}
}

Просмотреть файл

@ -1,34 +0,0 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.IO;
namespace WebManager
{
public class AppController : Controller
{
private readonly IHostingEnvironment _hostEnvironment;
private string _indexFile = null;
public AppController(IHostingEnvironment env)
{
_hostEnvironment = env;
}
[HttpGet]
public IActionResult Index(string registry)
{
if (_indexFile == null)
{
_indexFile = System.IO.File.ReadAllText(
Path.Combine(_hostEnvironment.WebRootPath, "index.html"));
}
return new ContentResult()
{
Content = _indexFile,
ContentType = "text/html",
StatusCode = StatusCodes.Status200OK
};
}
}
}

Просмотреть файл

@ -0,0 +1,242 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.ContainerRegistry;
using Microsoft.Azure.ContainerRegistry.Models;
using Newtonsoft.Json;
using System;
using System.Threading;
using System.Threading.Tasks;
using WebManager.Utility;
namespace WebManager.Controllers
{
[Route("v2")]
public class DockerController : Controller
{
public RegistryCredential GetDockerCredential()
{
if (!Request.Headers.ContainsKey("Authorization"))
{
return null;
}
var header = Request.Headers["Authorization"];
if (header.Count < 1)
{
return null;
}
if (!header[0].StartsWith("Basic "))
{
return null;
}
string basicAuth = header[0].Substring("Basic ".Length);
if (!Request.Headers.ContainsKey("Registry"))
{
return null;
}
return new RegistryCredential() { BasicAuth = basicAuth, Registry = Request.Headers["Registry"] };
}
/// <summary>
/// The client should set the following headers:
/// Authorization: Basic
/// Registry: (the name of the registry to access)
/// </summary>
[HttpGet("_catalog")]
public async Task<IActionResult> Catalog([FromQuery(Name = "n")] int n = 10, [FromQuery(Name = "last")] string last = "")
{
RegistryCredential cred = GetDockerCredential();
if (cred == null)
{
return new UnauthorizedResult();
}
var base64EncodedBytes = System.Convert.FromBase64String(cred.BasicAuth);
var decodedAuth = System.Text.Encoding.UTF8.GetString(base64EncodedBytes);
var user = decodedAuth.Split(":")[0];
var password = decodedAuth.Split(":")[1];
int timeoutInMilliseconds = 1500000;
CancellationToken ct = new CancellationTokenSource(timeoutInMilliseconds).Token;
var client = LoginBasic(ct, user, password, cred.Registry);
try
{
var repositories = await client.GetRepositoriesAsync(last, n);
var jsonString = JsonConvert.SerializeObject(repositories);
if (repositories.Names != null && repositories.Names.Count > 0)
{
var lastRepo = repositories.Names[repositories.Names.Count - 1];
var linkHeader = $"</v2/_catalog?last={last}&n={n}&orderby=>; rel=\"next\"";
Response.Headers.Add("Link", linkHeader);
}
return new ContentResult()
{
Content = jsonString,
ContentType = "application/json",
StatusCode = 200
};
}
catch (AcrErrorsException e)
{
return new ContentResult()
{
Content = e.Response.Content,
ContentType = "application/json",
StatusCode = (int)e.Response.StatusCode
};
}
}
[HttpGet("{repo}/manifests/{tag}")]
public async Task<IActionResult> Manifest(string repo, string tag)
{
RegistryCredential cred = GetDockerCredential();
if (cred == null)
{
return new UnauthorizedResult();
}
var base64EncodedBytes = System.Convert.FromBase64String(cred.BasicAuth);
var decodedAuth = System.Text.Encoding.UTF8.GetString(base64EncodedBytes);
var user = decodedAuth.Split(":")[0];
var password = decodedAuth.Split(":")[1];
int timeoutInMilliseconds = 1500000;
CancellationToken ct = new CancellationTokenSource(timeoutInMilliseconds).Token;
var client = LoginBasic(ct, user, password, cred.Registry);
try
{
var acceptString = "application/vnd.docker.distribution.manifest.v2+json";
var manifest = await client.GetManifestAsync(repo, tag, acceptString);
var jsonString = JsonConvert.SerializeObject(manifest);
return new ContentResult()
{
Content = jsonString,
ContentType = "application/json",
StatusCode = 200
};
}
catch (AcrErrorsException e)
{
return new ContentResult()
{
Content = e.Response.Content,
ContentType = "application/json",
StatusCode = (int)e.Response.StatusCode
};
}
}
/// <summary>
/// The client should set the following headers:
/// Authorization: Basic
/// Registry: (the name of the registry to access)
/// </summary>
[HttpGet("{name}/tags/list")]
public async Task<IActionResult> ListTags(string name, [FromQuery(Name = "n")] int n = 10, [FromQuery(Name = "last")] string last = "")
{
RegistryCredential cred = GetDockerCredential();
if (cred == null)
{
return new UnauthorizedResult();
}
var base64EncodedBytes = System.Convert.FromBase64String(cred.BasicAuth);
var decodedAuth = System.Text.Encoding.UTF8.GetString(base64EncodedBytes);
var user = decodedAuth.Split(":")[0];
var password = decodedAuth.Split(":")[1];
int timeoutInMilliseconds = 1500000;
CancellationToken ct = new CancellationTokenSource(timeoutInMilliseconds).Token;
var client = LoginBasic(ct, user, password, cred.Registry);
try
{
var tags = await client.GetAcrTagsAsync(name, last, n);
var jsonString = JsonConvert.SerializeObject(tags);
if (tags.TagsAttributes != null && tags.TagsAttributes.Count > 0)
{
var lastTag = tags.TagsAttributes[tags.TagsAttributes.Count - 1].Name;
var linkHeader = $"</acr/v1/{name}/_tags?last={lastTag}&n={n}&orderby=>; rel=\"next\"";
Response.Headers.Add("Link", linkHeader);
}
return new ContentResult()
{
Content = jsonString,
ContentType = "application/json",
StatusCode = 200
};
}
catch (AcrErrorsException e)
{
return new ContentResult()
{
Content = e.Response.Content,
ContentType = "application/json",
StatusCode = (int)e.Response.StatusCode
};
}
}
[HttpGet]
public async Task<IActionResult> VerifyCredential()
{
RegistryCredential cred = GetDockerCredential();
if (cred == null)
{
return new UnauthorizedResult();
}
if (cred.Registry == null)
{
return new UnauthorizedResult();
}
if (!cred.Registry.Contains("."))
{
return new UnauthorizedResult();
}
var base64EncodedBytes = System.Convert.FromBase64String(cred.BasicAuth);
var decodedAuth = System.Text.Encoding.UTF8.GetString(base64EncodedBytes);
var user = decodedAuth.Split(":")[0];
var password = decodedAuth.Split(":")[1];
int timeoutInMilliseconds = 1500000;
CancellationToken ct = new CancellationTokenSource(timeoutInMilliseconds).Token;
var client = LoginBasic(ct, user, password, cred.Registry);
try
{
await client.GetDockerRegistryV2SupportAsync();
return new OkResult();
}
catch (AcrErrorsException e)
{
return new ContentResult()
{
Content = e.Response.Content,
ContentType = "application/json",
StatusCode = (int)e.Response.StatusCode
};
}
}
private static AzureContainerRegistryClient LoginBasic(CancellationToken ct, string username, string password, string loginUrl)
{
AcrClientCredentials credentials = new AcrClientCredentials(AcrClientCredentials.LoginMode.Basic, loginUrl, username, password, ct);
AzureContainerRegistryClient client = new AzureContainerRegistryClient(credentials);
client.LoginUri = "https://" + loginUrl;
return client;
}
}
}

Просмотреть файл

@ -1,6 +1,25 @@
FROM microsoft/aspnetcore:1.0.1
ENTRYPOINT ["dotnet", "WebManager.dll"]
ARG source=.
FROM mcr.microsoft.com/dotnet/core/aspnet:3.0.0-preview8-buster-slim AS base
WORKDIR /app
EXPOSE 80
COPY $source .
EXPOSE 443
FROM mcr.microsoft.com/dotnet/core/sdk:3.0.100-preview8-buster AS build
RUN apt-get update -yq && apt-get upgrade -yq && apt-get install -yq curl git nano nuget
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - && apt-get install -yq nodejs build-essential
RUN npm install -g npm
WORKDIR /src
COPY . .
WORKDIR "/src/WebManager/app"
RUN rm -rf node_modules
RUN npm install
WORKDIR "/src/WebManager"
RUN dotnet restore
RUN dotnet build -c Release -o /app --no-restore
FROM build AS publish
RUN dotnet publish "WebManager.csproj" -c Release -o /app --no-restore
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "WebManager.dll"]

Просмотреть файл

@ -1,8 +0,0 @@
FROM microsoft/aspnetcore-build:1.0.1
WORKDIR /app
ENV ASPNETCORE_URLS http://+:80
EXPOSE 80
COPY . .
RUN npm install -g webpack
RUN dotnet restore
ENTRYPOINT ["dotnet", "run"]

Просмотреть файл

@ -0,0 +1,26 @@
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>

Просмотреть файл

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace WebManager.Pages
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class ErrorModel : PageModel
{
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}
}

Просмотреть файл

@ -0,0 +1,3 @@
@using WebManager
@namespace WebManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Просмотреть файл

@ -1,6 +1,5 @@
using System.IO;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
namespace WebManager
{
@ -8,14 +7,12 @@ namespace WebManager
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseWebRoot("app")
.UseStartup<Startup>();
}
}

Просмотреть файл

@ -3,27 +3,37 @@
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:5000/",
"sslPort": 0
"applicationUrl": "http://localhost:56488",
"sslPort": 44350
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"WebManager": {
"commandName": "Project",
"launchUrl": "http://localhost:5000/",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "http://localhost:{ServicePort}"
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"environmentVariables": {
"ASPNETCORE_URLS": "https://+:443;http://+:80",
"ASPNETCORE_HTTPS_PORT": "44351"
},
"httpPort": 56489,
"useSSL": true,
"sslPort": 44351
}
}
}

Просмотреть файл

@ -1,137 +0,0 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using WebManager.Utility;
namespace WebManager.Services
{
public class DockerApiService
{
private HttpClientHandler handler;
private HttpClient client;
public DockerApiService()
{
handler = new HttpClientHandler();
handler.Proxy = null;
handler.UseProxy = false;
handler.UseCookies = false;
client = new HttpClient(handler);
}
public async Task<bool> TestCredentials(string registry, string authString)
{
try
{
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get,
new Uri(new Uri("https://" + registry), "/v2"));
message.Headers.Authorization = new AuthenticationHeaderValue("Basic", authString);
var resp = await client.SendAsync(message);
return resp.StatusCode != HttpStatusCode.Unauthorized;
}
catch (HttpRequestException)
{
return false;
}
}
/// <summary>
/// Tries to read the tags for a repository with the given credentials.
/// If authentication failed, returns null.
/// If any other error occured, throws an exception.
/// </summary>
public async Task<Tuple<string, HttpStatusCode, string>> ListTags(RegistryCredential cred, string repoName, string queryString)
{
try
{
string endpoint = $"/v2/{repoName}/tags/list{queryString}";
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get,
new Uri(new Uri("https://" + cred.Registry), endpoint));
message.Headers.Authorization = new AuthenticationHeaderValue("Basic", cred.BasicAuth);
var resp = await client.SendAsync(message);
if (resp.StatusCode == HttpStatusCode.Unauthorized)
{
return null;
}
return Tuple.Create(await resp.Content.ReadAsStringAsync(), resp.StatusCode,
resp.Headers.Contains("Link") ? resp.Headers.GetValues("Link").First() : null);
}
catch (HttpRequestException)
{
return null;
}
}
/// <summary>
/// Tries to read the registry catalog with the given credentials.
/// If authentication failed, returns null.
/// If any other error occured, throws an exception.
/// </summary>
/// <returns>A tuple containing the result, and the HTTP Link header.</returns>
public async Task<Tuple<string, HttpStatusCode, string>> Catalog(RegistryCredential cred, string queryString)
{
try
{
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get,
new Uri(new Uri("https://" + cred.Registry), "/v2/_catalog" + queryString));
message.Headers.Authorization = new AuthenticationHeaderValue("Basic", cred.BasicAuth);
var resp = await client.SendAsync(message);
if (resp.StatusCode == HttpStatusCode.Unauthorized)
{
return null;
}
return Tuple.Create(await resp.Content.ReadAsStringAsync(), resp.StatusCode,
resp.Headers.Contains("Link") ? resp.Headers.GetValues("Link").First() : null);
}
catch (HttpRequestException)
{
return null;
}
}
/// <summary>
/// Reads a manifest.
/// </summary>
public async Task<Tuple<string, HttpStatusCode>> Manifest(RegistryCredential cred, string repo, string tag)
{
try
{
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get,
new Uri(new Uri("https://" + cred.Registry), $"/v2/{repo}/manifests/{tag}"));
message.Headers.Authorization = new AuthenticationHeaderValue("Basic", cred.BasicAuth);
message.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(
"application/vnd.docker.distribution.manifest.v1+json", 0.5));
message.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(
"application/vnd.docker.distribution.manifest.v2+json", 0.6));
var resp = await client.SendAsync(message);
if (resp.StatusCode == HttpStatusCode.Unauthorized)
{
return null;
}
return Tuple.Create(await resp.Content.ReadAsStringAsync(), resp.StatusCode);
}
catch (HttpRequestException)
{
return null;
}
}
}
}

Просмотреть файл

@ -1,90 +1,64 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SpaServices;
using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using WebManager.Services;
namespace WebManager
{
public class Startup
{
public Startup(IHostingEnvironment env)
public Startup(IConfiguration configuration)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
if (env.IsEnvironment("Development"))
{
// This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
builder.AddApplicationInsightsSettings(developerMode: true);
}
builder.AddEnvironmentVariables();
Configuration = builder.Build();
Configuration = configuration;
}
public IConfigurationRoot Configuration { get; }
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddApplicationInsightsTelemetry(Configuration);
services.AddMvc(option => option.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddMvc().AddWebApiConventions();
services.AddSingleton(typeof(DockerApiService));
services.AddOptions();
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "app/build";
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseApplicationInsightsRequestTelemetry();
app.UseApplicationInsightsExceptionTelemetry();
app.UseStaticFiles();
app.UseMvc(routes =>
if (env.IsDevelopment())
{
routes.MapRoute("ACRManager.API.Catalog",
"v2/_catalog",
new { controller = "Api", action = "Catalog" }
);
routes.MapRoute("ACRManager.API.Manifest",
"v2/{repo}/manifests/{tag}",
new { controller = "Api", action = "Manifest" }
);
routes.MapRoute("ACRManager.API.ListTags",
"v2/{name}/tags/list",
new { controller = "Api", action = "ListTags" }
);
routes.MapRoute("ACRManager.API.VerifyCredential",
"v2/",
new { controller = "Api", action = "VerifyCredential" }
);
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
routes.MapRoute("default0",
"",
new { controller = "App", action = "Index" }
);
routes.MapRoute("default1",
"{a}",
new { controller = "App", action = "Index" }
);
routes.MapRoute("default2",
"{a}/{b}",
new { controller = "App", action = "Index" }
);
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseMvc();
app.UseSpa(spa =>
{
spa.Options.SourcePath = "app";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
}
}
}

Просмотреть файл

@ -0,0 +1,80 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
<IsPackable>false</IsPackable>
<SpaRoot>app\</SpaRoot>
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
<RootNamespace>WebManager</RootNamespace>
<ApplicationIcon />
<Win32Resource />
<UserSecretsId>d5c6cdcc-261f-4f2c-9710-82147c854643</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.0.0-preview8.19405.7" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.0.0-preview8.19405.7" />
<PackageReference Include="Microsoft.Azure.Runtime.ContainerRegistry" Version="1.0.0-dev.20190814.1" />
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="3.6.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.7.9" />
</ItemGroup>
<ItemGroup>
<!-- Don't publish the SPA source files, but do show them in the project files list -->
<Content Remove="$(SpaRoot)**" />
<None Remove="$(SpaRoot)**" />
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
</ItemGroup>
<ItemGroup>
<None Remove="app\src\application.tsx" />
<None Remove="app\src\index.tsx" />
<None Remove="app\src\components\login.tsx" />
<None Remove="app\src\services\credential.ts" />
<None Remove="app\src\services\docker.ts" />
</ItemGroup>
<ItemGroup>
<TypeScriptCompile Remove="app\src\components\auth-banner.tsx" />
<TypeScriptCompile Remove="app\src\components\catalog.tsx" />
<TypeScriptCompile Remove="app\src\components\history.ts" />
<TypeScriptCompile Remove="app\src\components\manifest-viewer.tsx" />
<TypeScriptCompile Remove="app\src\components\repository-list-entry.tsx" />
<TypeScriptCompile Remove="app\src\components\repository-list.tsx" />
<TypeScriptCompile Remove="app\src\components\repository-tag-list.tsx" />
<TypeScriptCompile Remove="app\src\components\repository-tag-viewer.tsx" />
<TypeScriptCompile Remove="app\src\components\repository.tsx" />
</ItemGroup>
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
<!-- Ensure Node.js is installed -->
<Exec Command="node --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
</Exec>
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
<Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
</Target>
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="$(SpaRoot)build\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>

Просмотреть файл

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<TypeScriptCompileOnSaveEnabled>False</TypeScriptCompileOnSaveEnabled>
<TypeScriptCompileBlocked>True</TypeScriptCompileBlocked>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>3cc6f00a-da65-4ccd-96c1-80054da8d7dd</ProjectGuid>
<RootNamespace>WebManager</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet.Web\Microsoft.DotNet.Web.targets" Condition="'$(VSToolsPath)' != ''" />
<ProjectExtensions>
<VisualStudio>
<UserProperties />
</VisualStudio>
</ProjectExtensions>
</Project>

21
src/WebManager/app/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,21 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

13305
src/WebManager/app/package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,54 @@
{
"name": "WebManager",
"version": "0.1.0",
"private": true,
"dependencies": {
"@types/axios": "^0.14.0",
"@types/jest": "^24.0.17",
"@types/node": "^12.7.2",
"@types/react": "^16.9.2",
"@types/react-dom": "^16.8.5",
"@types/react-router": "^5.0.3",
"@types/react-router-dom": "^4.3.4",
"axios": "^0.19.0",
"bootstrap": "^4.3.1",
"es6-promise": "^4.2.8",
"jquery": "3.4.1",
"office-ui-fabric-react": "^7.25.0",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-router-bootstrap": "^0.25.0",
"react-router-dom": "^5.0.1",
"react-scripts": "^3.1.1",
"reactstrap": "^8.0.1",
"rimraf": "^3.0.0",
"typescript": "^3.5.3",
"typescript-eslint": "^0.0.1-alpha.0"
},
"devDependencies": {
"ajv": "^6.10.2",
"cross-env": "^5.2.0"
},
"eslintConfig": {
"extends": "react-app"
},
"scripts": {
"start": "rimraf ./build && react-scripts start",
"build": "react-scripts build",
"test": "cross-env CI=true react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"lint": "eslint ./src/"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Двоичные данные
src/WebManager/app/public/favicon.ico Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 31 KiB

Просмотреть файл

@ -23,10 +23,6 @@
<div id="app-loading" class="app-loading ms-fontColor-black"></div>
</div>
<script src="/scripts/react/dist/react.js"></script>
<script src="/scripts/react-dom/dist/react-dom.js"></script>
<script src="/app/dist/bundle.js"></script>
<div class="body-bottom-padding"></div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,15 @@
{
"short_name": "WebManager",
"name": "WebManager",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 2.3 KiB

После

Ширина:  |  Высота:  |  Размер: 2.3 KiB

Просмотреть файл

@ -1,4 +1,4 @@
body {
body {
margin: 0;
}
@ -67,6 +67,7 @@ pre {
.banner-logged-in {
display: block;
}
.banner-logout {
text-decoration: underline;
display: inline-block;
@ -103,7 +104,6 @@ pre {
.login-title {
padding-top: 100px;
margin: auto;
text-align: center;
height: 50px;
}
@ -132,6 +132,7 @@ pre {
.breadcrumb li a {
max-width: 320px !important;
}
.breadcrumb {
margin-bottom: 20px !important;
}
@ -139,9 +140,11 @@ pre {
.repo-list-item::before {
content: none;
}
.repo-list-item:hover {
cursor: default !important;
}
.repo-list-item {
border-top: 1px solid #EEE;
border-bottom: 1px solid #EEE;
@ -150,11 +153,14 @@ pre {
.repo-list-item-tags-item::before {
content: none;
}
.repo-list-item-tags-item:hover {
background-color: #DDD !important;
}
.repo-list-item-tags-item {
}
.repo-list-item-tags-icon {
position: absolute;
left: 10px;
@ -201,46 +207,47 @@ pre {
margin: 0px 0px;
}
.manifest-entry:last-child {
border-bottom: none;
}
.manifest-entry:last-child {
border-bottom: none;
}
.manifest-entry ul {
padding: 0;
margin: 0;
}
.manifest-entry ul {
padding: 0;
margin: 0;
}
.manifest-entry li {
padding-left: 20px;
position: relative;
list-style: none;
margin-bottom: 1em;
border-bottom: 1px solid #AAA;
}
.manifest-entry li {
padding-left: 20px;
position: relative;
list-style: none;
margin-bottom: 1em;
border-bottom: 1px solid #AAA;
}
.manifest-entry li:last-child {
border-bottom: none;
}
.manifest-entry li:last-child {
border-bottom: none;
}
.manifest-entry span {
display: block;
}
.manifest-entry span {
display: block;
}
.manifest-entry pre {
margin: 0;
overflow-x: auto;
display: inline-block;
max-width: 100%;
background: #EEE;
}
.manifest-entry pre {
margin: 0;
overflow-x: auto;
display: inline-block;
max-width: 100%;
background: #EEE;
}
.clickable {
cursor: pointer !important;
}
.clickable:hover {
cursor: pointer !important;
}
.clickable:hover {
cursor: pointer !important;
}
.body-bottom-padding {
padding-bottom: 150px;
}
}

Просмотреть файл

@ -1,7 +1,6 @@
import * as React from "react";
import { Router, Route, browserHistory } from "react-router";
import { Docker } from "../services/docker";
import * as React from "react";
import { Route, Router, Switch } from "react-router";
import history from './history'
import { Catalog } from "./catalog";
import { Repository } from "./repository";
@ -17,11 +16,12 @@ export class Application extends React.Component<IApplicationProps, IApplication
render(): JSX.Element {
return (
<Router history={browserHistory}>
<Route path="/" component={Login} />
<Route path=":registryName" component={Catalog} />
<Route path=":registryName/:repositoryName" component={Repository} />
</Router>
);
<Router history={history} >
<Switch>
<Route exact path='/' component={Login} />
<Route exact path="/:registryName" component={Catalog} />
<Route exact path="/:registryName/:repositoryName" component={Repository} />
</Switch>
</Router>);
}
}

Просмотреть файл

@ -57,13 +57,13 @@ export class AuthBanner extends React.Component<IAuthBannerProps, IAuthBannerSta
onUsernameChange(e: React.FormEvent<HTMLInputElement>): void {
this.setState({
formUsername: e.target.value.replace(/[^\x00-\x7F]/g, ""),
formUsername: (e.target as HTMLInputElement).value.replace(/[^\x00-\x7F]/g, ""),
} as IAuthBannerState);
}
onPasswordChange(e: React.FormEvent<HTMLInputElement>): void {
this.setState({
formPassword: e.target.value.replace(/[^\x00-\x7F]/g, ""),
formPassword: (e.target as HTMLInputElement).value.replace(/[^\x00-\x7F]/g, ""),
} as IAuthBannerState);
}

Просмотреть файл

@ -1,12 +1,12 @@
import * as React from "react";
import { browserHistory } from "react-router";
import history from './history'
import { Breadcrumb, IBreadcrumbItem } from "office-ui-fabric-react/lib/Breadcrumb";
import { Docker } from "../services/docker";
import { AuthBanner } from "./auth-banner";
import { RepositoryList } from "./repository-list";
export interface ICatalogProps { params: any }
export interface ICatalogProps { match: any }
interface ICatalogState { isLoggedIn: boolean, service: Docker }
export class Catalog extends React.Component<ICatalogProps, ICatalogState> {
@ -14,7 +14,7 @@ export class Catalog extends React.Component<ICatalogProps, ICatalogState> {
super(props);
this.state = {
service: new Docker(this.props.params.registryName),
service: new Docker(this.props.match.params.registryName),
isLoggedIn: false
};
}
@ -32,7 +32,7 @@ export class Catalog extends React.Component<ICatalogProps, ICatalogState> {
}
onRepositoryClick(repository: string): void {
browserHistory.push(`/${this.props.params.registryName}/${repository}`);
history.push(`/${this.props.match.params.registryName}/${repository}`);
}
render(): JSX.Element {
@ -47,10 +47,10 @@ export class Catalog extends React.Component<ICatalogProps, ICatalogState> {
{
text: "Home",
key: "1",
onClick: () => browserHistory.push("/")
onClick: () => history.push("/")
},
{
text: this.props.params.registryName,
text: this.props.match.params.registryName,
key: "2"
}
]} className="breadcrumb" />

Просмотреть файл

@ -0,0 +1,3 @@
import { createBrowserHistory } from 'history';
export default createBrowserHistory()

Просмотреть файл

@ -1,15 +1,13 @@
import * as React from "react";
import { browserHistory } from "react-router";
import { CancelTokenSource } from "axios";
import {
Button,
ButtonType
} from "office-ui-fabric-react/lib/Button";
} from "office-ui-fabric-react";
import { RegistryCredentials, CredentialService } from "../services/credential";
import { Docker } from "../services/docker";
import { RepositoryList } from "./repository-list";
import history from './history'
export interface ILoginProps { }
interface ILoginState {
@ -56,19 +54,19 @@ export class Login extends React.Component<ILoginProps, ILoginState> {
onRegistryChange(e: React.FormEvent<HTMLInputElement>): void {
this.setState({
formRegistry: e.target.value.replace(/[^\x00-\x7F]/g, ""),
formRegistry: (e.target as HTMLInputElement).value.replace(/[^\x00-\x7F]/g, ""),
} as ILoginState);
}
onUsernameChange(e: React.FormEvent<HTMLInputElement>): void {
this.setState({
formUsername: e.target.value.replace(/[^\x00-\x7F]/g, ""),
formUsername: (e.target as HTMLInputElement).value.replace(/[^\x00-\x7F]/g, ""),
} as ILoginState);
}
onPasswordChange(e: React.FormEvent<HTMLInputElement>): void {
this.setState({
formPassword: e.target.value.replace(/[^\x00-\x7F]/g, ""),
formPassword: (e.target as HTMLInputElement).value.replace(/[^\x00-\x7F]/g, ""),
} as ILoginState);
}
@ -103,7 +101,7 @@ export class Login extends React.Component<ILoginProps, ILoginState> {
if (success) {
this.credService.setRegistryCredentials(service.registryName, cred);
browserHistory.push("/" + service.registryName);
history.push("/" + service.registryName)
}
else {
this.setState({

Просмотреть файл

@ -51,27 +51,37 @@ export class RepositoryList extends React.Component<IRepositoryListProps, IRepos
this.cancel = this.props.service.createCancelToken();
this.props.service.getRepos(10, lastRepo, this.cancel.token)
.then(value => {
this.cancel = null;
.then((value: { repositories: any; httpLink: any; }) => {
this.cancel = null;
if (!value) return;
if (!value) return;
this.setState((prevState, props) => {
if (prevState.repositories == null) {
prevState.repositories = [];
}
for (let repository of value.repositories) {
prevState.repositories.push(repository);
}
prevState.hasMoreRepositories = value.httpLink !== undefined;
return prevState;
this.setState((prevState) => {
let newRepositories: string[] = []
if (prevState.repositories != null) {
for (let repository of prevState.repositories) {
newRepositories.push(repository);
}
}
if (value.repositories != null) {
for (let repository of value.repositories) {
newRepositories.push(repository);
}
return {
repositories: newRepositories,
hasMoreRepositories: value.httpLink !== undefined
};
}
else {
return {
repositories: prevState.repositories,
hasMoreRepositories: false
};
}
});
}).catch((err: any) => {
this.cancel = null;
});
}).catch((err: any) => {
this.cancel = null;
});
}
renderRepositories(): JSX.Element[] {

Просмотреть файл

@ -57,7 +57,7 @@ export class RepositoryTagList extends React.Component<IRepositoryTagListProps,
return this.state.tags.map((tag: string) => (
<li key={tag}
className="ms-ListItem is-selectable repo-list-item-tags-item"
onClick={() => { this.props.onTagClick(tag); } }>
onClick={() => { this.props.onTagClick(tag); }}>
<i className="ms-Icon ms-Icon--Tag repo-list-item-tags-icon" aria-hidden="true"></i>
{tag}
</li>
@ -88,23 +88,34 @@ export class RepositoryTagList extends React.Component<IRepositoryTagListProps,
this.cancel = this.props.service.createCancelToken();
this.props.service.getTagsForRepo(this.props.repositoryName, 10, last, this.cancel.token)
.then(value => {
.then((value: { tags: any; httpLink: any; }) => {
this.cancel = null;
if (!value) return;
this.setState((prevState, props) => {
if (prevState.tags == null) {
prevState.tags = [];
this.setState((prevState) => {
let newTags: string[] = []
if (prevState.tags != null) {
for (let tag of prevState.tags) {
newTags.push(tag);
}
}
for (let tag of value.tags) {
prevState.tags.push(tag);
if (value.tags != null) {
for (let tag of value.tags) {
newTags.push(tag.name);
}
return {
tags: newTags,
hasMoreTags: value.httpLink !== undefined
};
}
else {
return {
tags: prevState.tags,
hasMoreTags: false
};
}
prevState.hasMoreTags = value.httpLink !== undefined;
return prevState;
});
}).catch(err => {
}).catch((err: any) => {
this.cancel = null;
this.setState({
error: err.toString()

Просмотреть файл

@ -32,7 +32,7 @@ export class RepositoryTagViewer extends React.Component<IRepositoryTagViewerPro
this.cancel = this.props.service.createCancelToken();
this.props.service.getManifest(this.props.repositoryName, tag, this.cancel.token)
.then(value => {
.then((value: {manifest: any}) => {
this.cancel = null;
if (!value) return;
@ -40,7 +40,7 @@ export class RepositoryTagViewer extends React.Component<IRepositoryTagViewerPro
manifest: value.manifest,
manifestError: null
} as IRepositoryTagViewerState);
}).catch(err => {
}).catch((err: any) => {
this.cancel = null;
try {

Просмотреть файл

@ -1,12 +1,12 @@
import * as React from "react";
import { browserHistory } from "react-router";
import history from "./history";
import { Breadcrumb, IBreadcrumbItem } from "office-ui-fabric-react/lib/Breadcrumb";
import { Docker } from "../services/docker";
import { AuthBanner } from "./auth-banner";
import { RepositoryTagViewer } from "./repository-tag-viewer";
export interface IRepositoryProps { params: any }
export interface IRepositoryProps { match: any }
interface IRepositoryState { isLoggedIn: boolean, service: Docker }
export class Repository extends React.Component<IRepositoryProps, IRepositoryState> {
@ -14,7 +14,7 @@ export class Repository extends React.Component<IRepositoryProps, IRepositorySta
super(props);
this.state = {
service: new Docker(this.props.params.registryName),
service: new Docker(this.props.match.params.registryName),
isLoggedIn: false
};
}
@ -43,15 +43,15 @@ export class Repository extends React.Component<IRepositoryProps, IRepositorySta
{
text: "Home",
key: "1",
onClick: () => browserHistory.push("/")
onClick: () => history.push("/")
},
{
text: this.state.service.registryName,
key: "2",
onClick: () => browserHistory.push("/" + this.props.params.registryName)
onClick: () => history.push("/" + this.props.match.params.registryName)
},
{
text: this.props.params.repositoryName,
text: this.props.match.params.repositoryName,
key: "3"
}
]} className="breadcrumb" />
@ -61,7 +61,7 @@ export class Repository extends React.Component<IRepositoryProps, IRepositorySta
<div>
<RepositoryTagViewer
service={this.state.service}
repositoryName={this.props.params.repositoryName}
repositoryName={this.props.match.params.repositoryName}
/>
</div>
}

Просмотреть файл

@ -1,4 +1,4 @@
import * as React from "react";
import * as React from "react";
import * as ReactDOM from "react-dom";
import * as ES6Promise from "es6-promise";

1
src/WebManager/app/src/react-app-env.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1 @@
/// <reference types="react-scripts" />

Просмотреть файл

@ -1,4 +1,4 @@
export class RegistryCredentials {
export class RegistryCredentials {
basicAuth: string;
username: string;
}

Просмотреть файл

@ -19,7 +19,7 @@ export class Docker {
// note: we can't use the Link HTTP header yet because we need to forward requests
// through the server in order to satisfy CORS policies
getRepos(maxResults: number | null = 10, last: string = null, cancel: CancelToken = null):
Promise<{ repositories: string[], httpLink: string }> {
any {
let cred: RegistryCredentials = this.credService.getRegistryCredentials(this.registryName);
if (!cred) {
return Promise.resolve({ repositories: null, httpLink: null });
@ -28,7 +28,7 @@ export class Docker {
let config: AxiosRequestConfig = {
cancelToken: cancel,
baseURL: this.registryEndpoint,
params: { },
params: {},
headers: {
"Registry": this.registryName,
"Authorization": "Basic " + cred.basicAuth
@ -57,7 +57,7 @@ export class Docker {
}
getManifest(repo: string, tag: string, cancel: CancelToken = null):
Promise<{ manifest: string }> {
any {
let cred: RegistryCredentials = this.credService.getRegistryCredentials(this.registryName);
if (!cred) {
return Promise.resolve({ manifest: null });
@ -66,7 +66,7 @@ export class Docker {
let config: AxiosRequestConfig = {
cancelToken: cancel,
baseURL: this.registryEndpoint,
params: { },
params: {},
headers: {
"Registry": this.registryName,
"Accept": "application/vnd.docker.distribution.manifest.v2+json; 0.6, " +
@ -92,7 +92,7 @@ export class Docker {
// for whatever reason, the docker registry doesn't respect the tag pagination API...
// this will just return all the tags at once
getTagsForRepo(repo: string, maxResults: number | null = 10, last: string = null, cancel: CancelToken = null):
Promise<{ tags: string[], httpLink: string }> {
any {
let cred: RegistryCredentials = this.credService.getRegistryCredentials(this.registryName);
if (!cred) {
return Promise.resolve({ tags: null, httpLink: null });
@ -101,7 +101,7 @@ export class Docker {
let config: AxiosRequestConfig = {
cancelToken: cancel,
baseURL: this.registryEndpoint,
params: { },
params: {},
headers: {
"Registry": this.registryName,
"Authorization": "Basic " + cred.basicAuth
@ -134,7 +134,7 @@ export class Docker {
});
}
tryAuthenticate(cred: RegistryCredentials, cancel: CancelToken = null): Promise<boolean> {
tryAuthenticate(cred: RegistryCredentials, cancel: CancelToken = null): any {
let config: AxiosRequestConfig = {
cancelToken: cancel,
baseURL: this.registryEndpoint,

Просмотреть файл

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"strictPropertyInitialization": false,
"strictNullChecks": false,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": [
"src"
]
}

Просмотреть файл

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

Просмотреть файл

@ -1,10 +1,8 @@
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}

Просмотреть файл

@ -1,17 +0,0 @@
version: '2'
services:
webmanager:
build:
args:
source: obj/Docker/empty/
labels:
- "com.microsoft.visualstudio.targetoperatingsystem=linux"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- DOTNET_USE_POLLING_FILE_WATCHER=1
volumes:
- .:/app
- ~/.nuget/packages:/root/.nuget/packages:ro
- ~/clrdbg:/clrdbg:ro
entrypoint: tail -f /dev/null

Просмотреть файл

@ -1,9 +0,0 @@
version: '2'
services:
webmanager:
labels:
- "com.microsoft.visualstudio.targetoperatingsystem=linux"
volumes:
- ~/clrdbg:/clrdbg:ro
entrypoint: tail -f /dev/null

Просмотреть файл

@ -1,10 +0,0 @@
version: '2'
services:
webmanager:
image: user/webmanager${TAG}
build:
context: .
dockerfile: Dockerfile
ports:
- "80"

Просмотреть файл

@ -1,8 +0,0 @@
{
"projects": [
"WebManager"
],
"sdk": {
"version": "1.0.0-preview2-003131"
}
}

Просмотреть файл

@ -1,43 +0,0 @@
var gulp = require("gulp");
var clean = require("gulp-clean");
var destPath = "./wwwroot/libs/";
var exec = require("child_process").exec;
gulp.task("clean", function () {
return gulp.src("./wwwroot/*")
.pipe(clean({ force: true }));
});
gulp.task("scripts", () => {
gulp.src([
"react/dist/**",
"react-dom/dist/**"
], {
cwd: "./node_modules/**"
}).pipe(gulp.dest("./wwwroot/scripts/"));
});
gulp.task("static", function () {
return gulp.src("./static/**").pipe(gulp.dest("./wwwroot/"));
});
gulp.task("webpack", function (done) {
return exec("webpack", function (err, stdout, stderr) {
gulp.src("./app/dist/*").pipe(gulp.dest("./wwwroot/app/dist/"));
console.log(stdout);
done(err);
});
});
gulp.task("watch", ["scripts", "watch.webpack", "watch.static"]);
gulp.task("watch.webpack", ["webpack"], function () {
return gulp.watch("./app/src/**/*", ["webpack"]);
});
gulp.task("watch.static", ["static"], function () {
return gulp.watch("./static/**/*", ["static"]);
});
gulp.task("default", ["scripts", "webpack", "static"]);

Просмотреть файл

@ -1,13 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=759670
// for the documentation about the jsconfig.json format
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"allowSyntheticDefaultImports": true
},
"exclude": [
"./node_modules",
"./app/dist"
]
}

Просмотреть файл

@ -1,30 +0,0 @@
{
"name": "WebManager",
"version": "1.0.0",
"description": "Web management portal for Azure Container Registry services.",
"repository": "https://github.com/azure/acr-web-manager",
"main": "src/index.tsx",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"devDependencies": {
"@types/history": "2.0.39",
"@types/react": "0.14.43",
"@types/react-dom": "^0.14.18",
"@types/react-router": "2.0.38",
"es6-promise": "4.0.5",
"axios": "0.15.2",
"react": "^15.3.2",
"react-dom": "^15.3.2",
"react-router": "3.0.0",
"react-json-tree": "0.10.0",
"office-ui-fabric-react": "0.63.4",
"gulp": "3.9.1",
"gulp-clean": "0.3.2",
"source-map-loader": "^0.1.5",
"ts-loader": "0.9.5",
"typescript": "2.0.9"
}
}

Просмотреть файл

@ -1,62 +0,0 @@
{
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.0.1",
"type": "platform"
},
"Microsoft.ApplicationInsights.AspNetCore": "1.0.0",
"Microsoft.AspNetCore.Mvc": "1.0.1",
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.0.1",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
"Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0",
"Microsoft.Extensions.Logging": "1.0.0",
"Microsoft.Extensions.Logging.Console": "1.0.0",
"Microsoft.Extensions.Logging.Debug": "1.0.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0",
"Microsoft.AspNetCore.StaticFiles": "1.0.0",
"Newtonsoft.Json": "9.0.1",
"Microsoft.AspNetCore.Mvc.WebApiCompatShim": "1.0.1"
},
"tools": {
"Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final"
},
"frameworks": {
"netcoreapp1.0": {
"imports": [
"dotnet5.6",
"portable-net45+win8"
]
}
},
"buildOptions": {
"emitEntryPoint": true,
"preserveCompilationContext": true,
"debugType": "portable"
},
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true
}
},
"publishOptions": {
"include": [
"wwwroot",
"**/*.cshtml",
"appsettings.json",
"web.config",
"docker-compose.yml",
"Dockerfile",
".dockerignore"
]
},
"scripts": {
"precompile": [
"gulp"
],
"postpublish": [
"dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%"
]
}
}

Просмотреть файл

@ -1,20 +0,0 @@
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react",
"baseUrl": "./src",
"paths": {
"*": [
"*"
]
}
},
"exclude": [
"./dist/",
"./node_modules/"
]
}

Просмотреть файл

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!--
Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380
-->
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/>
</handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false"/>
</system.webServer>
</configuration>

Просмотреть файл

@ -1,35 +0,0 @@
module.exports = {
entry: "./app/src/index.tsx",
output: {
filename: "./app/dist/bundle.js",
},
// Enable sourcemaps for debugging webpack's output.
devtool: "source-map",
resolve: {
// Add '.ts' and '.tsx' as resolvable extensions.
"extensions": ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"]
},
module: {
loaders: [
// All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'.
{ test: /\.tsx?$/, loader: "ts-loader" }
],
preLoaders: [
// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
{ test: /\.js$/, loader: "source-map-loader" }
]
},
// When importing a module whose path matches one of the following, just
// assume a corresponding global variable exists and use that instead.
// This is important because it allows us to avoid bundling all of our
// dependencies, which allows browsers to cache those libraries between builds.
externals: {
"react": "React",
"react-dom": "ReactDOM"
},
};

6
src/nuget.config Normal file
Просмотреть файл

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="NewFeed" value="Packages" />
</packageSources>
</configuration>