T gigo/update (#10)
* 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:
Родитель
11558e887a
Коммит
4c7512dd72
|
@ -9,3 +9,5 @@ src/**/project.lock.json
|
|||
src/.vs/
|
||||
*.swp
|
||||
*~
|
||||
*.js
|
||||
*.js.map
|
||||
|
|
14
README.md
14
README.md
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
@ -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
|
Двоичные данные
src/Packages/Microsoft.Azure.Runtime.ContainerRegistry.1.0.0-dev.20190814.1.nupkg
Normal file
Двоичные данные
src/Packages/Microsoft.Azure.Runtime.ContainerRegistry.1.0.0-dev.20190814.1.nupkg
Normal file
Двоичный файл не отображается.
|
@ -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
|
|
@ -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,89 +1,63 @@
|
|||
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>
|
|
@ -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*
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 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,45 +207,46 @@ 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";
|
||||
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="NewFeed" value="Packages" />
|
||||
</packageSources>
|
||||
</configuration>
|
Загрузка…
Ссылка в новой задаче