diff --git a/.gitignore b/.gitignore index 2394333d9..a1e88f9b8 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,7 @@ *_i.c *_p.c Ankh.Load -Backup* +Backup*/ CVS/ PrecompiledWeb/ UpgradeLog*.* @@ -58,3 +58,10 @@ source/packages source/OctopusTools.v2.ncrunchsolution *.orig *.userprefs +*.lock.json +.vs +.vscode +/tools/ +/artifacts/ +/publish/ +TestResult.xml \ No newline at end of file diff --git a/source/Octopus.Cli/Octopus.Cli.nuspec b/BuildAssets/OctopusTools.nuspec similarity index 88% rename from source/Octopus.Cli/Octopus.Cli.nuspec rename to BuildAssets/OctopusTools.nuspec index 60bce278b..58e8335c3 100644 --- a/source/Octopus.Cli/Octopus.Cli.nuspec +++ b/BuildAssets/OctopusTools.nuspec @@ -20,8 +20,6 @@ true - - - + diff --git a/source/Octopus.Cli/init.ps1 b/BuildAssets/init.ps1 similarity index 100% rename from source/Octopus.Cli/init.ps1 rename to BuildAssets/init.ps1 diff --git a/GitVersionConfig.yaml b/GitVersionConfig.yaml deleted file mode 100644 index c1a0dc7bf..000000000 --- a/GitVersionConfig.yaml +++ /dev/null @@ -1 +0,0 @@ -mode: ContinuousDeployment \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..38433ec27 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,11 @@ +version: '{build}' +configuration: Release +platform: Any CPU +build_script: +- cmd: build.cmd +artifacts: +- path: artifacts\**\*.* +cache: +- '%USERPROFILE%\.nuget\packages' +test: off +deploy: off diff --git a/build.cake b/build.cake new file mode 100644 index 000000000..a49189c8b --- /dev/null +++ b/build.cake @@ -0,0 +1,207 @@ +////////////////////////////////////////////////////////////////////// +// TOOLS +////////////////////////////////////////////////////////////////////// +#tool "nuget:?package=GitVersion.CommandLine&prerelease" +#tool "nuget:?package=ILRepack" +#addin "nuget:?package=Newtonsoft.Json" +#addin "nuget:?package=SharpCompress" + +using Path = System.IO.Path; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using IO = System.IO; +using SharpCompress; +using SharpCompress.Common; +using SharpCompress.Writer; + +////////////////////////////////////////////////////////////////////// +// ARGUMENTS +////////////////////////////////////////////////////////////////////// +var target = Argument("target", "Default"); +var configuration = Argument("configuration", "Release"); + +/////////////////////////////////////////////////////////////////////////////// +// GLOBAL VARIABLES +/////////////////////////////////////////////////////////////////////////////// +var publishDir = "./publish"; +var artifactsDir = "./artifacts"; +var assetDir = "./BuildAssets"; +var globalAssemblyFile = "./source/Octo/Properties/AssemblyInfo.cs"; +var projectToPublish = "./source/Octo"; +var projectToPublishProjectJson = Path.Combine(projectToPublish, "project.json"); +var octopusClientFolder = "./source/Octopus.Client"; +var isContinuousIntegrationBuild = !BuildSystem.IsLocalBuild; +var octoPublishFolder = Path.Combine(publishDir, "Octo"); +var octoMergedFolder = Path.Combine(publishDir, "OctoMerged"); + +var gitVersionInfo = GitVersion(new GitVersionSettings { + OutputType = GitVersionOutput.Json +}); + +var nugetVersion = gitVersionInfo.NuGetVersion; + +/////////////////////////////////////////////////////////////////////////////// +// SETUP / TEARDOWN +/////////////////////////////////////////////////////////////////////////////// +Setup(context => +{ + Information("Building OctopusClients v{0}", nugetVersion); +}); + +Teardown(context => +{ + Information("Finished running tasks."); +}); + +////////////////////////////////////////////////////////////////////// +// PRIVATE TASKS +////////////////////////////////////////////////////////////////////// + +Task("__Default") + .IsDependentOn("__Clean") + .IsDependentOn("__Restore") + .IsDependentOn("__UpdateAssemblyVersionInformation") + .IsDependentOn("__Build") + .IsDependentOn("__Test") + .IsDependentOn("__Publish") + .IsDependentOn("__PackNuget"); + +Task("__Clean") + .Does(() => +{ + CleanDirectory(artifactsDir); + CleanDirectory(publishDir); + CleanDirectories("./source/**/bin"); + CleanDirectories("./source/**/obj"); +}); + +Task("__Restore") + .Does(() => DotNetCoreRestore()); + +Task("__UpdateAssemblyVersionInformation") + .WithCriteria(isContinuousIntegrationBuild) + .Does(() => +{ + GitVersion(new GitVersionSettings { + UpdateAssemblyInfo = true, + UpdateAssemblyInfoFilePath = globalAssemblyFile + }); + + Information("AssemblyVersion -> {0}", gitVersionInfo.AssemblySemVer); + Information("AssemblyFileVersion -> {0}", $"{gitVersionInfo.MajorMinorPatch}.0"); + Information("AssemblyInformationalVersion -> {0}", gitVersionInfo.InformationalVersion); + if(BuildSystem.IsRunningOnTeamCity) + BuildSystem.TeamCity.SetBuildNumber(gitVersionInfo.NuGetVersion); + if(BuildSystem.IsRunningOnAppVeyor) + AppVeyor.UpdateBuildVersion(gitVersionInfo.NuGetVersion); +}); + +Task("__Build") + .IsDependentOn("__UpdateProjectJsonVersion") + .IsDependentOn("__UpdateAssemblyVersionInformation") + .Does(() => +{ + DotNetCoreBuild("**/project.json", new DotNetCoreBuildSettings + { + Configuration = configuration + }); +}); + +Task("__Test") + .Does(() => +{ + GetFiles("**/*Tests/project.json") + .ToList() + .ForEach(testProjectFile => + { + DotNetCoreTest(testProjectFile.ToString(), new DotNetCoreTestSettings + { + Configuration = configuration, + WorkingDirectory = Path.GetDirectoryName(testProjectFile.ToString()) + }); + }); +}); + +Task("__UpdateProjectJsonVersion") + .WithCriteria(isContinuousIntegrationBuild) + .Does(() => +{ + Information("Updating {0} version -> {1}", projectToPublishProjectJson, nugetVersion); + ModifyJson(Path.Combine(octopusClientFolder, "project.json"), json => json["version"] = nugetVersion); + ModifyJson(projectToPublishProjectJson, json => json["version"] = nugetVersion); +}); + +private void ModifyJson(string jsonFile, Action modify) +{ + var json = JsonConvert.DeserializeObject(IO.File.ReadAllText(jsonFile)); + modify(json); + IO.File.WriteAllText(jsonFile, JsonConvert.SerializeObject(json, Formatting.Indented)); +} + + +Task("__Publish") + .Does(() => +{ + DotNetCorePublish(projectToPublish, new DotNetCorePublishSettings + { + Configuration = configuration, + OutputDirectory = octoPublishFolder + }); +}); + +Task("__MergeOctoExe") + .Does(() => { + CreateDirectory(octoMergedFolder); + ILRepack( + Path.Combine(octoMergedFolder, "Octo.exe"), + Path.Combine(octoPublishFolder, "Octo.exe"), + IO.Directory.EnumerateFiles(octoPublishFolder, "*.dll").Select(f => (FilePath) f), + new ILRepackSettings { + Internalize = true, + Libs = new List() { octoPublishFolder } + } + ); + DeleteFile(Path.Combine(octoMergedFolder, "Octo.pdb")); + CopyFileToDirectory(Path.Combine(octoPublishFolder, "Octo.exe.config"), octoMergedFolder); + }); + +Task("__PackNuget") + .IsDependentOn("__Publish") + .IsDependentOn("__PackOctopusToolsNuget") + .IsDependentOn("__PackClientNuget"); + +Task("__PackClientNuget") + .Does(() => { + DotNetCorePack(octopusClientFolder, new DotNetCorePackSettings { + Configuration = configuration, + OutputDirectory = artifactsDir + }); + }); + +Task("__PackOctopusToolsNuget") + .IsDependentOn("__MergeOctoExe") + .Does(() => { + var nugetPackDir = Path.Combine(publishDir, "nuget"); + var nuspecFile = "OctopusTools.nuspec"; + + CopyDirectory(octoMergedFolder, nugetPackDir); + CopyFileToDirectory(Path.Combine(assetDir, "init.ps1"), nugetPackDir); + CopyFileToDirectory(Path.Combine(assetDir, nuspecFile), nugetPackDir); + + NuGetPack(Path.Combine(nugetPackDir, nuspecFile), new NuGetPackSettings { + Version = nugetVersion, + OutputDirectory = artifactsDir + }); + }); + + +////////////////////////////////////////////////////////////////////// +// TASKS +////////////////////////////////////////////////////////////////////// +Task("Default") + .IsDependentOn("__Default"); + +////////////////////////////////////////////////////////////////////// +// EXECUTION +////////////////////////////////////////////////////////////////////// +RunTarget(target); diff --git a/build.cmd b/build.cmd index 479c6687e..f77be6b72 100644 --- a/build.cmd +++ b/build.cmd @@ -1,3 +1,24 @@ -@echo off +@ECHO OFF +REM see http://joshua.poehls.me/powershell-batch-file-wrapper/ -PowerShell.exe -ExecutionPolicy Bypass -File "tools\Build.ps1" +SET SCRIPTNAME=%~d0%~p0%~n0.ps1 +SET ARGS=%* +IF [%ARGS%] NEQ [] GOTO ESCAPE_ARGS + +:POWERSHELL +PowerShell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy Unrestricted -Command "& { $ErrorActionPreference = 'Stop'; & '%SCRIPTNAME%' @args; EXIT $LASTEXITCODE }" %ARGS% +EXIT /B %ERRORLEVEL% + +:ESCAPE_ARGS +SET ARGS=%ARGS:"=\"% +SET ARGS=%ARGS:`=``% +SET ARGS=%ARGS:'=`'% +SET ARGS=%ARGS:$=`$% +SET ARGS=%ARGS:{=`}% +SET ARGS=%ARGS:}=`}% +SET ARGS=%ARGS:(=`(% +SET ARGS=%ARGS:)=`)% +SET ARGS=%ARGS:,=`,% +SET ARGS=%ARGS:^%=% + +GOTO POWERSHELL \ No newline at end of file diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 000000000..ab54dc8a7 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,189 @@ +########################################################################## +# This is the Cake bootstrapper script for PowerShell. +# This file was downloaded from https://github.com/cake-build/resources +# Feel free to change this file to fit your needs. +########################################################################## + +<# + +.SYNOPSIS +This is a Powershell script to bootstrap a Cake build. + +.DESCRIPTION +This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) +and execute your Cake build script with the parameters you provide. + +.PARAMETER Script +The build script to execute. +.PARAMETER Target +The build script target to run. +.PARAMETER Configuration +The build configuration to use. +.PARAMETER Verbosity +Specifies the amount of information to be displayed. +.PARAMETER Experimental +Tells Cake to use the latest Roslyn release. +.PARAMETER WhatIf +Performs a dry run of the build script. +No tasks will be executed. +.PARAMETER Mono +Tells Cake to use the Mono scripting engine. +.PARAMETER SkipToolPackageRestore +Skips restoring of packages. +.PARAMETER ScriptArgs +Remaining arguments are added here. + +.LINK +http://cakebuild.net + +#> + +[CmdletBinding()] +Param( + [string]$Script = "build.cake", + [string]$Target = "Default", + [ValidateSet("Release", "Debug")] + [string]$Configuration = "Release", + [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] + [string]$Verbosity = "Verbose", + [switch]$Experimental = $true, + [Alias("DryRun","Noop")] + [switch]$WhatIf, + [switch]$Mono, + [switch]$SkipToolPackageRestore, + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$ScriptArgs +) + +[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null +function MD5HashFile([string] $filePath) +{ + if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) + { + return $null + } + + [System.IO.Stream] $file = $null; + [System.Security.Cryptography.MD5] $md5 = $null; + try + { + $md5 = [System.Security.Cryptography.MD5]::Create() + $file = [System.IO.File]::OpenRead($filePath) + return [System.BitConverter]::ToString($md5.ComputeHash($file)) + } + finally + { + if ($file -ne $null) + { + $file.Dispose() + } + } +} + +Write-Host "Preparing to run build script..." + +if(!$PSScriptRoot){ + $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent +} + +$TOOLS_DIR = Join-Path $PSScriptRoot "tools" +$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" +$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" +$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" +$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" +$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" + +# Should we use mono? +$UseMono = ""; +if($Mono.IsPresent) { + Write-Verbose -Message "Using the Mono based scripting engine." + $UseMono = "-mono" +} + +# Should we use the new Roslyn? +$UseExperimental = ""; +if($Experimental.IsPresent -and !($Mono.IsPresent)) { + Write-Verbose -Message "Using experimental version of Roslyn." + $UseExperimental = "-experimental" +} + +# Is this a dry run? +$UseDryRun = ""; +if($WhatIf.IsPresent) { + $UseDryRun = "-dryrun" +} + +# Make sure tools folder exists +if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { + Write-Verbose -Message "Creating tools directory..." + New-Item -Path $TOOLS_DIR -Type directory | out-null +} + +# Make sure that packages.config exist. +if (!(Test-Path $PACKAGES_CONFIG)) { + Write-Verbose -Message "Downloading packages.config..." + try { (New-Object System.Net.WebClient).DownloadFile("http://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch { + Throw "Could not download packages.config." + } +} + +# Try find NuGet.exe in path if not exists +if (!(Test-Path $NUGET_EXE)) { + Write-Verbose -Message "Trying to find nuget.exe in PATH..." + $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_) } + $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 + if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { + Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." + $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName + } +} + +# Try download NuGet.exe if not exists +if (!(Test-Path $NUGET_EXE)) { + Write-Verbose -Message "Downloading NuGet.exe..." + try { + (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) + } catch { + Throw "Could not download NuGet.exe." + } +} + +# Save nuget.exe path to environment to be available to child processed +$ENV:NUGET_EXE = $NUGET_EXE + +# Restore tools from NuGet? +if(-Not $SkipToolPackageRestore.IsPresent) { + Push-Location + Set-Location $TOOLS_DIR + + # Check for changes in packages.config and remove installed tools if true. + [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) + if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or + ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { + Write-Verbose -Message "Missing or changed package.config hash..." + Remove-Item * -Recurse -Exclude packages.config,nuget.exe + } + + Write-Verbose -Message "Restoring tools from NuGet..." + $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" + + if ($LASTEXITCODE -ne 0) { + Throw "An error occured while restoring NuGet tools." + } + else + { + $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" + } + Write-Verbose -Message ($NuGetOutput | out-string) + Pop-Location +} + +# Make sure that Cake has been installed. +if (!(Test-Path $CAKE_EXE)) { + Throw "Could not find Cake.exe at $CAKE_EXE" +} + +# Start Cake +Write-Host "Running build script..." +Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs" +exit $LASTEXITCODE \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 000000000..6e8f207c8 --- /dev/null +++ b/build.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash + +########################################################################## +# This is the Cake bootstrapper script for Linux and OS X. +# This file was downloaded from https://github.com/cake-build/resources +# Feel free to change this file to fit your needs. +########################################################################## + +# Define directories. +SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +TOOLS_DIR=$SCRIPT_DIR/tools +NUGET_EXE=$TOOLS_DIR/nuget.exe +CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe +PACKAGES_CONFIG=$TOOLS_DIR/packages.config +PACKAGES_CONFIG_MD5=$TOOLS_DIR/packages.config.md5sum + +# Define md5sum or md5 depending on Linux/OSX +MD5_EXE= +if [[ "$(uname -s)" == "Darwin" ]]; then + MD5_EXE="md5 -r" +else + MD5_EXE="md5sum" +fi + +# Define default arguments. +SCRIPT="build.cake" +TARGET="Default" +CONFIGURATION="Release" +VERBOSITY="verbose" +DRYRUN= +SHOW_VERSION=false +SCRIPT_ARGUMENTS=() + +# Parse arguments. +for i in "$@"; do + case $1 in + -s|--script) SCRIPT="$2"; shift ;; + -t|--target) TARGET="$2"; shift ;; + -c|--configuration) CONFIGURATION="$2"; shift ;; + -v|--verbosity) VERBOSITY="$2"; shift ;; + -d|--dryrun) DRYRUN="-dryrun" ;; + --version) SHOW_VERSION=true ;; + --) shift; SCRIPT_ARGUMENTS+=("$@"); break ;; + *) SCRIPT_ARGUMENTS+=("$1") ;; + esac + shift +done + +# Make sure the tools folder exist. +if [ ! -d "$TOOLS_DIR" ]; then + mkdir "$TOOLS_DIR" +fi + +# Make sure that packages.config exist. +if [ ! -f "$TOOLS_DIR/packages.config" ]; then + echo "Downloading packages.config..." + curl -Lsfo "$TOOLS_DIR/packages.config" http://cakebuild.net/download/bootstrapper/packages + if [ $? -ne 0 ]; then + echo "An error occured while downloading packages.config." + exit 1 + fi +fi + +# Download NuGet if it does not exist. +if [ ! -f "$NUGET_EXE" ]; then + echo "Downloading NuGet..." + curl -Lsfo "$NUGET_EXE" https://dist.nuget.org/win-x86-commandline/latest/nuget.exe + if [ $? -ne 0 ]; then + echo "An error occured while downloading nuget.exe." + exit 1 + fi +fi + +# Restore tools from NuGet. +pushd "$TOOLS_DIR" >/dev/null +if [ ! -f $PACKAGES_CONFIG_MD5 ] || [ "$( cat $PACKAGES_CONFIG_MD5 | sed 's/\r$//' )" != "$( $MD5_EXE $PACKAGES_CONFIG | awk '{ print $1 }' )" ]; then + find . -type d ! -name . | xargs rm -rf +fi + +mono "$NUGET_EXE" install -ExcludeVersion +if [ $? -ne 0 ]; then + echo "Could not restore NuGet packages." + exit 1 +fi + +$MD5_EXE $PACKAGES_CONFIG | awk '{ print $1 }' >| $PACKAGES_CONFIG_MD5 + +popd >/dev/null + +# Make sure that Cake has been installed. +if [ ! -f "$CAKE_EXE" ]; then + echo "Could not find Cake.exe at '$CAKE_EXE'." + exit 1 +fi + +# Start Cake +if $SHOW_VERSION; then + exec mono "$CAKE_EXE" -version +else + exec mono "$CAKE_EXE" $SCRIPT -verbosity=$VERBOSITY -configuration=$CONFIGURATION -target=$TARGET $DRYRUN "${SCRIPT_ARGUMENTS[@]}" +fi \ No newline at end of file diff --git a/gitversion.yml b/gitversion.yml new file mode 100644 index 000000000..c31148888 --- /dev/null +++ b/gitversion.yml @@ -0,0 +1 @@ +mode: Mainline \ No newline at end of file diff --git a/source/Dockerfile b/source/Dockerfile deleted file mode 100644 index 8639d5ecf..000000000 --- a/source/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM mono:onbuild -ENTRYPOINT [ "mono", "Octo.exe" ] -CMD [ "help" ] diff --git a/source/Octopus.Cli.Tests/Client/OctopusSessionFactoryFixture.cs b/source/Octo.Tests/Client/OctopusSessionFactoryFixture.cs similarity index 100% rename from source/Octopus.Cli.Tests/Client/OctopusSessionFactoryFixture.cs rename to source/Octo.Tests/Client/OctopusSessionFactoryFixture.cs diff --git a/source/Octopus.Cli.Tests/Commands/ApiCommandFixture.cs b/source/Octo.Tests/Commands/ApiCommandFixture.cs similarity index 100% rename from source/Octopus.Cli.Tests/Commands/ApiCommandFixture.cs rename to source/Octo.Tests/Commands/ApiCommandFixture.cs diff --git a/source/Octopus.Cli.Tests/Commands/ApiCommandFixtureBase.cs b/source/Octo.Tests/Commands/ApiCommandFixtureBase.cs similarity index 100% rename from source/Octopus.Cli.Tests/Commands/ApiCommandFixtureBase.cs rename to source/Octo.Tests/Commands/ApiCommandFixtureBase.cs diff --git a/source/Octopus.Cli.Tests/Commands/CleanEnvironmentCommandFixture.cs b/source/Octo.Tests/Commands/CleanEnvironmentCommandFixture.cs similarity index 85% rename from source/Octopus.Cli.Tests/Commands/CleanEnvironmentCommandFixture.cs rename to source/Octo.Tests/Commands/CleanEnvironmentCommandFixture.cs index 0e2c33f39..6bb1dcc0a 100644 --- a/source/Octopus.Cli.Tests/Commands/CleanEnvironmentCommandFixture.cs +++ b/source/Octo.Tests/Commands/CleanEnvironmentCommandFixture.cs @@ -5,6 +5,7 @@ using Octopus.Cli.Commands; using Octopus.Cli.Infrastructure; using Octopus.Client.Model; +using FluentAssertions; #pragma warning disable 618 namespace Octopus.Cli.Tests.Commands @@ -104,26 +105,32 @@ public void ShouldRemoveMachinesBelongingToMultipleEnvironmentsInsteadOfDeleting Repository.Machines.Received().Delete(machineList[1]); } - [Test, ExpectedException(typeof(CommandException), ExpectedMessage = "Please specify an environment name using the parameter: --environment=XYZ")] + [Test] public void ShouldNotCleanEnvironmentWithMissingEnvironmentArgs() { - listMachinesCommand.Execute(CommandLineArgs.ToArray()); + Action exec = () => listMachinesCommand.Execute(CommandLineArgs.ToArray()); + exec.ShouldThrow() + .WithMessage("Please specify an environment name using the parameter: --environment=XYZ"); } - [Test, ExpectedException(typeof(CommandException), ExpectedMessage = "Please specify a status using the parameter: --status or --health-status")] + [Test] public void ShouldNotCleanEnvironmentWithMissingStatusArgs() { CommandLineArgs.Add("-environment=Development"); - listMachinesCommand.Execute(CommandLineArgs.ToArray()); + Action exec = () => listMachinesCommand.Execute(CommandLineArgs.ToArray()); + exec.ShouldThrow() + .WithMessage("Please specify a status using the parameter: --status or --health-status"); } - [Test, ExpectedException(typeof(CouldNotFindException), ExpectedMessage = "Could not find the specified environment; either it does not exist or you lack permissions to view it.")] + [Test] public void ShouldNotCleanTheEnvironmentIfEnvironmentNotFound() { CommandLineArgs.Add("-environment=Development"); CommandLineArgs.Add("-status=Offline"); - listMachinesCommand.Execute(CommandLineArgs.ToArray()); + Action exec = () => listMachinesCommand.Execute(CommandLineArgs.ToArray()); + exec.ShouldThrow() + .WithMessage("Could not find the specified environment; either it does not exist or you lack permissions to view it."); } } } diff --git a/source/Octopus.Cli.Tests/Commands/CreateChannelCommandFixture.cs b/source/Octo.Tests/Commands/CreateChannelCommandFixture.cs similarity index 100% rename from source/Octopus.Cli.Tests/Commands/CreateChannelCommandFixture.cs rename to source/Octo.Tests/Commands/CreateChannelCommandFixture.cs diff --git a/source/Octopus.Cli.Tests/Commands/CreateReleaseCommandFixture.cs b/source/Octo.Tests/Commands/CreateReleaseCommandFixture.cs similarity index 100% rename from source/Octopus.Cli.Tests/Commands/CreateReleaseCommandFixture.cs rename to source/Octo.Tests/Commands/CreateReleaseCommandFixture.cs diff --git a/source/Octopus.Cli.Tests/Commands/DeleteAutoDeployOverrideCommandFixture.cs b/source/Octo.Tests/Commands/DeleteAutoDeployOverrideCommandFixture.cs similarity index 100% rename from source/Octopus.Cli.Tests/Commands/DeleteAutoDeployOverrideCommandFixture.cs rename to source/Octo.Tests/Commands/DeleteAutoDeployOverrideCommandFixture.cs diff --git a/source/Octopus.Cli.Tests/Commands/DeployReleaseCommandTestFixture.cs b/source/Octo.Tests/Commands/DeployReleaseCommandTestFixture.cs similarity index 100% rename from source/Octopus.Cli.Tests/Commands/DeployReleaseCommandTestFixture.cs rename to source/Octo.Tests/Commands/DeployReleaseCommandTestFixture.cs diff --git a/source/Octopus.Cli.Tests/Commands/DummyApiCommand.cs b/source/Octo.Tests/Commands/DummyApiCommand.cs similarity index 100% rename from source/Octopus.Cli.Tests/Commands/DummyApiCommand.cs rename to source/Octo.Tests/Commands/DummyApiCommand.cs diff --git a/source/Octopus.Cli.Tests/Commands/HelpCommandFixture.cs b/source/Octo.Tests/Commands/HelpCommandFixture.cs similarity index 100% rename from source/Octopus.Cli.Tests/Commands/HelpCommandFixture.cs rename to source/Octo.Tests/Commands/HelpCommandFixture.cs diff --git a/source/Octopus.Cli.Tests/Commands/ListEnvironmentsCommandFixture.cs b/source/Octo.Tests/Commands/ListEnvironmentsCommandFixture.cs similarity index 100% rename from source/Octopus.Cli.Tests/Commands/ListEnvironmentsCommandFixture.cs rename to source/Octo.Tests/Commands/ListEnvironmentsCommandFixture.cs diff --git a/source/Octopus.Cli.Tests/Commands/ListMachinesCommandFixture.cs b/source/Octo.Tests/Commands/ListMachinesCommandFixture.cs similarity index 98% rename from source/Octopus.Cli.Tests/Commands/ListMachinesCommandFixture.cs rename to source/Octo.Tests/Commands/ListMachinesCommandFixture.cs index 412953d78..6e5069ba7 100644 --- a/source/Octopus.Cli.Tests/Commands/ListMachinesCommandFixture.cs +++ b/source/Octo.Tests/Commands/ListMachinesCommandFixture.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using FluentAssertions; using NSubstitute; using NUnit.Framework; using Octopus.Cli.Commands; @@ -310,13 +311,15 @@ public void ShouldSupportStateFilters() Log.Received().Information(MachineLogFormat, "PC01466", "Healthy - Disabled", "Machines-002", "Development"); } - [Test, ExpectedException(typeof(CommandException), ExpectedMessage = "The `--health-status` parameter is only available on Octopus Server instances from 3.4.0 onwards.")] + [Test] public void ShouldThrowIfUsingHealthStatusPre34() { CommandLineArgs.Add("--health-status=Online"); Repository.Client.RootDocument.Version = "3.1.0"; - listMachinesCommand.Execute(CommandLineArgs.ToArray()); + Action exec = () => listMachinesCommand.Execute(CommandLineArgs.ToArray()); + exec.ShouldThrow() + .WithMessage("The `--health-status` parameter is only available on Octopus Server instances from 3.4.0 onwards."); } [Test] diff --git a/source/Octopus.Cli.Tests/Commands/ListProjectsCommandFixture.cs b/source/Octo.Tests/Commands/ListProjectsCommandFixture.cs similarity index 100% rename from source/Octopus.Cli.Tests/Commands/ListProjectsCommandFixture.cs rename to source/Octo.Tests/Commands/ListProjectsCommandFixture.cs diff --git a/source/Octopus.Cli.Tests/Commands/ListReleasesCommandFixture.cs b/source/Octo.Tests/Commands/ListReleasesCommandFixture.cs similarity index 100% rename from source/Octopus.Cli.Tests/Commands/ListReleasesCommandFixture.cs rename to source/Octo.Tests/Commands/ListReleasesCommandFixture.cs diff --git a/source/Octopus.Cli.Tests/Commands/OptionsFixture.cs b/source/Octo.Tests/Commands/OptionsFixture.cs similarity index 100% rename from source/Octopus.Cli.Tests/Commands/OptionsFixture.cs rename to source/Octo.Tests/Commands/OptionsFixture.cs diff --git a/source/Octopus.Cli.Tests/Commands/OverrideAutoDeployCommandFixture.cs b/source/Octo.Tests/Commands/OverrideAutoDeployCommandFixture.cs similarity index 100% rename from source/Octopus.Cli.Tests/Commands/OverrideAutoDeployCommandFixture.cs rename to source/Octo.Tests/Commands/OverrideAutoDeployCommandFixture.cs diff --git a/source/Octopus.Cli.Tests/Commands/PackageVersionResolverFixture.cs b/source/Octo.Tests/Commands/PackageVersionResolverFixture.cs similarity index 100% rename from source/Octopus.Cli.Tests/Commands/PackageVersionResolverFixture.cs rename to source/Octo.Tests/Commands/PackageVersionResolverFixture.cs diff --git a/source/Octopus.Cli.Tests/Commands/Resources/CreateRelease.config.txt b/source/Octo.Tests/Commands/Resources/CreateRelease.config.txt similarity index 100% rename from source/Octopus.Cli.Tests/Commands/Resources/CreateRelease.config.txt rename to source/Octo.Tests/Commands/Resources/CreateRelease.config.txt diff --git a/source/Octopus.Cli.Tests/Commands/SpeakCommand.cs b/source/Octo.Tests/Commands/SpeakCommand.cs similarity index 100% rename from source/Octopus.Cli.Tests/Commands/SpeakCommand.cs rename to source/Octo.Tests/Commands/SpeakCommand.cs diff --git a/source/Octopus.Cli.Tests/Extensions/TimeSpanExtensionsFixture.cs b/source/Octo.Tests/Extensions/TimeSpanExtensionsFixture.cs similarity index 100% rename from source/Octopus.Cli.Tests/Extensions/TimeSpanExtensionsFixture.cs rename to source/Octo.Tests/Extensions/TimeSpanExtensionsFixture.cs diff --git a/source/Octopus.Cli.Tests/Helpers/SemanticVersionInfo.cs b/source/Octo.Tests/Helpers/SemanticVersionInfo.cs similarity index 100% rename from source/Octopus.Cli.Tests/Helpers/SemanticVersionInfo.cs rename to source/Octo.Tests/Helpers/SemanticVersionInfo.cs diff --git a/source/Octopus.Cli.Tests/Helpers/TestCommandExtensions.cs b/source/Octo.Tests/Helpers/TestCommandExtensions.cs similarity index 100% rename from source/Octopus.Cli.Tests/Helpers/TestCommandExtensions.cs rename to source/Octo.Tests/Helpers/TestCommandExtensions.cs diff --git a/source/Octo.Tests/Octo.Tests.xproj b/source/Octo.Tests/Octo.Tests.xproj new file mode 100644 index 000000000..9baf07fef --- /dev/null +++ b/source/Octo.Tests/Octo.Tests.xproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + b05e3858-5607-46db-876c-4e1f5728a4bb + Octopus.Cli.Tests + .\obj + .\bin\ + + + 2.0 + + + \ No newline at end of file diff --git a/source/Octopus.Cli.Tests/Properties/AssemblyInfo.cs b/source/Octo.Tests/Properties/AssemblyInfo.cs similarity index 100% rename from source/Octopus.Cli.Tests/Properties/AssemblyInfo.cs rename to source/Octo.Tests/Properties/AssemblyInfo.cs diff --git a/source/Octopus.Cli.Tests/Util/UriExtensionsFixture.cs b/source/Octo.Tests/Util/UriExtensionsFixture.cs similarity index 100% rename from source/Octopus.Cli.Tests/Util/UriExtensionsFixture.cs rename to source/Octo.Tests/Util/UriExtensionsFixture.cs diff --git a/source/Octopus.Cli.Tests/app.config b/source/Octo.Tests/app.config similarity index 100% rename from source/Octopus.Cli.Tests/app.config rename to source/Octo.Tests/app.config diff --git a/source/Octo.Tests/project.json b/source/Octo.Tests/project.json new file mode 100644 index 000000000..db41d2d8b --- /dev/null +++ b/source/Octo.Tests/project.json @@ -0,0 +1,32 @@ +{ + "version": "0.0.0-*", + "dependencies": { + "GitVersionTask": "3.3.0", + "Newtonsoft.Json": "9.0.1", + "NSubstitute": "1.7.2.0", + "FluentAssertions": "4.12.0", + "NUnit": "3.4.1", + "Serilog": "2.1.0", + "dotnet-test-nunit": "3.4.0-beta-2", + "Octopus.Client": { + "target": "project" + }, + "Octo": { + "target": "project" + } + }, + "testRunner": "nunit", + "frameworks": { + "net452": { + "frameworkAssemblies": { + "System.Core": "*", + "System.Net": "*", + "System.Xml.Linq": "*", + "System.Data.DataSetExtensions": "*", + "Microsoft.CSharp": "*", + "System.Data": "*", + "System.Xml": "*" + } + } + } +} \ No newline at end of file diff --git a/source/Octo.exe.sln b/source/Octo.exe.sln deleted file mode 100644 index 95cab3b2f..000000000 --- a/source/Octo.exe.sln +++ /dev/null @@ -1,49 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Octopus.Cli", "Octopus.Cli\Octopus.Cli.csproj", "{D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Octopus.Cli.Tests", "Octopus.Cli.Tests\Octopus.Cli.Tests.csproj", "{B05E3858-5607-46DB-876C-4E1F5728A4BB}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A88CC6CE-8758-46E3-B646-26088F238774}" - ProjectSection(SolutionItems) = preProject - Solution Items\SolutionInfo.cs = Solution Items\SolutionInfo.cs - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|Mixed Platforms = Debug|Mixed Platforms - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|Mixed Platforms = Release|Mixed Platforms - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}.Debug|Any CPU.ActiveCfg = Debug|x86 - {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}.Debug|x86.ActiveCfg = Debug|x86 - {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}.Debug|x86.Build.0 = Debug|x86 - {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}.Release|Any CPU.ActiveCfg = Release|x86 - {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}.Release|Mixed Platforms.Build.0 = Release|x86 - {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}.Release|x86.ActiveCfg = Release|x86 - {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}.Release|x86.Build.0 = Release|x86 - {B05E3858-5607-46DB-876C-4E1F5728A4BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B05E3858-5607-46DB-876C-4E1F5728A4BB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B05E3858-5607-46DB-876C-4E1F5728A4BB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {B05E3858-5607-46DB-876C-4E1F5728A4BB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {B05E3858-5607-46DB-876C-4E1F5728A4BB}.Debug|x86.ActiveCfg = Debug|Any CPU - {B05E3858-5607-46DB-876C-4E1F5728A4BB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B05E3858-5607-46DB-876C-4E1F5728A4BB}.Release|Any CPU.Build.0 = Release|Any CPU - {B05E3858-5607-46DB-876C-4E1F5728A4BB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {B05E3858-5607-46DB-876C-4E1F5728A4BB}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {B05E3858-5607-46DB-876C-4E1F5728A4BB}.Release|x86.ActiveCfg = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/source/Octo.exe.sln.DotSettings b/source/Octo.exe.sln.DotSettings deleted file mode 100644 index 542dd4c63..000000000 --- a/source/Octo.exe.sln.DotSettings +++ /dev/null @@ -1,3 +0,0 @@ - - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> \ No newline at end of file diff --git a/source/Octopus.Cli/Commands/ApiCommand.cs b/source/Octo/Commands/ApiCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/ApiCommand.cs rename to source/Octo/Commands/ApiCommand.cs diff --git a/source/Octopus.Cli/Commands/ChannelVersionRuleTester.cs b/source/Octo/Commands/ChannelVersionRuleTester.cs similarity index 100% rename from source/Octopus.Cli/Commands/ChannelVersionRuleTester.cs rename to source/Octo/Commands/ChannelVersionRuleTester.cs diff --git a/source/Octopus.Cli/Commands/CleanEnvironmentCommand.cs b/source/Octo/Commands/CleanEnvironmentCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/CleanEnvironmentCommand.cs rename to source/Octo/Commands/CleanEnvironmentCommand.cs diff --git a/source/Octopus.Cli/Commands/CreateAutoDeployOverrideCommand.cs b/source/Octo/Commands/CreateAutoDeployOverrideCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/CreateAutoDeployOverrideCommand.cs rename to source/Octo/Commands/CreateAutoDeployOverrideCommand.cs diff --git a/source/Octopus.Cli/Commands/CreateChannelCommand.cs b/source/Octo/Commands/CreateChannelCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/CreateChannelCommand.cs rename to source/Octo/Commands/CreateChannelCommand.cs diff --git a/source/Octopus.Cli/Commands/CreateEnvironmentCommand.cs b/source/Octo/Commands/CreateEnvironmentCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/CreateEnvironmentCommand.cs rename to source/Octo/Commands/CreateEnvironmentCommand.cs diff --git a/source/Octopus.Cli/Commands/CreateProjectCommand.cs b/source/Octo/Commands/CreateProjectCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/CreateProjectCommand.cs rename to source/Octo/Commands/CreateProjectCommand.cs diff --git a/source/Octopus.Cli/Commands/CreateReleaseCommand.cs b/source/Octo/Commands/CreateReleaseCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/CreateReleaseCommand.cs rename to source/Octo/Commands/CreateReleaseCommand.cs diff --git a/source/Octopus.Cli/Commands/DeleteAutoDeployOverrideCommand.cs b/source/Octo/Commands/DeleteAutoDeployOverrideCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/DeleteAutoDeployOverrideCommand.cs rename to source/Octo/Commands/DeleteAutoDeployOverrideCommand.cs diff --git a/source/Octopus.Cli/Commands/DeleteReleasesCommand.cs b/source/Octo/Commands/DeleteReleasesCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/DeleteReleasesCommand.cs rename to source/Octo/Commands/DeleteReleasesCommand.cs diff --git a/source/Octopus.Cli/Commands/DeployReleaseCommand.cs b/source/Octo/Commands/DeployReleaseCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/DeployReleaseCommand.cs rename to source/Octo/Commands/DeployReleaseCommand.cs diff --git a/source/Octopus.Cli/Commands/DeploymentCommandBase.cs b/source/Octo/Commands/DeploymentCommandBase.cs similarity index 100% rename from source/Octopus.Cli/Commands/DeploymentCommandBase.cs rename to source/Octo/Commands/DeploymentCommandBase.cs diff --git a/source/Octopus.Cli/Commands/DumpDeploymentsCommand.cs b/source/Octo/Commands/DumpDeploymentsCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/DumpDeploymentsCommand.cs rename to source/Octo/Commands/DumpDeploymentsCommand.cs diff --git a/source/Octopus.Cli/Commands/ExportCommand.cs b/source/Octo/Commands/ExportCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/ExportCommand.cs rename to source/Octo/Commands/ExportCommand.cs diff --git a/source/Octopus.Cli/Commands/HealthStatusProvider.cs b/source/Octo/Commands/HealthStatusProvider.cs similarity index 100% rename from source/Octopus.Cli/Commands/HealthStatusProvider.cs rename to source/Octo/Commands/HealthStatusProvider.cs diff --git a/source/Octopus.Cli/Commands/HelpCommand.cs b/source/Octo/Commands/HelpCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/HelpCommand.cs rename to source/Octo/Commands/HelpCommand.cs diff --git a/source/Octopus.Cli/Commands/IChannelVersionRuleTester.cs b/source/Octo/Commands/IChannelVersionRuleTester.cs similarity index 100% rename from source/Octopus.Cli/Commands/IChannelVersionRuleTester.cs rename to source/Octo/Commands/IChannelVersionRuleTester.cs diff --git a/source/Octopus.Cli/Commands/IPackageBuilder.cs b/source/Octo/Commands/IPackageBuilder.cs similarity index 100% rename from source/Octopus.Cli/Commands/IPackageBuilder.cs rename to source/Octo/Commands/IPackageBuilder.cs diff --git a/source/Octopus.Cli/Commands/IPackageVersionResolver.cs b/source/Octo/Commands/IPackageVersionResolver.cs similarity index 100% rename from source/Octopus.Cli/Commands/IPackageVersionResolver.cs rename to source/Octo/Commands/IPackageVersionResolver.cs diff --git a/source/Octopus.Cli/Commands/IReleasePlanBuilder.cs b/source/Octo/Commands/IReleasePlanBuilder.cs similarity index 100% rename from source/Octopus.Cli/Commands/IReleasePlanBuilder.cs rename to source/Octo/Commands/IReleasePlanBuilder.cs diff --git a/source/Octopus.Cli/Commands/ImportCommand.cs b/source/Octo/Commands/ImportCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/ImportCommand.cs rename to source/Octo/Commands/ImportCommand.cs diff --git a/source/Octopus.Cli/Commands/ListEnvironmentsCommand.cs b/source/Octo/Commands/ListEnvironmentsCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/ListEnvironmentsCommand.cs rename to source/Octo/Commands/ListEnvironmentsCommand.cs diff --git a/source/Octopus.Cli/Commands/ListLatestDeploymentsCommand.cs b/source/Octo/Commands/ListLatestDeploymentsCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/ListLatestDeploymentsCommand.cs rename to source/Octo/Commands/ListLatestDeploymentsCommand.cs diff --git a/source/Octopus.Cli/Commands/ListMachinesCommand.cs b/source/Octo/Commands/ListMachinesCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/ListMachinesCommand.cs rename to source/Octo/Commands/ListMachinesCommand.cs diff --git a/source/Octopus.Cli/Commands/ListProjectsCommand.cs b/source/Octo/Commands/ListProjectsCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/ListProjectsCommand.cs rename to source/Octo/Commands/ListProjectsCommand.cs diff --git a/source/Octopus.Cli/Commands/ListReleasesCommand.cs b/source/Octo/Commands/ListReleasesCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/ListReleasesCommand.cs rename to source/Octo/Commands/ListReleasesCommand.cs diff --git a/source/Octopus.Cli/Commands/ListTenantsCommand.cs b/source/Octo/Commands/ListTenantsCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/ListTenantsCommand.cs rename to source/Octo/Commands/ListTenantsCommand.cs diff --git a/source/Octopus.Cli/Commands/NuGet/PackageBuilder.cs b/source/Octo/Commands/NuGet/PackageBuilder.cs similarity index 100% rename from source/Octopus.Cli/Commands/NuGet/PackageBuilder.cs rename to source/Octo/Commands/NuGet/PackageBuilder.cs diff --git a/source/Octopus.Cli/Commands/NuGetPackageBuilder.cs b/source/Octo/Commands/NuGetPackageBuilder.cs similarity index 100% rename from source/Octopus.Cli/Commands/NuGetPackageBuilder.cs rename to source/Octo/Commands/NuGetPackageBuilder.cs diff --git a/source/Octopus.Cli/Commands/OptionGroup.cs b/source/Octo/Commands/OptionGroup.cs similarity index 100% rename from source/Octopus.Cli/Commands/OptionGroup.cs rename to source/Octo/Commands/OptionGroup.cs diff --git a/source/Octopus.Cli/Commands/Options.cs b/source/Octo/Commands/Options.cs similarity index 100% rename from source/Octopus.Cli/Commands/Options.cs rename to source/Octo/Commands/Options.cs diff --git a/source/Octopus.Cli/Commands/PackCommand.cs b/source/Octo/Commands/PackCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/PackCommand.cs rename to source/Octo/Commands/PackCommand.cs diff --git a/source/Octopus.Cli/Commands/PackageVersionResolver.cs b/source/Octo/Commands/PackageVersionResolver.cs similarity index 100% rename from source/Octopus.Cli/Commands/PackageVersionResolver.cs rename to source/Octo/Commands/PackageVersionResolver.cs diff --git a/source/Octopus.Cli/Commands/ProjectExport.cs b/source/Octo/Commands/ProjectExport.cs similarity index 100% rename from source/Octopus.Cli/Commands/ProjectExport.cs rename to source/Octo/Commands/ProjectExport.cs diff --git a/source/Octopus.Cli/Commands/PromoteReleaseCommand.cs b/source/Octo/Commands/PromoteReleaseCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/PromoteReleaseCommand.cs rename to source/Octo/Commands/PromoteReleaseCommand.cs diff --git a/source/Octopus.Cli/Commands/PushCommand.cs b/source/Octo/Commands/PushCommand.cs similarity index 100% rename from source/Octopus.Cli/Commands/PushCommand.cs rename to source/Octo/Commands/PushCommand.cs diff --git a/source/Octopus.Cli/Commands/ReleasePlan.cs b/source/Octo/Commands/ReleasePlan.cs similarity index 100% rename from source/Octopus.Cli/Commands/ReleasePlan.cs rename to source/Octo/Commands/ReleasePlan.cs diff --git a/source/Octopus.Cli/Commands/ReleasePlanBuilder.cs b/source/Octo/Commands/ReleasePlanBuilder.cs similarity index 100% rename from source/Octopus.Cli/Commands/ReleasePlanBuilder.cs rename to source/Octo/Commands/ReleasePlanBuilder.cs diff --git a/source/Octopus.Cli/Commands/ReleasePlanItem.cs b/source/Octo/Commands/ReleasePlanItem.cs similarity index 100% rename from source/Octopus.Cli/Commands/ReleasePlanItem.cs rename to source/Octo/Commands/ReleasePlanItem.cs diff --git a/source/Octopus.Cli/Commands/TaskOutputProgressPrinter.cs b/source/Octo/Commands/TaskOutputProgressPrinter.cs similarity index 100% rename from source/Octopus.Cli/Commands/TaskOutputProgressPrinter.cs rename to source/Octo/Commands/TaskOutputProgressPrinter.cs diff --git a/source/Octopus.Cli/Commands/ZipPackageBuilder.cs b/source/Octo/Commands/ZipPackageBuilder.cs similarity index 100% rename from source/Octopus.Cli/Commands/ZipPackageBuilder.cs rename to source/Octo/Commands/ZipPackageBuilder.cs diff --git a/source/Octopus.Cli/Diagnostics/LogExtensions.cs b/source/Octo/Diagnostics/LogExtensions.cs similarity index 100% rename from source/Octopus.Cli/Diagnostics/LogExtensions.cs rename to source/Octo/Diagnostics/LogExtensions.cs diff --git a/source/Octopus.Cli/Diagnostics/LoggingModule.cs b/source/Octo/Diagnostics/LoggingModule.cs similarity index 100% rename from source/Octopus.Cli/Diagnostics/LoggingModule.cs rename to source/Octo/Diagnostics/LoggingModule.cs diff --git a/source/Octopus.Cli/Exporters/BaseExporter.cs b/source/Octo/Exporters/BaseExporter.cs similarity index 100% rename from source/Octopus.Cli/Exporters/BaseExporter.cs rename to source/Octo/Exporters/BaseExporter.cs diff --git a/source/Octopus.Cli/Exporters/ExportMetadata.cs b/source/Octo/Exporters/ExportMetadata.cs similarity index 100% rename from source/Octopus.Cli/Exporters/ExportMetadata.cs rename to source/Octo/Exporters/ExportMetadata.cs diff --git a/source/Octopus.Cli/Exporters/ExporterAttribute.cs b/source/Octo/Exporters/ExporterAttribute.cs similarity index 100% rename from source/Octopus.Cli/Exporters/ExporterAttribute.cs rename to source/Octo/Exporters/ExporterAttribute.cs diff --git a/source/Octopus.Cli/Exporters/ExporterLocator.cs b/source/Octo/Exporters/ExporterLocator.cs similarity index 100% rename from source/Octopus.Cli/Exporters/ExporterLocator.cs rename to source/Octo/Exporters/ExporterLocator.cs diff --git a/source/Octopus.Cli/Exporters/FileSystemExporter.cs b/source/Octo/Exporters/FileSystemExporter.cs similarity index 100% rename from source/Octopus.Cli/Exporters/FileSystemExporter.cs rename to source/Octo/Exporters/FileSystemExporter.cs diff --git a/source/Octopus.Cli/Exporters/IExporter.cs b/source/Octo/Exporters/IExporter.cs similarity index 100% rename from source/Octopus.Cli/Exporters/IExporter.cs rename to source/Octo/Exporters/IExporter.cs diff --git a/source/Octopus.Cli/Exporters/IExporterLocator.cs b/source/Octo/Exporters/IExporterLocator.cs similarity index 100% rename from source/Octopus.Cli/Exporters/IExporterLocator.cs rename to source/Octo/Exporters/IExporterLocator.cs diff --git a/source/Octopus.Cli/Exporters/IExporterMetadata.cs b/source/Octo/Exporters/IExporterMetadata.cs similarity index 100% rename from source/Octopus.Cli/Exporters/IExporterMetadata.cs rename to source/Octo/Exporters/IExporterMetadata.cs diff --git a/source/Octopus.Cli/Exporters/ProjectExporter.cs b/source/Octo/Exporters/ProjectExporter.cs similarity index 100% rename from source/Octopus.Cli/Exporters/ProjectExporter.cs rename to source/Octo/Exporters/ProjectExporter.cs diff --git a/source/Octopus.Cli/Exporters/ReleaseExporter.cs b/source/Octo/Exporters/ReleaseExporter.cs similarity index 100% rename from source/Octopus.Cli/Exporters/ReleaseExporter.cs rename to source/Octo/Exporters/ReleaseExporter.cs diff --git a/source/Octopus.Cli/Extensions/AttributeExtensions.cs b/source/Octo/Extensions/AttributeExtensions.cs similarity index 100% rename from source/Octopus.Cli/Extensions/AttributeExtensions.cs rename to source/Octo/Extensions/AttributeExtensions.cs diff --git a/source/Octopus.Cli/Extensions/DynamicExtensions.cs b/source/Octo/Extensions/DynamicExtensions.cs similarity index 100% rename from source/Octopus.Cli/Extensions/DynamicExtensions.cs rename to source/Octo/Extensions/DynamicExtensions.cs diff --git a/source/Octopus.Cli/Extensions/TimeSpanExtensions.cs b/source/Octo/Extensions/TimeSpanExtensions.cs similarity index 100% rename from source/Octopus.Cli/Extensions/TimeSpanExtensions.cs rename to source/Octo/Extensions/TimeSpanExtensions.cs diff --git a/source/Octopus.Cli/Importers/BaseImporter.cs b/source/Octo/Importers/BaseImporter.cs similarity index 100% rename from source/Octopus.Cli/Importers/BaseImporter.cs rename to source/Octo/Importers/BaseImporter.cs diff --git a/source/Octopus.Cli/Importers/BaseValidatedImportSettings.cs b/source/Octo/Importers/BaseValidatedImportSettings.cs similarity index 100% rename from source/Octopus.Cli/Importers/BaseValidatedImportSettings.cs rename to source/Octo/Importers/BaseValidatedImportSettings.cs diff --git a/source/Octopus.Cli/Importers/CheckedReferences.cs b/source/Octo/Importers/CheckedReferences.cs similarity index 100% rename from source/Octopus.Cli/Importers/CheckedReferences.cs rename to source/Octo/Importers/CheckedReferences.cs diff --git a/source/Octopus.Cli/Importers/FileSystemImporter.cs b/source/Octo/Importers/FileSystemImporter.cs similarity index 100% rename from source/Octopus.Cli/Importers/FileSystemImporter.cs rename to source/Octo/Importers/FileSystemImporter.cs diff --git a/source/Octopus.Cli/Importers/IImporter.cs b/source/Octo/Importers/IImporter.cs similarity index 100% rename from source/Octopus.Cli/Importers/IImporter.cs rename to source/Octo/Importers/IImporter.cs diff --git a/source/Octopus.Cli/Importers/IImporterLocator.cs b/source/Octo/Importers/IImporterLocator.cs similarity index 100% rename from source/Octopus.Cli/Importers/IImporterLocator.cs rename to source/Octo/Importers/IImporterLocator.cs diff --git a/source/Octopus.Cli/Importers/IImporterMetadata.cs b/source/Octo/Importers/IImporterMetadata.cs similarity index 100% rename from source/Octopus.Cli/Importers/IImporterMetadata.cs rename to source/Octo/Importers/IImporterMetadata.cs diff --git a/source/Octopus.Cli/Importers/ImporterAttribute.cs b/source/Octo/Importers/ImporterAttribute.cs similarity index 100% rename from source/Octopus.Cli/Importers/ImporterAttribute.cs rename to source/Octo/Importers/ImporterAttribute.cs diff --git a/source/Octopus.Cli/Importers/ImporterLocator.cs b/source/Octo/Importers/ImporterLocator.cs similarity index 100% rename from source/Octopus.Cli/Importers/ImporterLocator.cs rename to source/Octo/Importers/ImporterLocator.cs diff --git a/source/Octopus.Cli/Importers/ProjectImporter.cs b/source/Octo/Importers/ProjectImporter.cs similarity index 100% rename from source/Octopus.Cli/Importers/ProjectImporter.cs rename to source/Octo/Importers/ProjectImporter.cs diff --git a/source/Octopus.Cli/Importers/ReleaseImporter.cs b/source/Octo/Importers/ReleaseImporter.cs similarity index 100% rename from source/Octopus.Cli/Importers/ReleaseImporter.cs rename to source/Octo/Importers/ReleaseImporter.cs diff --git a/source/Octopus.Cli/Infrastructure/AutofacExtensions.cs b/source/Octo/Infrastructure/AutofacExtensions.cs similarity index 100% rename from source/Octopus.Cli/Infrastructure/AutofacExtensions.cs rename to source/Octo/Infrastructure/AutofacExtensions.cs diff --git a/source/Octopus.Cli/Infrastructure/CommandAttribute.cs b/source/Octo/Infrastructure/CommandAttribute.cs similarity index 100% rename from source/Octopus.Cli/Infrastructure/CommandAttribute.cs rename to source/Octo/Infrastructure/CommandAttribute.cs diff --git a/source/Octopus.Cli/Infrastructure/CommandException.cs b/source/Octo/Infrastructure/CommandException.cs similarity index 100% rename from source/Octopus.Cli/Infrastructure/CommandException.cs rename to source/Octo/Infrastructure/CommandException.cs diff --git a/source/Octopus.Cli/Infrastructure/CommandLocator.cs b/source/Octo/Infrastructure/CommandLocator.cs similarity index 100% rename from source/Octopus.Cli/Infrastructure/CommandLocator.cs rename to source/Octo/Infrastructure/CommandLocator.cs diff --git a/source/Octopus.Cli/Infrastructure/CouldNotFindException.cs b/source/Octo/Infrastructure/CouldNotFindException.cs similarity index 100% rename from source/Octopus.Cli/Infrastructure/CouldNotFindException.cs rename to source/Octo/Infrastructure/CouldNotFindException.cs diff --git a/source/Octopus.Cli/Infrastructure/ICommand.cs b/source/Octo/Infrastructure/ICommand.cs similarity index 100% rename from source/Octopus.Cli/Infrastructure/ICommand.cs rename to source/Octo/Infrastructure/ICommand.cs diff --git a/source/Octopus.Cli/Infrastructure/ICommandLocator.cs b/source/Octo/Infrastructure/ICommandLocator.cs similarity index 100% rename from source/Octopus.Cli/Infrastructure/ICommandLocator.cs rename to source/Octo/Infrastructure/ICommandLocator.cs diff --git a/source/Octopus.Cli/Infrastructure/ICommandMetadata.cs b/source/Octo/Infrastructure/ICommandMetadata.cs similarity index 100% rename from source/Octopus.Cli/Infrastructure/ICommandMetadata.cs rename to source/Octo/Infrastructure/ICommandMetadata.cs diff --git a/source/Octopus.Cli/Model/ChannelVersionRuleTestResult.cs b/source/Octo/Model/ChannelVersionRuleTestResult.cs similarity index 100% rename from source/Octopus.Cli/Model/ChannelVersionRuleTestResult.cs rename to source/Octo/Model/ChannelVersionRuleTestResult.cs diff --git a/source/Octo/Octo.xproj b/source/Octo/Octo.xproj new file mode 100644 index 000000000..d863fe12a --- /dev/null +++ b/source/Octo/Octo.xproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + d1bfd88f-fb8e-49ec-968f-f4d9a8a52519 + Octopus.Cli + .\obj + .\bin\ + + + 2.0 + + + \ No newline at end of file diff --git a/source/Octopus.Cli/Program.cs b/source/Octo/Program.cs similarity index 100% rename from source/Octopus.Cli/Program.cs rename to source/Octo/Program.cs diff --git a/source/Octopus.Cli/Properties/AssemblyInfo.cs b/source/Octo/Properties/AssemblyInfo.cs similarity index 100% rename from source/Octopus.Cli/Properties/AssemblyInfo.cs rename to source/Octo/Properties/AssemblyInfo.cs diff --git a/source/Octopus.Cli/Properties/Icon.ico b/source/Octo/Properties/Icon.ico similarity index 100% rename from source/Octopus.Cli/Properties/Icon.ico rename to source/Octo/Properties/Icon.ico diff --git a/source/Octopus.Cli/Repositories/ActionTemplateRepository.cs b/source/Octo/Repositories/ActionTemplateRepository.cs similarity index 100% rename from source/Octopus.Cli/Repositories/ActionTemplateRepository.cs rename to source/Octo/Repositories/ActionTemplateRepository.cs diff --git a/source/Octopus.Cli/Repositories/IActionTemplateRepository.cs b/source/Octo/Repositories/IActionTemplateRepository.cs similarity index 100% rename from source/Octopus.Cli/Repositories/IActionTemplateRepository.cs rename to source/Octo/Repositories/IActionTemplateRepository.cs diff --git a/source/Octopus.Cli/Repositories/IOctopusRepositoryFactory.cs b/source/Octo/Repositories/IOctopusRepositoryFactory.cs similarity index 100% rename from source/Octopus.Cli/Repositories/IOctopusRepositoryFactory.cs rename to source/Octo/Repositories/IOctopusRepositoryFactory.cs diff --git a/source/Octopus.Cli/Repositories/OctopusRepositoryCommonQueries.cs b/source/Octo/Repositories/OctopusRepositoryCommonQueries.cs similarity index 100% rename from source/Octopus.Cli/Repositories/OctopusRepositoryCommonQueries.cs rename to source/Octo/Repositories/OctopusRepositoryCommonQueries.cs diff --git a/source/Octopus.Cli/Repositories/OctopusRepositoryFactory.cs b/source/Octo/Repositories/OctopusRepositoryFactory.cs similarity index 100% rename from source/Octopus.Cli/Repositories/OctopusRepositoryFactory.cs rename to source/Octo/Repositories/OctopusRepositoryFactory.cs diff --git a/source/Octopus.Cli/Util/AssemblyExtensions.cs b/source/Octo/Util/AssemblyExtensions.cs similarity index 100% rename from source/Octopus.Cli/Util/AssemblyExtensions.cs rename to source/Octo/Util/AssemblyExtensions.cs diff --git a/source/Octopus.Cli/Util/DeletionOptions.cs b/source/Octo/Util/DeletionOptions.cs similarity index 100% rename from source/Octopus.Cli/Util/DeletionOptions.cs rename to source/Octo/Util/DeletionOptions.cs diff --git a/source/Octopus.Cli/Util/FeatureDetectionExtensions.cs b/source/Octo/Util/FeatureDetectionExtensions.cs similarity index 100% rename from source/Octopus.Cli/Util/FeatureDetectionExtensions.cs rename to source/Octo/Util/FeatureDetectionExtensions.cs diff --git a/source/Octopus.Cli/Util/FeedCustomExpressionHelper.cs b/source/Octo/Util/FeedCustomExpressionHelper.cs similarity index 100% rename from source/Octopus.Cli/Util/FeedCustomExpressionHelper.cs rename to source/Octo/Util/FeedCustomExpressionHelper.cs diff --git a/source/Octopus.Cli/Util/Humanize.cs b/source/Octo/Util/Humanize.cs similarity index 100% rename from source/Octopus.Cli/Util/Humanize.cs rename to source/Octo/Util/Humanize.cs diff --git a/source/Octopus.Cli/Util/IOctopusFileSystem.cs b/source/Octo/Util/IOctopusFileSystem.cs similarity index 100% rename from source/Octopus.Cli/Util/IOctopusFileSystem.cs rename to source/Octo/Util/IOctopusFileSystem.cs diff --git a/source/Octopus.Cli/Util/LazyExtensions.cs b/source/Octo/Util/LazyExtensions.cs similarity index 100% rename from source/Octopus.Cli/Util/LazyExtensions.cs rename to source/Octo/Util/LazyExtensions.cs diff --git a/source/Octopus.Cli/Util/LineSplitter.cs b/source/Octo/Util/LineSplitter.cs similarity index 100% rename from source/Octopus.Cli/Util/LineSplitter.cs rename to source/Octo/Util/LineSplitter.cs diff --git a/source/Octopus.Cli/Util/ListExtensions.cs b/source/Octo/Util/ListExtensions.cs similarity index 100% rename from source/Octopus.Cli/Util/ListExtensions.cs rename to source/Octo/Util/ListExtensions.cs diff --git a/source/Octopus.Cli/Util/NumericExtensions.cs b/source/Octo/Util/NumericExtensions.cs similarity index 100% rename from source/Octopus.Cli/Util/NumericExtensions.cs rename to source/Octo/Util/NumericExtensions.cs diff --git a/source/Octopus.Cli/Util/OctopusPhysicalFileSystem.cs b/source/Octo/Util/OctopusPhysicalFileSystem.cs similarity index 100% rename from source/Octopus.Cli/Util/OctopusPhysicalFileSystem.cs rename to source/Octo/Util/OctopusPhysicalFileSystem.cs diff --git a/source/Octopus.Cli/Util/ReplaceStatus.cs b/source/Octo/Util/ReplaceStatus.cs similarity index 100% rename from source/Octopus.Cli/Util/ReplaceStatus.cs rename to source/Octo/Util/ReplaceStatus.cs diff --git a/source/Octopus.Cli/Util/ResourceCollectionExtensions.cs b/source/Octo/Util/ResourceCollectionExtensions.cs similarity index 100% rename from source/Octopus.Cli/Util/ResourceCollectionExtensions.cs rename to source/Octo/Util/ResourceCollectionExtensions.cs diff --git a/source/Octopus.Cli/Util/StringExtensions.cs b/source/Octo/Util/StringExtensions.cs similarity index 100% rename from source/Octopus.Cli/Util/StringExtensions.cs rename to source/Octo/Util/StringExtensions.cs diff --git a/source/Octopus.Cli/Util/UriExtensions.cs b/source/Octo/Util/UriExtensions.cs similarity index 100% rename from source/Octopus.Cli/Util/UriExtensions.cs rename to source/Octo/Util/UriExtensions.cs diff --git a/source/Octopus.Cli/app.config b/source/Octo/app.config similarity index 100% rename from source/Octopus.Cli/app.config rename to source/Octo/app.config diff --git a/source/Octo/init.ps1 b/source/Octo/init.ps1 new file mode 100644 index 000000000..cf2469ee6 --- /dev/null +++ b/source/Octo/init.ps1 @@ -0,0 +1,6 @@ +param($installPath, $toolsPath, $package, $project) + +$path = $env:PATH +if (!$path.Contains($toolsPath)) { + $env:PATH += ";$toolsPath" +} \ No newline at end of file diff --git a/source/Octo/project.json b/source/Octo/project.json new file mode 100644 index 000000000..c523b3137 --- /dev/null +++ b/source/Octo/project.json @@ -0,0 +1,52 @@ +{ + "version": "0.0.0-*", + "dependencies": { + "Autofac": "3.5.2", + "GitVersionTask": "3.3.0", + "ILRepack": "2.0.8", + "MarkdownSharp": "1.13.0.0", + "Microsoft.Web.Xdt": "2.1.1", + "Newtonsoft.Json": "9.0.1", + "NuGet.CommandLine": "3.4.3", + "NuGet.Common": "3.4.3", + "NuGet.Frameworks": "3.4.3", + "NuGet.Logging": "3.4.3", + "NuGet.Packaging": "3.4.3", + "NuGet.Packaging.Core": "3.4.3", + "NuGet.Packaging.Core.Types": "3.4.3", + "NuGet.Versioning": "3.4.3", + "Octostache": "1.0.2.29", + "Serilog": "2.1.0", + "Serilog.Sinks.ColoredConsole": "2.0.0", + "Serilog.Sinks.Trace": "2.0.0", + "Sprache": "2.0.0.50", + "Octopus.Client": { + "target": "project" + } + }, + "frameworks": { + "net45": { + "frameworkAssemblies": { + "System.ComponentModel.Composition": "*", + "System.ComponentModel.DataAnnotations": "*", + "System.Configuration": "*", + "System.Core": "*", + "System.IO.Compression": "*", + "System.Net": "*", + "System.Runtime.Serialization": "*", + "System.Web": "*", + "System.Xml.Linq": "*", + "System.Data.DataSetExtensions": "*", + "Microsoft.CSharp": "*", + "System.Data": "*", + "System.Xml": "*" + }, + "buildOptions": { + "platform": "anycpu32bitpreferred" + } + } + }, + "buildOptions": { + "emitEntryPoint": true + } +} \ No newline at end of file diff --git a/source/Octopus.Cli.Tests/Octopus.Cli.Tests.csproj b/source/Octopus.Cli.Tests/Octopus.Cli.Tests.csproj deleted file mode 100644 index 867349d64..000000000 --- a/source/Octopus.Cli.Tests/Octopus.Cli.Tests.csproj +++ /dev/null @@ -1,133 +0,0 @@ - - - - Debug - AnyCPU - {B05E3858-5607-46DB-876C-4E1F5728A4BB} - Library - Properties - Octopus.Cli.Tests - Octopus.Cli.Tests - v4.5 - 512 - ..\ - true - - - - - - true - full - false - bin\ - DEBUG;TRACE - prompt - 4 - x86 - false - - - pdbonly - true - bin\ - TRACE - prompt - 4 - true - false - - - - ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll - True - - - ..\packages\NUnit.2.6.3\lib\nunit.framework.dll - - - ..\packages\Octopus.Client.3.4.0\lib\net40\Octopus.Client.dll - True - - - ..\packages\Serilog.2.1.0\lib\net45\Serilog.dll - True - - - - - - - - - - - ..\packages\NSubstitute.1.7.2.0\lib\NET40\NSubstitute.dll - - - - - Properties\SolutionInfo.cs - - - Properties\VersionInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519} - OctopusTools - - - - - - PreserveNewest - - - Designer - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - \ No newline at end of file diff --git a/source/Octopus.Cli.Tests/Octopus.Cli.Tests.ncrunchproject b/source/Octopus.Cli.Tests/Octopus.Cli.Tests.ncrunchproject deleted file mode 100644 index 96f3af12d..000000000 --- a/source/Octopus.Cli.Tests/Octopus.Cli.Tests.ncrunchproject +++ /dev/null @@ -1,28 +0,0 @@ - - 1000 - false - false - false - true - false - false - false - false - false - true - true - false - true - true - true - 60000 - - - - AutoDetect - STA - x86 - - - - \ No newline at end of file diff --git a/source/Octopus.Cli.Tests/VersioningFixture.cs b/source/Octopus.Cli.Tests/VersioningFixture.cs deleted file mode 100644 index 43edfd5f2..000000000 --- a/source/Octopus.Cli.Tests/VersioningFixture.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using NUnit.Framework; -using Octopus.Cli.Tests.Helpers; -using Octopus.Client; - -namespace Octopus.Cli.Tests -{ - [TestFixture] - public class VersioningFixture - { - [Test] - public void TheTestAssemblyIsVersioned_WithGitVersion() - { - Assert.That(new SemanticVersionInfo(GetType().Assembly).Major, Is.GreaterThan(1)); - } - - [Test] - public void AllOctopusAssemblies_ShouldHaveTheSameSemanticVersion() - { - var octocliVersion = (new SemanticVersionInfo(typeof(Octopus.Cli.Program).Assembly)); - var octopusClientVersion = new SemanticVersionInfo(typeof(IOctopusRepository).Assembly); - - var isClientPreRelease = !string.IsNullOrEmpty(octopusClientVersion.PreReleaseTag); - var isThisPreRelease = !string.IsNullOrEmpty(octocliVersion.PreReleaseTag); - - Console.WriteLine($"Octopus.Client: {octopusClientVersion.SemVer}"); - Console.WriteLine($"Octopus.Cli (Octo.exe): {octocliVersion.SemVer}"); - - if (isClientPreRelease) Assert.That(isThisPreRelease, "We are using a pre-release version of Octopus.Client, so octo.exe should also be versioned as a pre-release. We should only build full-releases of octo.exe using full releases of Octopus.Client."); - else Assert.That(true); - } - } -} diff --git a/source/Octopus.Cli.Tests/packages.config b/source/Octopus.Cli.Tests/packages.config deleted file mode 100644 index f6fc62d31..000000000 --- a/source/Octopus.Cli.Tests/packages.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/source/Octopus.Cli/Octopus.Cli.csproj b/source/Octopus.Cli/Octopus.Cli.csproj deleted file mode 100644 index 26bb1a742..000000000 --- a/source/Octopus.Cli/Octopus.Cli.csproj +++ /dev/null @@ -1,266 +0,0 @@ - - - - Debug - x86 - {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519} - Exe - Properties - Octopus.Cli - Octo - v4.5 - - - 512 - ..\ - true - - - - - x86 - true - full - false - bin\ - DEBUG;TRACE - prompt - 4 - true - false - default - - - x86 - none - true - bin\ - TRACE - prompt - 4 - true - false - - - Properties\Icon.ico - false - - - - ..\packages\Autofac.3.5.2\lib\net40\Autofac.dll - True - - - ..\packages\MarkdownSharp.1.13.0.0\lib\35\MarkdownSharp.dll - True - - - ..\packages\Microsoft.Web.Xdt.2.1.1\lib\net40\Microsoft.Web.XmlTransform.dll - True - - - ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll - True - - - ..\packages\NuGet.Common.3.4.3\lib\net45\NuGet.Common.dll - True - - - ..\packages\NuGet.Frameworks.3.4.3\lib\net45\NuGet.Frameworks.dll - True - - - ..\packages\NuGet.Logging.3.4.3\lib\net45\NuGet.Logging.dll - True - - - ..\packages\NuGet.Packaging.3.4.3\lib\net45\NuGet.Packaging.dll - True - - - ..\packages\NuGet.Packaging.Core.3.4.3\lib\net45\NuGet.Packaging.Core.dll - True - - - ..\packages\NuGet.Packaging.Core.Types.3.4.3\lib\net45\NuGet.Packaging.Core.Types.dll - True - - - ..\packages\NuGet.Versioning.3.4.3\lib\net45\NuGet.Versioning.dll - True - - - ..\packages\Octopus.Client.3.4.0\lib\net40\Octopus.Client.dll - True - - - ..\packages\Octostache.1.0.2.29\lib\net40\Octostache.dll - True - - - ..\packages\Serilog.2.1.0\lib\net45\Serilog.dll - True - - - ..\packages\Serilog.Sinks.ColoredConsole.2.0.0\lib\net45\Serilog.Sinks.ColoredConsole.dll - True - - - ..\packages\Serilog.Sinks.Trace.2.0.0\lib\net45\Serilog.Sinks.Trace.dll - True - - - ..\packages\Sprache.2.0.0.50\lib\portable-net4+netcore45+win8+wp8+sl5+MonoAndroid+Xamarin.iOS10+MonoTouch\Sprache.dll - True - - - - - - - - - - - - - - - - - - - Properties\SolutionInfo.cs - - - Properties\VersionInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - Designer - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - \ No newline at end of file diff --git a/source/Octopus.Cli/OctopusTools.v2.ncrunchproject b/source/Octopus.Cli/OctopusTools.v2.ncrunchproject deleted file mode 100644 index 96f3af12d..000000000 --- a/source/Octopus.Cli/OctopusTools.v2.ncrunchproject +++ /dev/null @@ -1,28 +0,0 @@ - - 1000 - false - false - false - true - false - false - false - false - false - true - true - false - true - true - true - 60000 - - - - AutoDetect - STA - x86 - - - - \ No newline at end of file diff --git a/source/Octopus.Cli/packages.config b/source/Octopus.Cli/packages.config deleted file mode 100644 index a781669ce..000000000 --- a/source/Octopus.Cli/packages.config +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/source/Octopus.Client.Tests/Conventions/ClientConventions.cs b/source/Octopus.Client.Tests/Conventions/ClientConventions.cs new file mode 100644 index 000000000..9906eb9ae --- /dev/null +++ b/source/Octopus.Client.Tests/Conventions/ClientConventions.cs @@ -0,0 +1,292 @@ +using System; +using System.Linq; +using Autofac; +using Autofac.Util; +using Conventional; +using Conventional.Conventions; +using Nancy.Extensions; +using NUnit.Framework; +using Octopus.Client.Model; +using Octopus.Client.Repositories; +using Octopus.Client.Tests.Extensions; + +namespace Octopus.Client.Tests.Conventions +{ + [TestFixture] + public class ClientConventions + { + private static readonly Type[] RepositoryInterfaceTypes = typeof(IOctopusClient).Assembly.GetLoadableTypes() + .Where(t => t.IsInterface && t.Name.EndsWith("Repository")) + .Where(t => t != typeof(IOctopusRepository)) + .ToArray(); + private static readonly Type[] RepositoryTypes = typeof(IOctopusClient).Assembly.GetLoadableTypes() + .Where(t => !t.IsInterface && t.Name.EndsWith("Repository")) + .Where(t => t != typeof(OctopusRepository)) + .ToArray(); + private static readonly Type[] ResourceTypes = typeof(IOctopusClient).Assembly.GetLoadableTypes() + .Where(t => t.Name.EndsWith("Resource")) + .ToArray(); + private static readonly Type[] RepositoryResourceTypes = ResourceTypes + .Where(res => RepositoryTypes + .Any(rep => rep.BaseType?.IsGenericType == true && rep.BaseType?.GetGenericArguments().Contains(res) == true)) + .ToArray(); + + [Test] + public void AllRepositoriesShouldBeAvailableViaIOctopusRepository() + { + var exposedTypes = typeof (IOctopusRepository).GetProperties() + .Select(p => p.PropertyType) + .ToArray(); + + var missingTypes = RepositoryInterfaceTypes.Except(exposedTypes).ToArray(); + if (missingTypes.Any()) + { + Assert.Fail($"All *Repository types should be exposed by {nameof(IOctopusRepository)}. Missing: {missingTypes.Select(t => t.Name).CommaSeperate()}"); + } + } + + [Test] + public void AllRepositoriesShouldBeAvailableViaOctopusRepository() + { + var exposedTypes = typeof (OctopusRepository).GetProperties() + .Select(p => p.PropertyType) + .ToArray(); + + var missingTypes = RepositoryInterfaceTypes.Except(exposedTypes).ToArray(); + if (missingTypes.Any()) + { + Assert.Fail($"All *Repository types should be exposed by {nameof(OctopusRepository)}. Missing: {missingTypes.Select(t => t.Name).CommaSeperate()}"); + } + } + + [Test] + public void AllRepositoriesShouldImplementNonGenericSimpleInterface() + { + var repositoryInterfaceMap = RepositoryTypes.Select(r => new { Repository = r, Interface = r.GetInterfaces().Where(i => !i.IsGenericType)}).ToArray(); + var missingInterface = repositoryInterfaceMap.Where(x => x.Interface == null).ToArray(); + + if (missingInterface.Any()) + { + Assert.Fail($"All *Repository types should implement a non-generic interface representing the repository contract {nameof(OctopusRepository)}.{Environment.NewLine}{missingInterface.Select(x => $"{x.Repository.Name} expected to implement I{x.Repository.Name}").NewLineSeperate()}"); + } + } + + [Test] + public void AllRepositoriesShouldExposePublicMembersViaTheirInterfaces() + { + var exposureMap = RepositoryTypes + .Select(r => new + { + Repository = r, + DeclaredMethodMap = r.GetMethods() + .Where(m => m.DeclaringType == r) + .Select(m => new + { + DelcaredMethod = m, + r.GetInterfaces().Select(r.GetInterfaceMap).FirstOrDefault(map => map.TargetMethods.Contains(m)).InterfaceType + }).ToArray(), + }).ToArray(); + + var missingExposure = exposureMap.Where(x => x.DeclaredMethodMap.Any(map => map.InterfaceType == null)).ToArray(); + if (missingExposure.Any()) + { + Assert.Fail($"The following repositories do not expose at least one of their public members by interface, and hence won't be accessible by clients.{Environment.NewLine}{exposureMap.Where(x => x.DeclaredMethodMap.Any(map => map.InterfaceType == null)).Select(x => $"{x.Repository.Name}: {x.DeclaredMethodMap.Where(map => map.InterfaceType == null).Select(map => $"{map.DelcaredMethod.Name}({map.DelcaredMethod.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}").CommaSeperate()})").CommaSeperate()}").NewLineSeperate()}"); + } + } + + [Test] + public void AllRepositoryInterfacesShouldFollowTheseConventions() + { + RepositoryInterfaceTypes + .MustConformTo(Convention.MustLiveInNamespace("Octopus.Client.Repositories")) + .AndMustConformTo(Convention.NameMustEndWith("Repository")) + .WithFailureAssertion(Assert.Fail); + } + + [Test] + public void AllResourcesShouldLiveInTheCorrectParentNamespace() + { + ResourceTypes + .MustConformTo(new MustLiveInParentNamespaceConventionSpecification("Octopus.Client.Model")) + .WithFailureAssertion(Assert.Fail); + } + + [Test] + public void AllResourcePropertiesShouldHavePublicGetters() + { + ResourceTypes + .MustConformTo(Convention.PropertiesMustHavePublicGetters) + .WithFailureAssertion(Assert.Fail); + } + + [Test] + public void AllResourcePropertiesShouldHavePublicSetters() + { + ResourceTypes + .Except(new [] {typeof(LifecycleResource), typeof(DeploymentProcessResource)}) + .MustConformTo(Convention.PropertiesMustHavePublicSetters) + .WithFailureAssertion(Assert.Fail); + } + + [Test] + public void RepositoriesShouldNotRepresentMultipleResourceTypes() + { + var repositoryResourceMap = RepositoryTypes + .Select(t => new + { + Repository = t, + ResourceTypes = t.GetInterfaces().Concat(new [] { t.BaseType }) + .Where(i => i.IsGenericType) + .SelectMany(i => i.GetGenericArguments()) + .Distinct() + .ToArray() + }).ToArray(); + + var confusedRepositories = repositoryResourceMap.Where(x => x.ResourceTypes.Length > 1).ToArray(); + + if (confusedRepositories.Any()) + { + Assert.Fail($"Repositories should represent consistent Resource type. These repositories have an identity crisis: {Environment.NewLine}{confusedRepositories.Select(x => $"{x.Repository.Name}<{x.ResourceTypes.Select(r => r.Name).CommaSeperate()}>").NewLineSeperate()}"); + } + } + + [Test] + public void RepositoriesShouldBeNamedLikeTheirResourceType() + { + var repositoryResourceMap = RepositoryTypes + .Select(t => new + { + Repository = t, + ResourceTypes = t.GetInterfaces().Concat(new[] { t.BaseType }) + .Where(i => i.IsGenericType) + .SelectMany(i => i.GetGenericArguments()) + .Distinct() + .ToArray() + }).ToArray(); + + var confusingRepositories = repositoryResourceMap + .Where(x => x.ResourceTypes.Any()) + .Where(x => !x.Repository.Name.StartsWith(x.ResourceTypes.First().Name.Replace("Resource", ""))) + .ToArray(); + + if (confusingRepositories.Any()) + { + Assert.Fail($"Repositories should be named like their Resource type. These repositories could be confusing: {Environment.NewLine}{confusingRepositories.Select(x => $"{x.Repository.Name}<{x.ResourceTypes.Select(r => r.Name).CommaSeperate()}> - based on the resource type this should be named something like {x.ResourceTypes.First().Name.Replace("Resource", "")}Repository? Or maybe this is using the wrong generic type argument?").NewLineSeperate()}"); + } + } + + [Test] + public void TopLevelResourcesWithAPublicNamePropertyShouldProbablyImplementINamedResource() + { + var ignored = new[] + { + typeof (DeploymentResource), + typeof (TaskResource) + }; + + var shouldProbablyBeINamedResource = RepositoryResourceTypes + .Except(ignored) + .Where(t => t.GetProperty("Name") != null && !t.IsAssignableTo()) + .ToArray(); + + if (shouldProbablyBeINamedResource.Any()) + { + Assert.Fail($"The following top-level resource types have a Name property, and should probably implement INamedResource: {Environment.NewLine}{shouldProbablyBeINamedResource.Select(t => t.Name).NewLineSeperate()}"); + } + } + + [Test] + public void SomeINamedResourcesShouldNeverBeIFindByNameToAvoidGettingTheWrongAnswer() + { + var denied = new[] + { + typeof (IChannelRepository), + typeof (IDeploymentProcessRepository), + typeof (ITaskRepository) + }; + + var misleadingRepositories = denied.Where(r => r.IsAssignableToGenericType(typeof (IFindByName<>))).ToArray(); + + if (misleadingRepositories.Any()) + { + Assert.Fail($"The following repositories allow the client to FindByName, but this will end up returning a misleading result, and the resource should be loaded in another way:{Environment.NewLine}{misleadingRepositories.Select(r => $"{r.Name}").NewLineSeperate()}"); + } + } + + [Test] + public void RepositoriesThatGetNamedResourcesShouldUsuallyImplementIFindByName() + { + var ignored = new[] + { + typeof (IChannelRepository), + typeof (IProjectTriggerRepository) + }; + + var getsResources = RepositoryInterfaceTypes + .Except(ignored) + .Where(t => t.IsAssignableToGenericType(typeof (IGet<>))) + .ToArray(); + + var getsNamedResources = getsResources + .Where(t => t.GetInterfaces() + .Where(i => i.IsClosedTypeOf(typeof(IGet<>))) + .Any(i => i.GenericTypeArguments.Any(r => r.IsAssignableTo()))) + .ToArray(); + + var canFindByName = getsNamedResources + .Where(t => t.GetInterfaces().Any(i => i.IsClosedTypeOf(typeof (IFindByName<>)))) + .ToArray(); + + var missingFindByName = getsNamedResources.Except(canFindByName).ToArray(); + + if (missingFindByName.Any()) + { + Assert.Fail($"Repositories that implement IGet should usually implement IFindByName, unless that named resource is a singleton or owned by another aggregate.{Environment.NewLine}{missingFindByName.Select(t => t.Name).NewLineSeperate()}"); + } + } + + [Test] + public void MostRepositoriesThatGetResourcesShouldImplementIPaginate() + { + var ignored = new[] + { + typeof (IDeploymentProcessRepository), + typeof (IInterruptionRepository), + typeof (IEventRepository), + typeof (IVariableSetRepository), + typeof (IChannelRepository), + typeof (IProjectTriggerRepository) + }; + + var missing = RepositoryInterfaceTypes + .Except(ignored) + .Where(t => t.IsAssignableToGenericType(typeof(IGet<>))) + .Except(RepositoryInterfaceTypes.Where(t => t.IsAssignableToGenericType(typeof (IPaginate<>)))) + .ToArray(); + + if (missing.Any()) + { + Assert.Fail($"Most repositories that get resources should implement IPaginate unless the repository should target one specific resource like a singleton or child of another aggregate.{Environment.NewLine}{missing.Select(t => t.Name).NewLineSeperate()}"); + } + } + + public class MustLiveInParentNamespaceConventionSpecification : ConventionSpecification + { + private readonly string parentNamespace; + + protected override string FailureMessage => "Must live in parent namespace {0} but actually lives in namespace {1}"; + + public MustLiveInParentNamespaceConventionSpecification(string parentNamespace) + { + this.parentNamespace = parentNamespace; + } + + public override ConventionResult IsSatisfiedBy(Type type) + { + if (type.Namespace != null && type.Namespace.StartsWith(parentNamespace)) + return ConventionResult.Satisfied(type.FullName); + return ConventionResult.NotSatisfied(type.FullName, string.Format(FailureMessage, parentNamespace, type.Namespace)); + } + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client.Tests/DefaultLinkResolverFixture.cs b/source/Octopus.Client.Tests/DefaultLinkResolverFixture.cs new file mode 100644 index 000000000..5dbe421cb --- /dev/null +++ b/source/Octopus.Client.Tests/DefaultLinkResolverFixture.cs @@ -0,0 +1,75 @@ +using System; +using NUnit.Framework; + +namespace Octopus.Client.Tests +{ + [TestFixture] + public class DefaultLinkResolverFixture + { + [Test] + [TestCase("~/", "http://octopus/")] + [TestCase("~/api", "http://octopus/api")] + [TestCase("~/api/foo", "http://octopus/api/foo")] + [TestCase("~/api/foo/bar", "http://octopus/api/foo/bar")] + public void ShouldResolveWhenNoSuffixGiven(string link, string expectedResult) + { + var resolver = new DefaultLinkResolver(new Uri("http://octopus/")); + Assert.That(resolver.Resolve(link).ToString(), Is.EqualTo(expectedResult)); + } + + [Test] + [TestCase("~/", "http://octopus/")] + [TestCase("~/api", "http://octopus/api")] + [TestCase("~/api/foo", "http://octopus/api/foo")] + [TestCase("~/api/foo/bar", "http://octopus/api/foo/bar")] + public void ShouldResolveWhenApiSuffixGiven(string link, string expectedResult) + { + var resolver = new DefaultLinkResolver(new Uri("http://octopus/api")); + Assert.That(resolver.Resolve(link).ToString(), Is.EqualTo(expectedResult)); + } + + [Test] + [TestCase("~/", "http://octopus/vdir/")] + [TestCase("~/api", "http://octopus/vdir/api")] + [TestCase("~/api/foo", "http://octopus/vdir/api/foo")] + [TestCase("~/api/foo/bar", "http://octopus/vdir/api/foo/bar")] + public void ShouldResolveWhenVirtualDirectorySuffixGiven(string link, string expectedResult) + { + var resolver = new DefaultLinkResolver(new Uri("http://octopus/vdir")); + Assert.That(resolver.Resolve(link).ToString(), Is.EqualTo(expectedResult)); + } + + [Test] + [TestCase("~/", "http://octopus/vdir/")] + [TestCase("~/api", "http://octopus/vdir/api")] + [TestCase("~/api/foo", "http://octopus/vdir/api/foo")] + [TestCase("~/api/foo/bar", "http://octopus/vdir/api/foo/bar")] + public void ShouldResolveWhenVirtualDirectoryApiSuffixGiven(string link, string expectedResult) + { + var resolver = new DefaultLinkResolver(new Uri("http://octopus/vdir/api")); + Assert.That(resolver.Resolve(link).ToString(), Is.EqualTo(expectedResult)); + } + + [Test] + [TestCase("~/", "http://octopus/vdir1/vdir2/")] + [TestCase("~/api", "http://octopus/vdir1/vdir2/api")] + [TestCase("~/api/foo", "http://octopus/vdir1/vdir2/api/foo")] + [TestCase("~/api/foo/bar", "http://octopus/vdir1/vdir2/api/foo/bar")] + public void ShouldResolveWhenNestedVirtualDirectorySuffixGiven(string link, string expectedResult) + { + var resolver = new DefaultLinkResolver(new Uri("http://octopus/vdir1/vdir2")); + Assert.That(resolver.Resolve(link).ToString(), Is.EqualTo(expectedResult)); + } + + [Test] + [TestCase("~/", "http://octopus/vdir1/vdir2/")] + [TestCase("~/api", "http://octopus/vdir1/vdir2/api")] + [TestCase("~/api/foo", "http://octopus/vdir1/vdir2/api/foo")] + [TestCase("~/api/foo/bar", "http://octopus/vdir1/vdir2/api/foo/bar")] + public void ShouldResolveWhenNestedVirtualDirectoryApiSuffixGiven(string link, string expectedResult) + { + var resolver = new DefaultLinkResolver(new Uri("http://octopus/vdir1/vdir2/api")); + Assert.That(resolver.Resolve(link).ToString(), Is.EqualTo(expectedResult)); + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client.Tests/Extensions/StringExtensions.cs b/source/Octopus.Client.Tests/Extensions/StringExtensions.cs new file mode 100644 index 000000000..dff74fc25 --- /dev/null +++ b/source/Octopus.Client.Tests/Extensions/StringExtensions.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace Octopus.Client.Tests.Extensions +{ + public static class StringExtensions + { + public static string RemoveNewlines(this string str) + { + return str?.Replace("\r", "").Replace("\n", ""); + } + + public static string NewLineSeperate(this IEnumerable items) => string.Join(Environment.NewLine, items); + public static string CommaSeperate(this IEnumerable items) => string.Join(", ", items); + } +} \ No newline at end of file diff --git a/source/Octopus.Client.Tests/Model/NonEmptyCollectionItemAttributeFixture.cs b/source/Octopus.Client.Tests/Model/NonEmptyCollectionItemAttributeFixture.cs new file mode 100644 index 000000000..56e26aae1 --- /dev/null +++ b/source/Octopus.Client.Tests/Model/NonEmptyCollectionItemAttributeFixture.cs @@ -0,0 +1,32 @@ +using System; +using NUnit.Framework; +using Octopus.Client.Model; +using Octopus.Client.Validation; + +namespace Octopus.Client.Tests.Model +{ + [TestFixture] + public class NonEmptyCollectionItemAttributeFixture + { + [Test] + public void EmptyCollectionShouldPass() + { + var attribute = new NonEmptyCollectionItemAttribute(); + Assert.IsTrue(attribute.IsValid(new ReferenceCollection())); + } + + [Test] + public void CollectionWithBlankStringShouldFail() + { + var attribute = new NonEmptyCollectionItemAttribute(); + Assert.IsFalse(attribute.IsValid(new ReferenceCollection(new[] {""}))); + } + + [Test] + public void CollectionWithSomethingInItShouldPass() + { + var attribute = new NonEmptyCollectionItemAttribute(); + Assert.IsTrue(attribute.IsValid(new ReferenceCollection(new[] {"project-1"}))); + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client.Tests/Model/PasswordComplexityAttributeFixture.cs b/source/Octopus.Client.Tests/Model/PasswordComplexityAttributeFixture.cs new file mode 100644 index 000000000..0079575f4 --- /dev/null +++ b/source/Octopus.Client.Tests/Model/PasswordComplexityAttributeFixture.cs @@ -0,0 +1,28 @@ +using System; +using NUnit.Framework; +using Octopus.Client.Validation; + +namespace Octopus.Client.Tests.Model +{ + public class PasswordComplexityAttributeFixture + { + [Test] + [TestCase("", true)] // Special case because empty strings are caught by the [Required] rule + [TestCase("a", false)] + [TestCase("abc", false)] + [TestCase("abdefgh", false)] + [TestCase("password", false)] + [TestCase("Password", false)] + [TestCase("Password01", true)] + [TestCase("Password01!", true)] + [TestCase("Tr0ub4dor&3", true)] + [TestCase("correct horse battery stable", true)] + public void ShouldPassOrFail(string password, bool shouldBeValid) + { + var attribute = new PasswordComplexityAttribute(); + var wasValid = attribute.IsValid(password); + + Assert.That(wasValid, Is.EqualTo(shouldBeValid)); + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client.Tests/Model/SemanticVersionMaskFixture.cs b/source/Octopus.Client.Tests/Model/SemanticVersionMaskFixture.cs new file mode 100644 index 000000000..466d9493a --- /dev/null +++ b/source/Octopus.Client.Tests/Model/SemanticVersionMaskFixture.cs @@ -0,0 +1,118 @@ +using System; +using System.Linq; +using NUnit.Framework; +using Octopus.Client.Model; + +namespace Octopus.Client.Tests.Model +{ + [TestFixture] + public class SemanticVersionMaskFixture + { + [Test] + [TestCase("1.0", false)] + [TestCase("1.0.0", false)] + [TestCase("1.0.0.0", false)] + [TestCase("1.0.0.0-foo", false)] + [TestCase("1.0.0-foo", false)] + [TestCase("1.0-foo", false)] + [TestCase("1.-i", false)] + [TestCase("1.1.2-i", false)] + [TestCase("127.0.0.1-localhost", false)] + [TestCase("1.i", true)] + [TestCase("1.0.i", true)] + [TestCase("1.0.0.i", true)] + [TestCase("1.0.c.i", true)] + [TestCase("i.i", true)] + [TestCase("c.c", true)] + [TestCase("i.i.i", true)] + [TestCase("i.i.0", true)] + [TestCase("c.c.0", true)] + [TestCase("c.c.i.0", true)] + [TestCase("c.c.c.i", true)] + [TestCase("i.i.i.i", true)] + [TestCase("127.c.0.i-localhost", true)] + [TestCase("1.0.0-alpha.1", false)] + [TestCase("1.0.0-alpha.beta.1", false)] + [TestCase("1.0.0-alpha.i", true)] + [TestCase("1.0.0-alpha.beta9.i", true)] + [TestCase("1.0.c-alpha.beta9.i", true)] + [TestCase("1.0.0-alpha.i+foo", false)] + public void ShouldBeMask(string version, bool valid) + { + var isMask = SemanticVersionMask.IsMask(version); + Assert.That(isMask, Is.EqualTo(valid)); + } + + [Test] + [TestCase("0.i", "0.0", "0.1")] + [TestCase("1.i", "1.0", "1.1")] + [TestCase("1.1.i", "1.1.1234", "1.1.1235", Description = "Increment on build number increases build by one")] + [TestCase("c.c.i", "1.0.1", "1.0.2", Description = "Mixed substitutions")] + [TestCase("1.i", "1.1.5", "1.2.0", Description = "Reset missing sub-numbers to zero")] + [TestCase("1.i.1", "1.1.5", "1.2.1", Description = "Allows sub-numbers")] + [TestCase("1.c.i", "1.1", "1.1.1", Description = "Increment on non-existant sub-number assumes previous zero")] + [TestCase("1.c.i", "1.1.5", "1.1.6", Description = "Increment on current sub-number adds one")] + [TestCase("1.i.i", "1.1.5", "1.2.0", Description = "Increment on increment sub-number resets to zero")] + [TestCase("1.i.c", "1.1.5", "1.2.0", Description = "Current on increment sub-number resets to zero")] + [TestCase("2.19.i-channel", "2.19.30", "2.19.31-channel", Description = "Tag in mask preserved")] + [TestCase("2.19.i-channel", "2.19.30-baz", "2.19.31-channel", Description = "Tag in mask overrides current tag")] + [TestCase("2.19.i", "2.19.30-baz", "2.19.31", Description = "Tag on current ignored if not in mask")] + [TestCase("1.2.c-alpha.i", "1.2.3-alpha.4", "1.2.3-alpha.5", Description = "Increments pre-release")] + [TestCase("1.2.0-alpha.i", "1.2.3", "1.2.0-alpha.1", Description = "Increment on non-existintant pre-release identifier assumes previous zero")] + [TestCase("1.2.0-alpha.c.i.i", "1.2.0-alpha.2.3.4", "1.2.0-alpha.2.4.0", Description = "Increment on increment pre-release identifier resets to zero")] + public void ShouldApplyMask(string mask, string current, string expected) + { + var currentVersion = SemanticVersion.Parse(current); + var resultVersion = SemanticVersionMask.ApplyMask(mask, currentVersion); + var received = resultVersion.ToString(); + + Assert.That(received, Is.EqualTo(expected)); + } + + [Test] + [TestCase("1.c.i", "1.0.0", Description = "Current and increment substitute treated as zero")] + [TestCase("c.i", "0.0", Description = "Major will start from zero")] + [TestCase("c.i.1-foo", "0.0.1-foo", Description = "Tags are preserved")] + public void ShouldCreateZeroedVersionWhenNoCurrentPresent(string mask, string expected) + { + var resultVersion = SemanticVersionMask.ApplyMask(mask, null); + var received = resultVersion.ToString(); + + Assert.That(received, Is.EqualTo(expected)); + } + + [Test] + [TestCase("2.2.i", new string[] {"1.1.1", "2.2.1", "2.2.3", "2.2"}, "2.2.3", Description = "Trailing substitute")] + [TestCase("2.2.c", new string[] { "1.1.1", "2.2.1", "2.2.3", "2.2" }, "2.2.3", Description = "Substituion 'current' character")] + [TestCase("2.3.i", new string[] {"1.1.1", "2.2.1", "2.2.3", "2.3"}, "2.3", Description = "Version without build")] + [TestCase("2.i.i", new string[] { "1.1.1", "2.2.1", "2.2.3", "2.3" }, "2.3", Description = "Multiple substitutions")] + [TestCase("1.i.i", new string[] {"1.1.1", "2.2.1", "2.2.3", "2.3"}, "1.1.1", Description = "Non global max")] + [TestCase("2.19.i-channel", new string[] {"2.19.30-baz", "2.19.31", "2.20.0"}, "2.19.31", Description = "Tag in mask")] + [TestCase("2.19.i", new string[] { "2.19.30-baz", "2.19.29", "2.20.0" }, "2.19.30-baz", Description = "Tag in match")] + [TestCase("4.i.1-tagx", new string[] { "4.0.1-tagx", "4.1", "5.0.0" }, "4.1", Description = "Tag in match")] + [TestCase("2.i", new string[] { "2.19.2" }, "2.19.2")] + public void ShouldGetTheCorrectLatestVersion(string mask, string[] versionList, string expected) + { + var versions = versionList.Select(version => SemanticVersion.Parse(version)).ToList(); + var currentVersion = SemanticVersionMask.GetLatestMaskedVersion(mask, versions); + + Assert.AreEqual(currentVersion.ToString(), expected); + } + + [Test] + [TestCase("0.1.1", new string[] {"1.1.1", "2.2.1", "2.2.3", "2.3"})] + [TestCase("4.1.1", new string[] {"1.1.1", "2.2.1", "2.2.3", "2.3"})] + [TestCase("1.2.i", new string[] { "1.1.1", "2.2.1", "2.2.3", "2.3" })] + [TestCase("2.2.4.i", new string[] { "1.1.1", "2.2.1", "2.2.3", "2.3" })] + [TestCase("2.19", new string[] { "2.19.2" })] + [TestCase("2.2.4.i", new string[] { })] + public void ShouldNotMatchAnyVersions(string mask, string[] versionList) + { + var versions = versionList.Select(version => SemanticVersion.Parse(version)).ToList(); + var currentVersion = SemanticVersionMask.GetLatestMaskedVersion(mask, versions); + + Assert.IsNull(currentVersion); + } + + } +} \ No newline at end of file diff --git a/source/Octopus.Client.Tests/Model/Versioning/VersionComparerFixture.cs b/source/Octopus.Client.Tests/Model/Versioning/VersionComparerFixture.cs new file mode 100644 index 000000000..4356bbc95 --- /dev/null +++ b/source/Octopus.Client.Tests/Model/Versioning/VersionComparerFixture.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using Octopus.Client.Model; +using Octopus.Client.Model.Versioning; + +namespace Octopus.Client.Tests.Model.Versioning +{ + [TestFixture] + public class VersionComparerFixture + { + + [Test] + [TestCase("1.0.0", "1.0.0")] + [TestCase("1.0.0-BETA", "1.0.0-beta")] + [TestCase("1.0.0-BETA+AA", "1.0.0-beta+aa")] + [TestCase("1.0.0-BETA+AA", "1.0.0-beta+aa")] + [TestCase("1.0.0-BETA.X.y.5.77.0+AA", "1.0.0-beta.x.y.5.77.0+aa")] + public void VersionComparisonDefaultEqual(string version1, string version2) + { + // Arrange & Act + var match = Equals(VersionComparer.Default, version1, version2); + + // Assert + Assert.True(match); + } + + [TestCase("0.0.0", "1.0.0")] + [TestCase("1.1.0", "1.0.0")] + [TestCase("1.0.1", "1.0.0")] + [TestCase("1.0.1", "1.0.0")] + [TestCase("1.0.0-BETA", "1.0.0-beta2")] + [TestCase("1.0.0+AA", "1.0.0-beta+aa")] + [TestCase("1.0.0-BETA+AA", "1.0.0-beta")] + [TestCase("1.0.0-BETA.X.y.5.77.0+AA", "1.0.0-beta.x.y.5.79.0+aa")] + [TestCase("1.0.0+AA", "1.0.0+BB")] + public void VersionComparisonDefaultNotEqual(string version1, string version2) + { + // Arrange & Act + var match = !Equals(version1, version2); + + // Assert + Assert.True(match); + } + + [TestCase("0.0.0", "1.0.0")] + [TestCase("1.0.0", "1.1.0")] + [TestCase("1.0.0", "1.0.1")] + [TestCase("1.999.9999", "2.1.1")] + [TestCase("1.0.0-BETA", "1.0.0-beta2")] + [TestCase("1.0.0-beta+AA", "1.0.0+aa")] + [TestCase("1.0.0-BETA", "1.0.0-beta.1+AA")] + [TestCase("1.0.0-BETA.X.y.5.77.0+AA", "1.0.0-beta.x.y.5.79.0+aa")] + [TestCase("1.0.0-BETA.X.y.5.79.0+AA", "1.0.0-beta.x.y.5.790.0+abc")] + public void VersionComparisonDefaultLess(string version1, string version2) + { + // Arrange & Act + var result = Compare(VersionComparer.Default, version1, version2); + + // Assert + Assert.True(result < 0); + } + + static int Compare(IVersionComparer comparer, string version1, string version2) + { + // Act + var x = CompareOneWay(comparer, version1, version2); + var y = CompareOneWay(comparer, version2, version1) * -1; + + // Assert + Assert.AreEqual(x,y); + + return x; + } + + static int CompareOneWay(IVersionComparer comparer, string version1, string version2) + { + // Arrange + var a = SemanticVersion.Parse(version1); + var b = SemanticVersion.Parse(version2); + var c = StrictSemanticVersion.Parse(version1); + var d = StrictSemanticVersion.Parse(version2); + + // Act + var results = new List(); + results.Add(comparer.Compare(a, b)); + results.Add(comparer.Compare(a, d)); + results.Add(comparer.Compare(c, b)); + results.Add(comparer.Compare(c, d)); + + // Assert + Assert.True(results.FindAll(x => x == results[0]).Count == results.Count); + + return results[0]; + } + + static bool Equals(IVersionComparer comparer, string version1, string version2) + { + return EqualsOneWay(comparer, version1, version2) && EqualsOneWay(comparer, version2, version1); + } + + static bool EqualsOneWay(IVersionComparer comparer, string version1, string version2) + { + // Arrange + var a = SemanticVersion.Parse(version1); + var b = SemanticVersion.Parse(version2); + StrictSemanticVersion c = SemanticVersion.Parse(version1); + StrictSemanticVersion d = SemanticVersion.Parse(version2); + + // Act + var match = Compare(comparer, version1, version2) == 0; + match &= comparer.Equals(a, b); + match &= comparer.Equals(a, d); + match &= comparer.Equals(c, d); + match &= comparer.Equals(c, b); + + return match; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client.Tests/Octopus.Client.Tests.xproj b/source/Octopus.Client.Tests/Octopus.Client.Tests.xproj new file mode 100644 index 000000000..dab529bbb --- /dev/null +++ b/source/Octopus.Client.Tests/Octopus.Client.Tests.xproj @@ -0,0 +1,22 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + de6c675d-64b0-4ca5-a6b0-bee4e0e9a1b8 + Octopus.Client.Tests + .\obj + .\bin\ + v4.6.1 + + + 2.0 + + + + + + \ No newline at end of file diff --git a/source/Octopus.Client.Tests/OctopusRepositoryConventionFixture.cs b/source/Octopus.Client.Tests/OctopusRepositoryConventionFixture.cs new file mode 100644 index 000000000..2ffb4a798 --- /dev/null +++ b/source/Octopus.Client.Tests/OctopusRepositoryConventionFixture.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using NUnit.Framework; + +namespace Octopus.Client.Tests +{ + [TestFixture] + public class OctopusRepositoryConventionFixture + { + [Test] + public void EnsureAllRepositoryPropertiesHaveBeenAdded() + { + var i = typeof(IOctopusRepository); + var interfaceProps = i.GetProperties().Where(p => p.PropertyType.Name.EndsWith("Repository")).ToList(); + var t = typeof(OctopusRepository); + var implementationProps = t.GetProperties().Where(p => p.PropertyType.Name.EndsWith("Repository")).ToList(); + + Assert.That(interfaceProps.Count(), Is.EqualTo(implementationProps.Count()), GetDifferenceMessage(interfaceProps, implementationProps)); + } + + static string GetDifferenceMessage(IEnumerable interfaceProps, IEnumerable implementationProps) + { + + var ints = interfaceProps.Select(p => $"({p.PropertyType.Name}){p.Name}"); + var imps = implementationProps.Select(p => $"({p.PropertyType.Name}){p.Name}"); + var msg = "IOctopusRepository and OctopusRepository repository properties are out-of-sync"; + var missingInInts = imps.Except(ints); + + if (missingInInts.Any()) + { + msg += "\nMissing on Interface: " + string.Join(", ", missingInInts); + } + + return msg; + } + } +} diff --git a/source/Octopus.Client.Tests/Operations/RegisterMachineOperationFixture.cs b/source/Octopus.Client.Tests/Operations/RegisterMachineOperationFixture.cs new file mode 100644 index 000000000..35243ae41 --- /dev/null +++ b/source/Octopus.Client.Tests/Operations/RegisterMachineOperationFixture.cs @@ -0,0 +1,140 @@ +using System; +using System.Linq; +using NSubstitute; +using NUnit.Framework; +using Octopus.Client.Exceptions; +using Octopus.Client.Model; +using Octopus.Client.Model.Endpoints; +using Octopus.Client.Operations; + +namespace Octopus.Client.Tests.Operations +{ + [TestFixture] + public class RegisterMachineOperationFixture + { + RegisterMachineOperation operation; + IOctopusClientFactory clientFactory; + IOctopusClient client; + OctopusServerEndpoint serverEndpoint; + ResourceCollection environments; + ResourceCollection machines; + + [SetUp] + public void SetUp() + { + clientFactory = Substitute.For(); + client = Substitute.For(); + clientFactory.CreateClient(Arg.Any()).Returns(client); + operation = new RegisterMachineOperation(clientFactory); + serverEndpoint = new OctopusServerEndpoint("http://octopus", "ABC123"); + + environments = new ResourceCollection(new EnvironmentResource[0], LinkCollection.Self("/foo")); + machines = new ResourceCollection(new MachineResource[0], LinkCollection.Self("/foo")); + client.RootDocument.Returns(new RootResource {Links = LinkCollection.Self("/api").Add("Environments", "/api/environments").Add("Machines", "/api/machines")}); + + client.When(x => x.Paginate(Arg.Any(), Arg.Any(), Arg.Any, bool>>())) + .Do(ci => ci.Arg, bool>>()(environments)); + + client.When(x => x.Paginate(Arg.Any(), Arg.Any(), Arg.Any, bool>>())) + .Do(ci => ci.Arg, bool>>()(machines)); + + client.List(Arg.Any(), Arg.Any()).Returns(machines); + } + + [Test] + public void ShouldThrowIfEnvironmentNotFound() + { + operation.EnvironmentNames = new[] {"Atlantis"}; + + var ex = Assert.Throws(() => operation.Execute(serverEndpoint)); + Assert.That(ex.Message, Is.EqualTo("Could not find the environment Atlantis on the Octopus server.")); + } +#pragma warning disable 618 + [Test] + public void ShouldCreateNewMachine() + { + environments.Items.Add(new EnvironmentResource {Id = "environments-1", Name = "UAT", Links = LinkCollection.Self("/api/environments/environments-1").Add("Machines", "/api/environments/environments-1/machines")}); + environments.Items.Add(new EnvironmentResource {Id = "environments-2", Name = "Production", Links = LinkCollection.Self("/api/environments/environments-2").Add("Machines", "/api/environments/environments-2/machines")}); + + operation.TentacleThumbprint = "ABCDEF"; + operation.TentaclePort = 10930; + operation.MachineName = "Mymachine"; + operation.TentacleHostname = "Mymachine.test.com"; + operation.CommunicationStyle = CommunicationStyle.TentaclePassive; + operation.EnvironmentNames = new[] {"Production"}; + + operation.Execute(serverEndpoint); + + client.Received().Create("/api/machines", Arg.Is(m => + m.Name == "Mymachine" + && ((ListeningTentacleEndpointResource)m.Endpoint).Uri == "https://mymachine.test.com:10930/" + && m.EnvironmentIds.First() == "environments-2")); + } + + [Test] + public void ShouldNotUpdateExistingMachine() + { + environments.Items.Add(new EnvironmentResource {Id = "environments-1", Name = "UAT", Links = LinkCollection.Self("/api/environments/environments-1").Add("Machines", "/api/environments/environments-1/machines")}); + environments.Items.Add(new EnvironmentResource {Id = "environments-2", Name = "Production", Links = LinkCollection.Self("/api/environments/environments-2").Add("Machines", "/api/environments/environments-2/machines")}); + + machines.Items.Add(new MachineResource {Id = "machines/84", EnvironmentIds = new ReferenceCollection(new[] {"environments-1"}), Name = "Mymachine", Links = LinkCollection.Self("/machines/whatever/1")}); + + operation.TentacleThumbprint = "ABCDEF"; + operation.TentaclePort = 10930; + operation.MachineName = "Mymachine"; + operation.TentacleHostname = "Mymachine.test.com"; + operation.CommunicationStyle = CommunicationStyle.TentaclePassive; + operation.EnvironmentNames = new[] {"Production"}; + + Assert.Throws(() => operation.Execute(serverEndpoint)); + } + + [Test] + public void ShouldUpdateExistingMachineWhenForceIsEnabled() + { + environments.Items.Add(new EnvironmentResource {Id = "environments-1", Name = "UAT", Links = LinkCollection.Self("/api/environments/environments-1").Add("Machines", "/api/environments/environments-1/machines")}); + environments.Items.Add(new EnvironmentResource {Id = "environments-2", Name = "Production", Links = LinkCollection.Self("/api/environments/environments-2").Add("Machines", "/api/environments/environments-2/machines")}); + + machines.Items.Add(new MachineResource {Id = "machines/84", EnvironmentIds = new ReferenceCollection(new[] {"environments-1"}), Name = "Mymachine", Links = LinkCollection.Self("/machines/whatever/1")}); + + operation.TentacleThumbprint = "ABCDEF"; + operation.TentaclePort = 10930; + operation.MachineName = "Mymachine"; + operation.TentacleHostname = "Mymachine.test.com"; + operation.CommunicationStyle = CommunicationStyle.TentaclePassive; + operation.EnvironmentNames = new[] {"Production"}; + operation.AllowOverwrite = true; + + operation.Execute(serverEndpoint); + + client.Received().Update("/machines/whatever/1", Arg.Is(m => + m.Id == "machines/84" + && m.Name == "Mymachine" + && m.EnvironmentIds.First() == "environments-2")); + } + + [Test] + public void ShouldCreateWhenCantDeserializeMachines() + { + client.When(x => x.Paginate(Arg.Any(), Arg.Any(), Arg.Any, bool>>())) + .Throw(new OctopusDeserializationException(1, "Can not deserialize")); + + environments.Items.Add(new EnvironmentResource { Id = "environments-1", Name = "UAT", Links = LinkCollection.Self("/api/environments/environments-1").Add("Machines", "/api/environments/environments-1/machines") }); + environments.Items.Add(new EnvironmentResource { Id = "environments-2", Name = "Production", Links = LinkCollection.Self("/api/environments/environments-2").Add("Machines", "/api/environments/environments-2/machines") }); + + operation.TentacleThumbprint = "ABCDEF"; + operation.TentaclePort = 10930; + operation.MachineName = "Mymachine"; + operation.TentacleHostname = "Mymachine.test.com"; + operation.CommunicationStyle = CommunicationStyle.TentaclePassive; + operation.EnvironmentNames = new[] { "Production" }; + + operation.Execute(serverEndpoint); + + client.Received().Create("/api/machines", Arg.Is(m => + m.Name == "Mymachine" + && ((ListeningTentacleEndpointResource)m.Endpoint).Uri == "https://mymachine.test.com:10930/" + && m.EnvironmentIds.First() == "environments-2")); + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client.Tests/Serialization/ControlConverterTests.cs b/source/Octopus.Client.Tests/Serialization/ControlConverterTests.cs new file mode 100644 index 000000000..44921f341 --- /dev/null +++ b/source/Octopus.Client.Tests/Serialization/ControlConverterTests.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using FluentAssertions; +using Newtonsoft.Json; +using NUnit.Framework; +using Octopus.Client.Model.Forms; +using Octopus.Client.Serialization; + +namespace Octopus.Client.Tests.Serialization +{ + public class ControlConverterTests + { + [Test] + public void CanDeserializeVariableValue() + { + var input = new + { + Name = "name", + Label = "lbl", + Type= "VariableValue", + Description = "desc", + IsSecure = true + }; + + var result = Execute(input); + result.Label.Should().Be(input.Label); + result.Name.Should().Be(input.Name); + result.Description.Should().Be(input.Description); + result.IsSecure.Should().Be(input.IsSecure); + } + + [Test] + public void CanDeserializePreV3_1_6VariableValue() + { + var input = new + { + Label = "lbl", + Type= "VariableValue", + Description = "desc", + IsSecure = true + }; + + var result = Execute(input); + result.Label.Should().Be(input.Label); + result.Name.Should().Be(input.Label); + result.Description.Should().Be(input.Description); + result.IsSecure.Should().Be(input.IsSecure); + } + + private static T Execute(object input) + { + var settings = JsonSerialization.GetDefaultSerializerSettings(); + var json = JsonConvert.SerializeObject(input, settings); + return (T)new ControlConverter() + .ReadJson( + new JsonTextReader(new StringReader(json)), + typeof(T), + null, + JsonSerializer.Create(settings) + ); + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client.Tests/project.json b/source/Octopus.Client.Tests/project.json new file mode 100644 index 000000000..fc4bf745b --- /dev/null +++ b/source/Octopus.Client.Tests/project.json @@ -0,0 +1,32 @@ +{ + "version": "0.0.0-*", + "dependencies": { + "Serilog": "2.1.0", + "Octopus.Client": { + "target": "project" + }, + "NUnit": "3.4.1", + "dotnet-test-nunit": "3.4.0-beta-2", + "NSubstitute": "2.0.0-rc", + "FluentAssertions": "4.12.0", + "Autofac": "4.1.0", + "Nancy": "2.0.0-barneyrubble", + "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.AspNetCore.Server.Kestrel.Https": "1.0.0", + "Microsoft.AspNetCore.Owin": "1.0.0" + }, + "testRunner": "nunit", + "buildOptions": { + "embed": [ "**/*.pfx"] + }, + "frameworks": { + "net452": { + "dependencies": { + "Best.Conventional": "1.3.0.118" + }, + "buildOptions": { + "define": [ "HAS_BEST_CONVENTIONAL" ] + } + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/DefaultLinkResolver.cs b/source/Octopus.Client/DefaultLinkResolver.cs new file mode 100644 index 000000000..2812a0466 --- /dev/null +++ b/source/Octopus.Client/DefaultLinkResolver.cs @@ -0,0 +1,71 @@ +using System; + +namespace Octopus.Client +{ + /// + /// In the same way web browsers can follow links like "/api/foo" by knowing the URI of the current page, this class + /// allows application + /// links to be resolved into fully-qualified URI's. This implementation also supports virtual directories. It assumes + /// that the + /// API endpoint starts with /api. + /// + public class DefaultLinkResolver : ILinkResolver + { + readonly string baseUri; + readonly string rootUri; + + /// + /// Initializes a new instance of the class. + /// + /// The root URI of the server. + /// + /// A segment that users might or might not include when entering the root URI. If the + /// segment exists, it will be ignored. + /// + public DefaultLinkResolver(Uri root, string allUrisStartWith = "/api") + { + allUrisStartWith = (allUrisStartWith.EndsWith("/") ? allUrisStartWith : allUrisStartWith + "/"); + var applicationBaseUri = root.ToString(); + applicationBaseUri = (applicationBaseUri.EndsWith("/") ? applicationBaseUri : applicationBaseUri + "/"); + + var indexOfMandatorySegment = applicationBaseUri.LastIndexOf(allUrisStartWith, StringComparison.OrdinalIgnoreCase); + if (indexOfMandatorySegment >= 1) + { + applicationBaseUri = applicationBaseUri.Substring(0, indexOfMandatorySegment); + } + + baseUri = applicationBaseUri.TrimEnd('/'); + var parsed = new Uri(baseUri); + rootUri = parsed.Scheme + "://" + parsed.Authority; + } + + /// + /// Resolves the specified link into a fully qualified URI. + /// + /// The application relative link (should begin with a /). + /// + /// The fully resolved URI. + /// + public Uri Resolve(string link) + { + if (link.StartsWith("~/")) + { + return new Uri(baseUri + "/" + link.TrimStart('~', '/')); + } + + if (!link.StartsWith("/")) link = '/' + link; + return new Uri(rootUri + link); + } + + /// + /// Returns a that represents the root URI that URI's are resolved from. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + return baseUri; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/ChannelEditor.cs b/source/Octopus.Client/Editors/ChannelEditor.cs new file mode 100644 index 000000000..706ced146 --- /dev/null +++ b/source/Octopus.Client/Editors/ChannelEditor.cs @@ -0,0 +1,124 @@ +using System; +using Octopus.Client.Model; +using Octopus.Client.Repositories; + +namespace Octopus.Client.Editors +{ + public class ChannelEditor : IResourceEditor + { + private readonly IChannelRepository repository; + + public ChannelEditor(IChannelRepository repository) + { + this.repository = repository; + } + + public ChannelResource Instance { get; private set; } + + public ChannelEditor CreateOrModify(ProjectResource project, string name) + { + var existing = repository.FindByName(project, name); + + if (existing == null) + { + Instance = repository.Create(new ChannelResource + { + ProjectId = project.Id, + Name = name + }); + } + else + { + existing.Name = name; + + Instance = repository.Modify(existing); + } + + return this; + } + + public ChannelEditor CreateOrModify(ProjectResource project, string name, string description) + { + var existing = repository.FindByName(project, name); + + if (existing == null) + { + Instance = repository.Create(new ChannelResource + { + ProjectId = project.Id, + Name = name, + Description = description + }); + } + else + { + existing.Name = name; + existing.Description = description; + + Instance = repository.Modify(existing); + } + + return this; + } + + public ChannelEditor SetAsDefaultChannel() + { + Instance.SetAsDefaultChannel(); + return this; + } + + public ChannelEditor UsingLifecycle(LifecycleResource lifecycle) + { + Instance.UsingLifecycle(lifecycle); + return this; + } + + public ChannelEditor ClearRules() + { + Instance.ClearRules(); + return this; + } + + public ChannelEditor AddRule(ChannelVersionRuleResource rule) + { + Instance.AddRule(rule); + return this; + } + + public ChannelEditor AddCommonRuleForAllActions(string versionRange, string tagRegex, DeploymentProcessResource process) + { + Instance.AddCommonRuleForAllActions(versionRange, tagRegex, process); + return this; + } + + public ChannelEditor AddRule(string versionRange, string tagRegex, params DeploymentActionResource[] actions) + { + Instance.AddRule(versionRange, tagRegex, actions); + return this; + } + + public ChannelEditor ClearTenantTags() + { + Instance.ClearTenantTags(); + return this; + } + + public ChannelEditor AddOrUpdateTenantTags(params TagResource[] tags) + { + Instance.AddOrUpdateTenantTags(tags); + return this; + } + + public ChannelEditor Customize(Action customize) + { + customize?.Invoke(Instance); + return this; + } + + public ChannelEditor Save() + { + Instance = repository.Modify(Instance); + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/DeploymentProcess/DeploymentProcessEditor.cs b/source/Octopus.Client/Editors/DeploymentProcess/DeploymentProcessEditor.cs new file mode 100644 index 000000000..b55207b4f --- /dev/null +++ b/source/Octopus.Client/Editors/DeploymentProcess/DeploymentProcessEditor.cs @@ -0,0 +1,58 @@ +using System; +using Octopus.Client.Model; +using Octopus.Client.Repositories; + +namespace Octopus.Client.Editors.DeploymentProcess +{ + public class DeploymentProcessEditor : IResourceEditor + { + private readonly IDeploymentProcessRepository repository; + + public DeploymentProcessEditor(IDeploymentProcessRepository repository) + { + this.repository = repository; + } + + public DeploymentProcessResource Instance { get; private set; } + + public DeploymentProcessEditor Load(string id) + { + Instance = repository.Get(id); + return this; + } + + public DeploymentStepResource FindStep(string name) + { + return Instance.FindStep(name); + } + + public DeploymentStepResource AddOrUpdateStep(string name) + { + return Instance.AddOrUpdateStep(name); + } + + public DeploymentProcessEditor RemoveStep(string name) + { + Instance.RemoveStep(name); + return this; + } + + public DeploymentProcessEditor ClearSteps() + { + Instance.ClearSteps(); + return this; + } + + public DeploymentProcessEditor Customize(Action customize) + { + customize?.Invoke(Instance); + return this; + } + + public DeploymentProcessEditor Save() + { + Instance = repository.Modify(Instance); + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/DeploymentProcess/InlineScriptAction.cs b/source/Octopus.Client/Editors/DeploymentProcess/InlineScriptAction.cs new file mode 100644 index 000000000..d5bbc9eb1 --- /dev/null +++ b/source/Octopus.Client/Editors/DeploymentProcess/InlineScriptAction.cs @@ -0,0 +1,21 @@ +using System; +using Octopus.Client.Model; + +namespace Octopus.Client.Editors.DeploymentProcess +{ + public class InlineScriptAction : ScriptAction + { + private readonly string scriptBody; + + public InlineScriptAction(ScriptSyntax syntax, string scriptBody) + { + this.scriptBody = scriptBody; + Syntax = syntax; + } + + public override ScriptSource Source => ScriptSource.Inline; + public override ScriptSyntax Syntax { get; } + + public string GetScriptBody() => scriptBody; + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/DeploymentProcess/InlineScriptActionFromFileInAssembly.cs b/source/Octopus.Client/Editors/DeploymentProcess/InlineScriptActionFromFileInAssembly.cs new file mode 100644 index 000000000..8bd26f825 --- /dev/null +++ b/source/Octopus.Client/Editors/DeploymentProcess/InlineScriptActionFromFileInAssembly.cs @@ -0,0 +1,40 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using Octopus.Client.Model; + +namespace Octopus.Client.Editors.DeploymentProcess +{ + public class InlineScriptActionFromFileInAssembly : ScriptAction + { + public InlineScriptActionFromFileInAssembly(string resourceName, Assembly resourceAssembly = null) + { + ResourceAssembly = resourceAssembly ?? Assembly.GetEntryAssembly(); + var candidates = ResourceAssembly.GetManifestResourceNames().Where(name => name.EndsWith(resourceName)).ToArray(); + if (!candidates.Any()) + throw new ArgumentException($"There is no embedded resource in {ResourceAssembly.GetName().Name} like {resourceName}. Available resources are:{Environment.NewLine}{string.Join(Environment.NewLine, ResourceAssembly.GetManifestResourceNames().OrderBy(x => x))}"); + + if (candidates.Length > 1) + throw new ArgumentException($"There are {candidates.Length} embedded resources in {ResourceAssembly.GetName().Name} like {resourceName}, which one do you want?{Environment.NewLine}{string.Join(Environment.NewLine, candidates.OrderBy(x => x))}"); + + ResourceName = candidates.Single(); + } + + public override ScriptSource Source => ScriptSource.Inline; + public override ScriptSyntax Syntax => CalculateScriptType(ResourceName); + public Assembly ResourceAssembly { get; } + public string ResourceName { get; } + public string GetScriptBody() + { + using (var stream = ResourceAssembly.GetManifestResourceStream(ResourceName)) + { + if (stream == null) throw new ArgumentException(nameof(ResourceName), $"There is no Embedded Resource '{ResourceName}' in {ResourceAssembly}"); + using (var reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/DeploymentProcess/ScriptAction.cs b/source/Octopus.Client/Editors/DeploymentProcess/ScriptAction.cs new file mode 100644 index 000000000..331e116ee --- /dev/null +++ b/source/Octopus.Client/Editors/DeploymentProcess/ScriptAction.cs @@ -0,0 +1,32 @@ +using System; +using System.IO; +using System.Reflection; +using Octopus.Client.Model; + +namespace Octopus.Client.Editors.DeploymentProcess +{ + public abstract class ScriptAction + { + public abstract ScriptSource Source { get; } + public abstract ScriptSyntax Syntax { get; } + + protected ScriptSyntax CalculateScriptType(string scriptFileName) + { + if (string.Equals(Path.GetExtension(scriptFileName), ".ps1", StringComparison.OrdinalIgnoreCase)) return ScriptSyntax.PowerShell; + if (string.Equals(Path.GetExtension(scriptFileName), ".csx", StringComparison.OrdinalIgnoreCase)) return ScriptSyntax.CSharp; + if (string.Equals(Path.GetExtension(scriptFileName), ".sh", StringComparison.OrdinalIgnoreCase)) return ScriptSyntax.Bash; + if (string.Equals(Path.GetExtension(scriptFileName), ".fsx", StringComparison.OrdinalIgnoreCase)) return ScriptSyntax.FSharp; + throw new NotSupportedException($"{scriptFileName} is not one of the well known script types supported by Octopus Deploy."); + } + + public static InlineScriptAction InlineScript(ScriptSyntax syntax, string scriptBody) + { + return new InlineScriptAction(syntax, scriptBody); + } + + public static InlineScriptActionFromFileInAssembly InlineScriptFromFileInAssembly(string resourceName, Assembly resourceAssembly = null) + { + return new InlineScriptActionFromFileInAssembly(resourceName, resourceAssembly); + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/DeploymentProcess/ScriptActionFromFileInPackage.cs b/source/Octopus.Client/Editors/DeploymentProcess/ScriptActionFromFileInPackage.cs new file mode 100644 index 000000000..fff446c75 --- /dev/null +++ b/source/Octopus.Client/Editors/DeploymentProcess/ScriptActionFromFileInPackage.cs @@ -0,0 +1,21 @@ +using System; +using Octopus.Client.Model; + +namespace Octopus.Client.Editors.DeploymentProcess +{ + public class ScriptActionFromFileInPackage : ScriptAction + { + public ScriptActionFromFileInPackage(PackageResource package, string scriptFilePath) + { + PackageFeedId = package.FeedId; + PackageId = package.PackageId; + ScriptFilePath = scriptFilePath; + } + + public override ScriptSource Source => ScriptSource.Package; + public override ScriptSyntax Syntax => CalculateScriptType(ScriptFilePath); + public string PackageFeedId { get; protected set; } + public string PackageId { get; protected set; } + public string ScriptFilePath { get; protected set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/DeploymentProcess/ScriptSource.cs b/source/Octopus.Client/Editors/DeploymentProcess/ScriptSource.cs new file mode 100644 index 000000000..c115d63be --- /dev/null +++ b/source/Octopus.Client/Editors/DeploymentProcess/ScriptSource.cs @@ -0,0 +1,10 @@ +using System; + +namespace Octopus.Client.Editors.DeploymentProcess +{ + public enum ScriptSource + { + Inline, + Package, + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/DeploymentProcess/ScriptTarget.cs b/source/Octopus.Client/Editors/DeploymentProcess/ScriptTarget.cs new file mode 100644 index 000000000..eb1e4592f --- /dev/null +++ b/source/Octopus.Client/Editors/DeploymentProcess/ScriptTarget.cs @@ -0,0 +1,10 @@ +using System; + +namespace Octopus.Client.Editors.DeploymentProcess +{ + public enum ScriptTarget + { + Server, + Target + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/EnvironmentEditor.cs b/source/Octopus.Client/Editors/EnvironmentEditor.cs new file mode 100644 index 000000000..2dc626148 --- /dev/null +++ b/source/Octopus.Client/Editors/EnvironmentEditor.cs @@ -0,0 +1,72 @@ +using System; +using Octopus.Client.Model; +using Octopus.Client.Repositories; + +namespace Octopus.Client.Editors +{ + public class EnvironmentEditor : IResourceEditor + { + private readonly IEnvironmentRepository repository; + + public EnvironmentEditor(IEnvironmentRepository repository) + { + this.repository = repository; + } + + public EnvironmentResource Instance { get; private set; } + + public EnvironmentEditor CreateOrModify(string name) + { + var existing = repository.FindByName(name); + if (existing == null) + { + Instance = repository.Create(new EnvironmentResource + { + Name = name, + }); + } + else + { + existing.Name = name; + + Instance = repository.Modify(existing); + } + + return this; + } + + public EnvironmentEditor CreateOrModify(string name, string description) + { + var existing = repository.FindByName(name); + if (existing == null) + { + Instance = repository.Create(new EnvironmentResource + { + Name = name, + Description = description + }); + } + else + { + existing.Name = name; + existing.Description = description; + + Instance = repository.Modify(existing); + } + + return this; + } + + public EnvironmentEditor Customize(Action customize) + { + customize?.Invoke(Instance); + return this; + } + + public EnvironmentEditor Save() + { + Instance = repository.Modify(Instance); + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/IResourceEditor.cs b/source/Octopus.Client/Editors/IResourceEditor.cs new file mode 100644 index 000000000..197b00101 --- /dev/null +++ b/source/Octopus.Client/Editors/IResourceEditor.cs @@ -0,0 +1,19 @@ +using System; +using Octopus.Client.Model; + +namespace Octopus.Client.Editors +{ + public interface IResourceEditor : IResourceBuilder + where TResource : Resource + where TResourceBuilder : IResourceBuilder + { + TResource Instance { get; } + TResourceBuilder Customize(Action customize); + TResourceBuilder Save(); + } + + public interface IResourceBuilder + { + + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/LibraryVariableSetEditor.cs b/source/Octopus.Client/Editors/LibraryVariableSetEditor.cs new file mode 100644 index 000000000..da6478bec --- /dev/null +++ b/source/Octopus.Client/Editors/LibraryVariableSetEditor.cs @@ -0,0 +1,84 @@ +using System; +using Octopus.Client.Model; +using Octopus.Client.Repositories; + +namespace Octopus.Client.Editors +{ + public class LibraryVariableSetEditor : IResourceEditor + { + private readonly ILibraryVariableSetRepository repository; + private readonly Lazy variables; + + public LibraryVariableSetEditor(ILibraryVariableSetRepository repository, IVariableSetRepository variableSetRepository) + { + this.repository = repository; + variables = new Lazy(() => new VariableSetEditor(variableSetRepository).Load(Instance.VariableSetId)); + } + + public LibraryVariableSetResource Instance { get; private set; } + + public VariableSetEditor Variables => variables.Value; + + public IVariableTemplateContainerEditor VariableTemplates => Instance; + + public LibraryVariableSetEditor CreateOrModify(string name) + { + var existing = repository.FindByName(name); + + if (existing == null) + { + Instance = repository.Create(new LibraryVariableSetResource + { + Name = name, + }); + } + else + { + existing.Name = name; + + Instance = repository.Modify(existing); + } + + return this; + } + + public LibraryVariableSetEditor CreateOrModify(string name, string description) + { + var existing = repository.FindByName(name); + + if (existing == null) + { + Instance = repository.Create(new LibraryVariableSetResource + { + Name = name, + Description = description + }); + } + else + { + existing.Name = name; + existing.Description = description; + + Instance = repository.Modify(existing); + } + + return this; + } + + public LibraryVariableSetEditor Customize(Action customize) + { + customize?.Invoke(Instance); + return this; + } + + public LibraryVariableSetEditor Save() + { + Instance = repository.Modify(Instance); + if (variables.IsValueCreated) + { + variables.Value.Save(); + } + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/LifecycleEditor.cs b/source/Octopus.Client/Editors/LifecycleEditor.cs new file mode 100644 index 000000000..2f7491d15 --- /dev/null +++ b/source/Octopus.Client/Editors/LifecycleEditor.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using Octopus.Client.Model; +using Octopus.Client.Repositories; + +namespace Octopus.Client.Editors +{ + public class LifecycleEditor : IResourceEditor + { + private readonly ILifecyclesRepository repository; + + public LifecycleEditor(ILifecyclesRepository repository) + { + this.repository = repository; + } + + public LifecycleResource Instance { get; private set; } + + public LifecycleEditor CreateOrModify(string name) + { + var existing = repository.FindByName(name); + if (existing == null) + { + Instance = repository.Create(new LifecycleResource + { + Name = name, + }); + } + else + { + existing.Name = name; + + Instance = repository.Modify(existing); + } + + return this; + } + + public LifecycleEditor CreateOrModify(string name, string description) + { + var existing = repository.FindByName(name); + if (existing == null) + { + Instance = repository.Create(new LifecycleResource + { + Name = name, + Description = description + }); + } + else + { + existing.Name = name; + existing.Description = description; + + Instance = repository.Modify(existing); + } + + return this; + } + + public PhaseResource AddOrUpdatePhase(string name) + { + return Instance.AddOrUpdatePhase(name); + } + + public LifecycleEditor AsSimplePromotionLifecycle(IEnumerable environments) + { + Clear(); + + foreach (var environment in environments) + { + AddOrUpdatePhase(environment.Name).WithOptionalDeploymentTargets(environment); + } + + return this; + } + + public LifecycleEditor Clear() + { + Instance.Clear(); + return this; + } + + public LifecycleEditor Customize(Action customize) + { + customize?.Invoke(Instance); + return this; + } + + public LifecycleEditor Save() + { + Instance = repository.Modify(Instance); + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/MachineEditor.cs b/source/Octopus.Client/Editors/MachineEditor.cs new file mode 100644 index 000000000..1044a70f0 --- /dev/null +++ b/source/Octopus.Client/Editors/MachineEditor.cs @@ -0,0 +1,128 @@ +using System; +using System.Linq; +using Octopus.Client.Model; +using Octopus.Client.Model.Endpoints; +using Octopus.Client.Repositories; + +namespace Octopus.Client.Editors +{ + public class MachineEditor : IResourceEditor + { + private readonly IMachineRepository repository; + + public MachineEditor(IMachineRepository repository) + { + this.repository = repository; + } + + public MachineResource Instance { get; private set; } + + public MachineEditor CreateOrModify( + string name, + EndpointResource endpoint, + EnvironmentResource[] environments, + string[] roles) + { + var existing = repository.FindByName(name); + if (existing == null) + { + Instance = repository.Create(new MachineResource + { + Name = name, + Endpoint = endpoint, + EnvironmentIds = new ReferenceCollection(environments.Select(e => e.Id)), + Roles = new ReferenceCollection(roles) + }); + } + else + { + existing.Name = name; + existing.Endpoint = endpoint; + existing.EnvironmentIds.ReplaceAll(environments.Select(e => e.Id)); + existing.Roles.ReplaceAll(roles); + + Instance = repository.Modify(existing); + } + + return this; + } + + public MachineEditor CreateOrModify( + string name, + EndpointResource endpoint, + EnvironmentResource[] environments, + string[] roles, + TenantResource[] tenants, + TagResource[] tenantTags) + { + var existing = repository.FindByName(name); + if (existing == null) + { + Instance = repository.Create(new MachineResource + { + Name = name, + Endpoint = endpoint, + EnvironmentIds = new ReferenceCollection(environments.Select(e => e.Id)), + Roles = new ReferenceCollection(roles), + TenantIds = new ReferenceCollection(tenants.Select(t => t.Id)), + TenantTags = new ReferenceCollection(tenantTags.Select(t => t.CanonicalTagName)) + }); + } + else + { + existing.Name = name; + existing.Endpoint = endpoint; + existing.EnvironmentIds.ReplaceAll(environments.Select(e => e.Id)); + existing.Roles.ReplaceAll(roles); + existing.TenantIds.ReplaceAll(tenants.Select(t => t.Id)); + existing.TenantTags.ReplaceAll(tenantTags.Select(t => t.CanonicalTagName)); + + Instance = repository.Modify(existing); + } + + return this; + } + + //public IEnumerable BuildSamples( + // this IMachineRepository repo, + // int numberOfDeploymentTargets, + // EnvironmentResource[] environments, + // string[] roles, + // Func nameBuilder = null, + // Action customizeEndpoint = null, + // Action customizeDeploymentTarget = null) + // where TEndpoint : EndpointResource, new() + //{ + // Log.Information("Building {Count} sample {Endpoint}...", numberOfDeploymentTargets, typeof(TEndpoint).Name); + + // var namer = nameBuilder ?? (i => RandomStringGenerator.Generate(16)); + // return Enumerable.Range(1, numberOfDeploymentTargets).Select(i => repository.Build(namer(i), environments, roles, customizeEndpoint, customizeDeploymentTarget)); + //} + + //public MachineResource ForTenant(this MachineResource machine, TenantResource tenant) + //{ + // machine.TenantIds.Add(tenant.Id); + + // return machine; + //} + + //public MachineResource ForTenantsWithTag(this MachineResource machine, TagResource tag) + //{ + // machine.TenantTags.Add(tag.CanonicalTagName); + + // return machine; + //} + + public MachineEditor Customize(Action customize) + { + customize?.Invoke(Instance); + return this; + } + + public MachineEditor Save() + { + Instance = repository.Modify(Instance); + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/ProjectChannelsEditor.cs b/source/Octopus.Client/Editors/ProjectChannelsEditor.cs new file mode 100644 index 000000000..d3dbd9412 --- /dev/null +++ b/source/Octopus.Client/Editors/ProjectChannelsEditor.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using Octopus.Client.Model; +using Octopus.Client.Repositories; + +namespace Octopus.Client.Editors +{ + public class ProjectChannelsEditor + { + private readonly IChannelRepository repository; + private readonly ProjectResource owner; + private readonly List trackedChannelBuilders = new List(); + + public ProjectChannelsEditor(IChannelRepository repository, ProjectResource owner) + { + this.repository = repository; + this.owner = owner; + } + + public ChannelEditor CreateOrModify(string name) + { + var channelBuilder = new ChannelEditor(repository).CreateOrModify(owner, name); + trackedChannelBuilders.Add(channelBuilder); + return channelBuilder; + } + + public ChannelEditor CreateOrModify(string name, string description) + { + var channelBuilder = new ChannelEditor(repository).CreateOrModify(owner, name, description); + trackedChannelBuilders.Add(channelBuilder); + return channelBuilder; + } + + public ProjectChannelsEditor Delete(string name) + { + var channel = repository.FindByName(owner, name); + if (channel != null) repository.Delete(channel); + return this; + } + + public ProjectChannelsEditor SaveAll() + { + trackedChannelBuilders.ForEach(x => x.Save()); + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/ProjectEditor.cs b/source/Octopus.Client/Editors/ProjectEditor.cs new file mode 100644 index 000000000..da826e747 --- /dev/null +++ b/source/Octopus.Client/Editors/ProjectEditor.cs @@ -0,0 +1,139 @@ +using System; +using System.IO; +using Octopus.Client.Editors.DeploymentProcess; +using Octopus.Client.Model; +using Octopus.Client.Repositories; + +namespace Octopus.Client.Editors +{ + public class ProjectEditor : IResourceEditor + { + private readonly IProjectRepository repository; + private readonly Lazy channels; + private readonly Lazy deploymentProcess; + private readonly Lazy triggers; + private readonly Lazy variables; + + public ProjectEditor( + IProjectRepository repository, + IChannelRepository channelRepository, + IDeploymentProcessRepository deploymentProcessRepository, + IProjectTriggerRepository projectTriggerRepository, + IVariableSetRepository variableSetRepository) + { + this.repository = repository; + channels = new Lazy(() => new ProjectChannelsEditor(channelRepository, Instance)); + deploymentProcess = new Lazy(() => new DeploymentProcessEditor(deploymentProcessRepository).Load(Instance.DeploymentProcessId)); + triggers = new Lazy(() => new ProjectTriggersEditor(projectTriggerRepository, Instance)); + variables = new Lazy(() => new VariableSetEditor(variableSetRepository).Load(Instance.VariableSetId)); + } + + public ProjectResource Instance { get; private set; } + + public ProjectChannelsEditor Channels => channels.Value; + + public DeploymentProcessEditor DeploymentProcess => deploymentProcess.Value; + + public ProjectTriggersEditor Triggers => triggers.Value; + + public VariableSetEditor Variables => variables.Value; + + public IVariableTemplateContainerEditor VariableTemplates => Instance; + + public ProjectEditor CreateOrModify(string name, ProjectGroupResource projectGroup, LifecycleResource lifecycle) + { + var existing = repository.FindByName(name); + + if (existing == null) + { + Instance = repository.Create(new ProjectResource + { + Name = name, + ProjectGroupId = projectGroup.Id, + LifecycleId = lifecycle.Id, + }); + } + else + { + existing.Name = name; + existing.ProjectGroupId = projectGroup.Id; + existing.LifecycleId = lifecycle.Id; + + Instance = repository.Modify(existing); + } + + return this; + } + + public ProjectEditor CreateOrModify(string name, ProjectGroupResource projectGroup, LifecycleResource lifecycle, string description) + { + var existing = repository.FindByName(name); + + if (existing == null) + { + Instance = repository.Create(new ProjectResource + { + Name = name, + ProjectGroupId = projectGroup.Id, + LifecycleId = lifecycle.Id, + Description = description + }); + } + else + { + existing.Name = name; + existing.ProjectGroupId = projectGroup.Id; + existing.LifecycleId = lifecycle.Id; + existing.Description = description; + + Instance = repository.Modify(existing); + } + + return this; + } + + public ProjectEditor SetLogo(string logoFilePath) + { + using (var stream = new FileStream(logoFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + repository.SetLogo(Instance, Path.GetFileName(logoFilePath), stream); + } + + return this; + } + + public ProjectEditor IncludingLibraryVariableSets(params LibraryVariableSetResource[] libraryVariableSets) + { + Instance.IncludingLibraryVariableSets(libraryVariableSets); + return this; + } + + public ProjectEditor Customize(Action customize) + { + customize?.Invoke(Instance); + return this; + } + + public ProjectEditor Save() + { + Instance = repository.Modify(Instance); + if (channels.IsValueCreated) + { + channels.Value.SaveAll(); + } + if (deploymentProcess.IsValueCreated) + { + deploymentProcess.Value.Save(); + } + if (triggers.IsValueCreated) + { + triggers.Value.SaveAll(); + } + if (variables.IsValueCreated) + { + variables.Value.Save(); + } + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/ProjectGroupEditor.cs b/source/Octopus.Client/Editors/ProjectGroupEditor.cs new file mode 100644 index 000000000..4ff7c54c9 --- /dev/null +++ b/source/Octopus.Client/Editors/ProjectGroupEditor.cs @@ -0,0 +1,71 @@ +using System; +using Octopus.Client.Model; +using Octopus.Client.Repositories; + +namespace Octopus.Client.Editors +{ + public class ProjectGroupEditor : IResourceEditor + { + private readonly IProjectGroupRepository repository; + + public ProjectGroupEditor(IProjectGroupRepository repository) + { + this.repository = repository; + } + + public ProjectGroupResource Instance { get; private set; } + + public ProjectGroupEditor CreateOrModify(string name) + { + var existing = repository.FindByName(name); + if (existing == null) + { + Instance = repository.Create(new ProjectGroupResource + { + Name = name, + }); + } + else + { + existing.Name = name; + + Instance = repository.Modify(existing); + } + + return this; + } + public ProjectGroupEditor CreateOrModify(string name, string description) + { + var existing = repository.FindByName(name); + if (existing == null) + { + Instance = repository.Create(new ProjectGroupResource + { + Name = name, + Description = description + }); + } + else + { + existing.Name = name; + existing.Description = description; + + Instance = repository.Modify(existing); + } + + return this; + } + + public ProjectGroupEditor Customize(Action customize) + { + customize?.Invoke(Instance); + return this; + } + + public ProjectGroupEditor Save() + { + Instance = repository.Modify(Instance); + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/ProjectTriggerEditor.cs b/source/Octopus.Client/Editors/ProjectTriggerEditor.cs new file mode 100644 index 000000000..60938809c --- /dev/null +++ b/source/Octopus.Client/Editors/ProjectTriggerEditor.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using Octopus.Client.Model; +using Octopus.Client.Repositories; + +namespace Octopus.Client.Editors +{ + public class ProjectTriggerEditor : IResourceEditor + { + private readonly IProjectTriggerRepository repository; + + public ProjectTriggerEditor(IProjectTriggerRepository repository) + { + this.repository = repository; + } + + public ProjectTriggerResource Instance { get; private set; } + + public ProjectTriggerEditor CreateOrModify(ProjectResource project, string name, ProjectTriggerType type, params ProjectTriggerConditionEvent[] conditions) + { + var conditionsCsv = string.Join(",", conditions.Select(x => x.ToString()).ToArray()); + + var existing = repository.FindByName(project, name); + if (existing == null) + { + Instance = repository.Create(new ProjectTriggerResource + { + Name = name, + ProjectId = project.Id, + Type = type, + Properties = + { + {"Octopus.ProjectTriggerCondition.Events", new PropertyValueResource(conditionsCsv)} + } + }); + } + else + { + existing.Name = name; + existing.Type = type; + existing.Properties["Octopus.ProjectTriggerCondition.Events"] = new PropertyValueResource(conditionsCsv); + Instance = repository.Modify(existing); + } + + return this; + } + + public ProjectTriggerEditor Customize(Action customize) + { + customize?.Invoke(Instance); + return this; + } + + public ProjectTriggerEditor Save() + { + Instance = repository.Modify(Instance); + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/ProjectTriggersEditor.cs b/source/Octopus.Client/Editors/ProjectTriggersEditor.cs new file mode 100644 index 000000000..aefc45dec --- /dev/null +++ b/source/Octopus.Client/Editors/ProjectTriggersEditor.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using Octopus.Client.Model; +using Octopus.Client.Repositories; + +namespace Octopus.Client.Editors +{ + public class ProjectTriggersEditor + { + private readonly IProjectTriggerRepository repository; + private readonly ProjectResource owner; + private readonly List trackedProjectTriggerBuilders = new List(); + + public ProjectTriggersEditor(IProjectTriggerRepository repository, ProjectResource owner) + { + this.repository = repository; + this.owner = owner; + } + + public ProjectTriggerEditor CreateOrModify(string name, ProjectTriggerType type, params ProjectTriggerConditionEvent[] conditions) + { + var projectTriggerBuilder = new ProjectTriggerEditor(repository).CreateOrModify(owner, name, type, conditions); + trackedProjectTriggerBuilders.Add(projectTriggerBuilder); + return projectTriggerBuilder; + } + + public ProjectTriggersEditor Delete(string name) + { + var trigger = repository.FindByName(owner, name); + if (trigger != null) repository.Delete(trigger); + return this; + } + + public ProjectTriggersEditor SaveAll() + { + trackedProjectTriggerBuilders.ForEach(x => x.Save()); + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/TagSetEditor.cs b/source/Octopus.Client/Editors/TagSetEditor.cs new file mode 100644 index 000000000..9a95aa9f4 --- /dev/null +++ b/source/Octopus.Client/Editors/TagSetEditor.cs @@ -0,0 +1,89 @@ +using System; +using Octopus.Client.Model; +using Octopus.Client.Repositories; + +namespace Octopus.Client.Editors +{ + public class TagSetEditor : IResourceEditor + { + private readonly ITagSetRepository repository; + + public TagSetEditor(ITagSetRepository repository) + { + this.repository = repository; + } + + public TagSetResource Instance { get; private set; } + + public TagSetEditor CreateOrModify(string name) + { + var existing = repository.FindByName(name); + if (existing == null) + { + Instance = repository.Create(new TagSetResource + { + Name = name, + }); + } + else + { + existing.Name = name; + Instance = repository.Modify(existing); + } + + return this; + } + + public TagSetEditor CreateOrModify(string name, string description) + { + var existing = repository.FindByName(name); + if (existing == null) + { + Instance = repository.Create(new TagSetResource + { + Name = name, + }); + } + else + { + existing.Description = description; + Instance = repository.Modify(existing); + } + + return this; + } + + public TagSetEditor ClearTags() + { + Instance.Tags.Clear(); + return this; + } + + public TagSetEditor AddOrUpdateTag( + string name, + string description = null, + string color = TagResource.StandardColor.DarkGrey) + { + Instance.AddOrUpdateTag(name, description, color); + return this; + } + + public TagSetEditor RemoveTag(string name) + { + Instance.RemoveTag(name); + return this; + } + + public TagSetEditor Customize(Action customize) + { + customize?.Invoke(Instance); + return this; + } + + public TagSetEditor Save() + { + Instance = repository.Modify(Instance); + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/TenantEditor.cs b/source/Octopus.Client/Editors/TenantEditor.cs new file mode 100644 index 000000000..931a632e1 --- /dev/null +++ b/source/Octopus.Client/Editors/TenantEditor.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using Octopus.Client.Model; +using Octopus.Client.Repositories; + +namespace Octopus.Client.Editors +{ + public class TenantEditor : IResourceEditor + { + private readonly ITenantRepository repository; + private readonly Lazy variables; + + public TenantEditor(ITenantRepository repository) + { + this.repository = repository; + variables = new Lazy(() => new TenantVariablesEditor(repository, Instance).Load()); + } + + public TenantResource Instance { get; private set; } + + public TenantVariablesEditor Variables => variables.Value; + + public TenantEditor CreateOrModify(string name) + { + var existing = repository.FindByName(name); + if (existing == null) + { + Instance = repository.Create(new TenantResource + { + Name = name, + }); + } + else + { + existing.Name = name; + Instance = repository.Modify(existing); + } + + return this; + } + + public TenantEditor SetLogo(string logoFilePath) + { + using (var stream = new FileStream(logoFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + repository.SetLogo(Instance, Path.GetFileName(logoFilePath), stream); + } + + return this; + } + + public TenantEditor ClearTags() + { + Instance.ClearTags(); + return this; + } + + public TenantEditor WithTag(TagResource tag) + { + Instance.WithTag(tag); + return this; + } + + public TenantEditor ClearProjects() + { + Instance.ClearProjects(); + return this; + } + + public TenantEditor ConnectToProjectAndEnvironments(ProjectResource project, params EnvironmentResource[] environments) + { + Instance.ConnectToProjectAndEnvironments(project, environments); + return this; + } + + public TenantEditor Customize(Action customize) + { + customize?.Invoke(Instance); + return this; + } + + public TenantEditor Save() + { + Instance = repository.Modify(Instance); + if (variables.IsValueCreated) + { + variables.Value.Save(); + } + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/TenantVariablesEditor.cs b/source/Octopus.Client/Editors/TenantVariablesEditor.cs new file mode 100644 index 000000000..e198e5066 --- /dev/null +++ b/source/Octopus.Client/Editors/TenantVariablesEditor.cs @@ -0,0 +1,38 @@ +using System; +using Octopus.Client.Model; +using Octopus.Client.Repositories; + +namespace Octopus.Client.Editors +{ + public class TenantVariablesEditor : IResourceEditor + { + private readonly ITenantRepository repository; + private readonly TenantResource tenant; + + public TenantVariablesEditor(ITenantRepository repository, TenantResource tenant) + { + this.repository = repository; + this.tenant = tenant; + } + + public TenantVariableResource Instance { get; private set; } + + public TenantVariablesEditor Load() + { + Instance = repository.GetVariables(tenant); + return this; + } + + public TenantVariablesEditor Customize(Action customize) + { + customize?.Invoke(Instance); + return this; + } + + public TenantVariablesEditor Save() + { + Instance = repository.ModifyVariables(tenant, Instance); + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/VariableSetEditor.cs b/source/Octopus.Client/Editors/VariableSetEditor.cs new file mode 100644 index 000000000..29f149704 --- /dev/null +++ b/source/Octopus.Client/Editors/VariableSetEditor.cs @@ -0,0 +1,54 @@ +using System; +using Octopus.Client.Model; +using Octopus.Client.Repositories; + +namespace Octopus.Client.Editors +{ + public class VariableSetEditor : IResourceEditor + { + private readonly IVariableSetRepository repository; + + public VariableSetEditor(IVariableSetRepository repository) + { + this.repository = repository; + } + + public VariableSetResource Instance { get; private set; } + + public VariableSetEditor Load(string id) + { + Instance = repository.Get(id); + return this; + } + + public VariableSetEditor AddOrUpdateVariableValue(string name, string value, ScopeSpecification scope, bool isSensitive) + { + Instance.AddOrUpdateVariableValue(name, value, scope, isSensitive); + return this; + } + + public VariableSetEditor AddOrUpdateVariableValue(string name, string value, ScopeSpecification scope) + { + Instance.AddOrUpdateVariableValue(name, value, scope); + return this; + } + + public VariableSetEditor AddOrUpdateVariableValue(string name, string value) + { + Instance.AddOrUpdateVariableValue(name, value); + return this; + } + + public VariableSetEditor Customize(Action customize) + { + customize?.Invoke(Instance); + return this; + } + + public VariableSetEditor Save() + { + Instance = repository.Modify(Instance); + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Editors/VariableTemplateContainerEditor.cs b/source/Octopus.Client/Editors/VariableTemplateContainerEditor.cs new file mode 100644 index 000000000..64446eb2d --- /dev/null +++ b/source/Octopus.Client/Editors/VariableTemplateContainerEditor.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using Octopus.Client.Model; + +namespace Octopus.Client.Editors +{ + public class VariableTemplateContainerEditor + where TContainer : IVariableTemplateContainer, IVariableTemplateContainerEditor + { + private readonly IVariableTemplateContainerEditor container; + + public VariableTemplateContainerEditor(IVariableTemplateContainerEditor container) + { + this.container = container; + } + + public VariableTemplateContainerEditor AddOrUpdateVariableTemplate(string name, string label, IDictionary displaySettings) + { + container.AddOrUpdateVariableTemplate(name, label, displaySettings); + return this; + } + + public VariableTemplateContainerEditor AddOrUpdateVariableTemplate(string name, string label, IDictionary displaySettings, string defaultValue, string helpText) + { + container.AddOrUpdateVariableTemplate(name, label, displaySettings, defaultValue, helpText); + return this; + } + + public VariableTemplateContainerEditor AddOrUpdateSingleLineTextTemplate(string name, string label) + { + container.AddOrUpdateSingleLineTextTemplate(name, label); + return this; + } + + public VariableTemplateContainerEditor AddOrUpdateSingleLineTextTemplate(string name, string label, string defaultValue, string helpText) + { + container.AddOrUpdateSingleLineTextTemplate(name, label, defaultValue, helpText); + return this; + } + + public VariableTemplateContainerEditor AddOrUpdateMultiLineTextTemplate(string name, string label) + { + container.AddOrUpdateMultiLineTextTemplate(name, label); + return this; + } + + public VariableTemplateContainerEditor AddOrUpdateMultiLineTextTemplate(string name, string label, string defaultValue, string helpText) + { + container.AddOrUpdateMultiLineTextTemplate(name, label, defaultValue, helpText); + return this; + } + + public VariableTemplateContainerEditor AddOrUpdateSensitiveTemplate(string name, string label) + { + container.AddOrUpdateSensitiveTemplate(name, label); + return this; + } + + public VariableTemplateContainerEditor AddOrUpdateSensitiveTemplate(string name, string label, string defaultValue, string helpText) + { + container.AddOrUpdateSensitiveTemplate(name, label, defaultValue, helpText); + return this; + } + + public VariableTemplateContainerEditor AddOrUpdateSelectTemplate(string name, string label, IDictionary options) + { + container.AddOrUpdateSelectTemplate(name, label, options); + return this; + } + + public VariableTemplateContainerEditor AddOrUpdateSelectTemplate(string name, string label, IDictionary options, string defaultValue, string helpText) + { + container.AddOrUpdateSelectTemplate(name, label, options, defaultValue, helpText); + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Exceptions/OctopusDeserializationException.cs b/source/Octopus.Client/Exceptions/OctopusDeserializationException.cs new file mode 100644 index 000000000..671fc5c3f --- /dev/null +++ b/source/Octopus.Client/Exceptions/OctopusDeserializationException.cs @@ -0,0 +1,15 @@ +using System; + +namespace Octopus.Client.Exceptions +{ + public class OctopusDeserializationException : OctopusException + { + public OctopusDeserializationException(int httpStatusCode, string message) : base(httpStatusCode, message) + { + } + + public OctopusDeserializationException(int httpStatusCode, string message, Exception innerException) : base(httpStatusCode, message, innerException) + { + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Exceptions/OctopusException.cs b/source/Octopus.Client/Exceptions/OctopusException.cs new file mode 100644 index 000000000..48f8cb408 --- /dev/null +++ b/source/Octopus.Client/Exceptions/OctopusException.cs @@ -0,0 +1,51 @@ +using System; + +namespace Octopus.Client.Exceptions +{ + /// + /// Base class for all exceptions thrown by the Octopus client. + /// + public abstract class OctopusException : Exception + { + readonly int httpStatusCode; + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code. + /// The message. + protected OctopusException(int httpStatusCode, string message) + : base(message) + { + this.httpStatusCode = httpStatusCode; + } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code. + /// The message. + /// The inner exception. + protected OctopusException(int httpStatusCode, string message, Exception innerException) + : base(message, innerException) + { + this.httpStatusCode = httpStatusCode; + } + + /// + /// Gets the HTTP status code. + /// + /// + /// The HTTP status code. + /// + public int HttpStatusCode + { + get { return httpStatusCode; } + } + + /// + /// Gets additional help that the server may have provided regarding the error. + /// + public string HelpText { get; internal set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Exceptions/OctopusExceptionFactory.cs b/source/Octopus.Client/Exceptions/OctopusExceptionFactory.cs new file mode 100644 index 000000000..13244d438 --- /dev/null +++ b/source/Octopus.Client/Exceptions/OctopusExceptionFactory.cs @@ -0,0 +1,132 @@ +using System; +using System.IO; +using System.Net; +using Newtonsoft.Json; + +namespace Octopus.Client.Exceptions +{ + /// + /// Factory for mapping HTTP errors into Octopus exceptions. + /// + public static class OctopusExceptionFactory + { + /// + /// Creates the appropriate from a HTTP response. + /// + /// The web exception. + /// The response. + /// A rich exception describing the problem. + public static OctopusException CreateException(WebException webException, HttpWebResponse response) + { + var statusCode = (int)response.StatusCode; + + var body = ""; + var responseStream = response.GetResponseStream(); + if (responseStream != null) + { + using (var reader = new StreamReader(responseStream)) + { + body = reader.ReadToEnd(); + } + } + + if (statusCode == 400 || statusCode == 409) // Bad request: usually validation error + { + var errors = JsonConvert.DeserializeObject(body); + return new OctopusValidationException(statusCode, errors.ErrorMessage, errors.Errors) {HelpText = errors.HelpText}; + } + + if (statusCode == 401 || statusCode == 403) // Forbidden, usually no API key or permissions + { + var errors = JsonConvert.DeserializeObject(body); + errors = errors ?? new OctopusErrorsContract {ErrorMessage = string.Format("The server returned a status code of {0}: {1}", statusCode, body)}; + return new OctopusSecurityException(statusCode, errors.ErrorMessage) {HelpText = errors.HelpText}; + } + + if (statusCode == 404) // Not found + { + var errorMessage = GetErrorMessage(body); + return new OctopusResourceNotFoundException(errorMessage); + } + + if (statusCode == 405) // Method not allowed + { + var errorMessage = GetErrorMessage(body); + return new OctopusMethodNotAllowedFoundException(errorMessage); + } + + var fullDetails = body; + string helpText = null; + try + { + var errors = JsonConvert.DeserializeObject(body); + if (errors != null) + { + fullDetails = "Octopus Server returned an error: " + errors.ErrorMessage; + helpText = errors.HelpText; + if (!string.IsNullOrWhiteSpace(errors.FullException)) + { + fullDetails += Environment.NewLine + "Server exception: " + Environment.NewLine + errors.FullException + Environment.NewLine + "-----------------------" + Environment.NewLine; + } + } + } + // ReSharper disable once EmptyGeneralCatchClause + catch + { + } + + return new OctopusServerException(statusCode, fullDetails) {HelpText = helpText}; + } + + static string GetErrorMessage(string body) + { + string errorMessage; + try + { + var errors = JsonConvert.DeserializeObject(body); + errorMessage = errors.ErrorMessage; + } + catch + { + errorMessage = body; + } + return errorMessage; + } + + /// + /// Error contract for error responses. + /// + public class OctopusErrorsContract + { + /// + /// Gets or sets the error message. + /// + /// + /// The error message. + /// + public string ErrorMessage { get; set; } + + /// + /// Gets or sets the full exception. + /// + /// + /// The full exception if available, or null. + /// + public string FullException { get; set; } + + /// + /// Gets or sets the errors. + /// + /// + /// The errors. + /// + public string[] Errors { get; set; } + + /// + /// Gets or sets additional help regarding the error. + /// + /// The help text, or null. + public string HelpText { get; set; } + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Exceptions/OctopusMethodNotAllowedFoundException.cs b/source/Octopus.Client/Exceptions/OctopusMethodNotAllowedFoundException.cs new file mode 100644 index 000000000..93067ac6d --- /dev/null +++ b/source/Octopus.Client/Exceptions/OctopusMethodNotAllowedFoundException.cs @@ -0,0 +1,20 @@ +using System; + +namespace Octopus.Client.Exceptions +{ + /// + /// An exception thrown when the Octopus Server responds with HTTP 405, which indicates that the + /// HTTP method (GET, POST, PUT, DELETE) is not supported on the specified resource. + /// + public class OctopusMethodNotAllowedFoundException : OctopusException + { + /// + /// Initializes a new instance of the class. + /// + /// The message. + public OctopusMethodNotAllowedFoundException(string message) + : base((int)System.Net.HttpStatusCode.MethodNotAllowed, message) + { + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Exceptions/OctopusResourceNotFoundException.cs b/source/Octopus.Client/Exceptions/OctopusResourceNotFoundException.cs new file mode 100644 index 000000000..057be3ae5 --- /dev/null +++ b/source/Octopus.Client/Exceptions/OctopusResourceNotFoundException.cs @@ -0,0 +1,20 @@ +using System; + +namespace Octopus.Client.Exceptions +{ + /// + /// An exception thrown when the Octopus Server responds with HTTP 404, such as when the specified + /// resource does not exist on the server. + /// + public class OctopusResourceNotFoundException : OctopusException + { + /// + /// Initializes a new instance of the class. + /// + /// The message. + public OctopusResourceNotFoundException(string message) + : base((int)System.Net.HttpStatusCode.NotFound, message) + { + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Exceptions/OctopusSecurityException.cs b/source/Octopus.Client/Exceptions/OctopusSecurityException.cs new file mode 100644 index 000000000..6990c74be --- /dev/null +++ b/source/Octopus.Client/Exceptions/OctopusSecurityException.cs @@ -0,0 +1,21 @@ +using System; + +namespace Octopus.Client.Exceptions +{ + /// + /// An exception thrown when the Octopus Server responds with HTTP 401 or 403, indicating that the current + /// user's API key was not valid, their account is disabled, or they don't have permission to perform the + /// specified action. + /// + public class OctopusSecurityException : OctopusException + { + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code. + /// The message. + public OctopusSecurityException(int httpStatusCode, string message) : base(httpStatusCode, message) + { + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Exceptions/OctopusServerException.cs b/source/Octopus.Client/Exceptions/OctopusServerException.cs new file mode 100644 index 000000000..c6d36f77a --- /dev/null +++ b/source/Octopus.Client/Exceptions/OctopusServerException.cs @@ -0,0 +1,22 @@ +using System; + +namespace Octopus.Client.Exceptions +{ + /// + /// An exception thrown when the Octopus Server responds with HTTP 500 or any other error, indicating that there was a + /// problem processing + /// the request. + /// + public class OctopusServerException : OctopusException + { + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code. + /// The message. + public OctopusServerException(int httpStatusCode, string message) + : base(httpStatusCode, message) + { + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Exceptions/OctopusValidationException.cs b/source/Octopus.Client/Exceptions/OctopusValidationException.cs new file mode 100644 index 000000000..45f81793d --- /dev/null +++ b/source/Octopus.Client/Exceptions/OctopusValidationException.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Octopus.Client.Exceptions +{ + /// + /// An exception thrown when there was a problem with the request (usually a HTTP 400). + /// + public class OctopusValidationException : OctopusException + { + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The errors. + public OctopusValidationException(string message, ICollection errors) + :this((int)System.Net.HttpStatusCode.BadRequest, message, errors) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code. + /// The message. + /// The errors. + public OctopusValidationException(int httpStatusCode, string message, ICollection errors) + : base(httpStatusCode, message + Environment.NewLine + Environment.NewLine + string.Join(Environment.NewLine, errors.Select(e => " - " + e)) + Environment.NewLine) + { + ErrorMessage = message; + Errors = errors.ToList().AsReadOnly(); + } + + /// + /// Gets the error message that was returned by the Octopus Server. + /// + public string ErrorMessage { get; private set; } + + /// + /// Gets a list of problems with the request that was returned by the Octopus Server. + /// + public ReadOnlyCollection Errors { get; private set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Exceptions/UnsupportedApiVersionException.cs b/source/Octopus.Client/Exceptions/UnsupportedApiVersionException.cs new file mode 100644 index 000000000..bdec31b67 --- /dev/null +++ b/source/Octopus.Client/Exceptions/UnsupportedApiVersionException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Octopus.Client.Exceptions +{ + /// + /// An exception thrown when the Octopus Server supports a version of the API that is incompatible with this class + /// library. + /// + public class UnsupportedApiVersionException : OctopusException + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public UnsupportedApiVersionException(string message) : base(200, message) + { + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Extensions/LazyExtensions.cs b/source/Octopus.Client/Extensions/LazyExtensions.cs new file mode 100644 index 000000000..32ecfe423 --- /dev/null +++ b/source/Octopus.Client/Extensions/LazyExtensions.cs @@ -0,0 +1,24 @@ +using System; + +// ReSharper disable CheckNamespace + +namespace Octopus.Client.Extensions +{ + /// + /// Extension methods for instances. + /// + static class LazyExtensions +// ReSharper restore CheckNamespace + { + /// + /// Forces the Lazy value to be loaded. + /// + /// The item type. + /// The lazy instance. + /// The value of the lazy instance. + public static T LoadValue(this Lazy lazy) + { + return lazy.Value; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Extensions/TaskStateExtensions.cs b/source/Octopus.Client/Extensions/TaskStateExtensions.cs new file mode 100644 index 000000000..34af489a5 --- /dev/null +++ b/source/Octopus.Client/Extensions/TaskStateExtensions.cs @@ -0,0 +1,13 @@ +using System; +using Octopus.Client.Model; + +namespace Octopus.Client.Extensions +{ + public static class TaskStateExtensions + { + public static bool IsCompleted(this TaskState state) + { + return !(state == TaskState.Executing || state == TaskState.Queued || state == TaskState.Cancelling); + } + } +} diff --git a/source/Octopus.Client/Extensions/TypeExtensions.cs b/source/Octopus.Client/Extensions/TypeExtensions.cs new file mode 100644 index 000000000..9ee045594 --- /dev/null +++ b/source/Octopus.Client/Extensions/TypeExtensions.cs @@ -0,0 +1,9 @@ +using System; + +namespace Octopus.Client.Extensions +{ + public static class TypeExtensions + { + public static object GetDefault(this Type t) => t.IsValueType ? Activator.CreateInstance(t) : null; + } +} \ No newline at end of file diff --git a/source/Octopus.Client/FodyWeavers.xml b/source/Octopus.Client/FodyWeavers.xml new file mode 100644 index 000000000..915502590 --- /dev/null +++ b/source/Octopus.Client/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/source/Octopus.Client/IHttpOctopusClient.cs b/source/Octopus.Client/IHttpOctopusClient.cs new file mode 100644 index 000000000..21451912b --- /dev/null +++ b/source/Octopus.Client/IHttpOctopusClient.cs @@ -0,0 +1,16 @@ +using System; +using System.Net; + +namespace Octopus.Client +{ + /// + /// Implemented by an that uses HTTP to communicate. + /// + public interface IHttpOctopusClient : IOctopusClient + { + /// + /// Occurs when a request is about to be sent. + /// + event Action BeforeSendingHttpRequest; + } +} \ No newline at end of file diff --git a/source/Octopus.Client/ILinkResolver.cs b/source/Octopus.Client/ILinkResolver.cs new file mode 100644 index 000000000..c12a0b415 --- /dev/null +++ b/source/Octopus.Client/ILinkResolver.cs @@ -0,0 +1,21 @@ +using System; + +namespace Octopus.Client +{ + /// + /// In the same way web browsers can follow links like "/foo" by knowing the URI of the current page, this class allows + /// application + /// links to be resolved into fully-qualified URI's. + /// + public interface ILinkResolver + { + /// + /// Resolves the specified link into a fully qualified URI. + /// + /// The application relative link (should begin with a /). + /// + /// The fully resolved URI. + /// + Uri Resolve(string link); + } +} \ No newline at end of file diff --git a/source/Octopus.Client/IOctopusClient.cs b/source/Octopus.Client/IOctopusClient.cs new file mode 100644 index 000000000..2ad4a6477 --- /dev/null +++ b/source/Octopus.Client/IOctopusClient.cs @@ -0,0 +1,285 @@ +using System; +using System.IO; +using Octopus.Client.Exceptions; +using Octopus.Client.Model; + +namespace Octopus.Client +{ + /// + /// Contract for a client to the Octopus Deploy HTTP API. + /// + public interface IOctopusClient : IDisposable + { + /// + /// Gets a document that identifies the Octopus server (from /api) and provides links to the resources available on the + /// server. Instead of hardcoding paths, + /// clients should use these link properties to traverse the resources on the server. This document is lazily loaded so + /// that it is only requested once for + /// the current . + /// + /// + /// HTTP 401 or 403: Thrown when the current user's API key was not valid, their + /// account is disabled, or they don't have permission to perform the specified action. + /// + /// + /// If any other error is successfully returned from the server (e.g., a 500 + /// server error). + /// + /// HTTP 400: If there was a problem with the request provided by the user. + /// HTTP 404: If the specified resource does not exist on the server. + RootResource RootDocument { get; } + + /// + /// Occurs when a request is about to be sent. + /// + event Action SendingOctopusRequest; + + /// + /// Occurs when a response is received from the Octopus server. + /// + event Action ReceivedOctopusResponse; + + /// + /// Fetches a collection of resources from the server using the HTTP GET verb. The collection itself will usually be + /// limited in size (pagination) and links to the next page of data is available in the + /// property. + /// + /// + /// HTTP 401 or 403: Thrown when the current user's API key was not valid, their + /// account is disabled, or they don't have permission to perform the specified action. + /// + /// + /// If any other error is successfully returned from the server (e.g., a 500 + /// server error). + /// + /// HTTP 400: If there was a problem with the request provided by the user. + /// HTTP 404: If the specified resource does not exist on the server. + /// The path from which to fetch the resources. + /// If the path is a URI template, parameters to use for substitution. + /// The collection of resources from the server. + ResourceCollection List(string path, object pathParameters = null); + + /// + /// Fetches a collection of resources from the server one page at a time using the HTTP GET verb. + /// + /// + /// HTTP 401 or 403: Thrown when the current user's API key was not valid, their + /// account is disabled, or they don't have permission to perform the specified action. + /// + /// + /// If any other error is successfully returned from the server (e.g., a 500 + /// server error). + /// + /// HTTP 400: If there was a problem with the request provided by the user. + /// HTTP 404: If the specified resource does not exist on the server. + /// The path from which to fetch the resources. + /// + /// A callback invoked for each page of data found. If the callback returns true, the next + /// page will also be requested. + /// + /// The collection of resources from the server. + void Paginate(string path, Func, bool> getNextPage); + + /// + /// Fetches a collection of resources from the server one page at a time using the HTTP GET verb. + /// + /// + /// HTTP 401 or 403: Thrown when the current user's API key was not valid, their + /// account is disabled, or they don't have permission to perform the specified action. + /// + /// + /// If any other error is successfully returned from the server (e.g., a 500 + /// server error). + /// + /// HTTP 400: If there was a problem with the request provided by the user. + /// HTTP 404: If the specified resource does not exist on the server. + /// The path from which to fetch the resources. + /// If the path is a URI template, parameters to use for substitution. + /// + /// A callback invoked for each page of data found. If the callback returns true, the next + /// page will also be requested. + /// + /// The collection of resources from the server. + void Paginate(string path, object pathParameters, Func, bool> getNextPage); + + /// + /// Fetches a single resource from the server using the HTTP GET verb. + /// + /// + /// HTTP 401 or 403: Thrown when the current user's API key was not valid, their + /// account is disabled, or they don't have permission to perform the specified action. + /// + /// + /// If any other error is successfully returned from the server (e.g., a 500 + /// server error). + /// + /// HTTP 400: If there was a problem with the request provided by the user. + /// HTTP 404: If the specified resource does not exist on the server. + /// The path from which to fetch the resource. + /// If the path is a URI template, parameters to use for substitution. + /// The resource from the server. + TResource Get(string path, object pathParameters = null); + + /// + /// Creates a resource at the given URI on the server using the POST verb, then performs a fresh GET request to fetch + /// the created item. + /// + /// + /// HTTP 401 or 403: Thrown when the current user's API key was not valid, their + /// account is disabled, or they don't have permission to perform the specified action. + /// + /// + /// If any other error is successfully returned from the server (e.g., a 500 + /// server error). + /// + /// HTTP 400: If there was a problem with the request provided by the user. + /// HTTP 404: If the specified resource does not exist on the server. + /// The path to the container resource. + /// The resource to create. + /// If the path is a URI template, parameters to use for substitution. + /// The latest copy of the resource from the server. + TResource Create(string path, TResource resource, object pathParameters = null); + + /// + /// Sends a command to a resource at the given URI on the server using the POST verb. + /// + /// + /// HTTP 401 or 403: Thrown when the current user's API key was not valid, their + /// account is disabled, or they don't have permission to perform the specified action. + /// + /// + /// If any other error is successfully returned from the server (e.g., a 500 + /// server error). + /// + /// HTTP 400: If there was a problem with the request provided by the user. + /// HTTP 404: If the specified resource does not exist on the server. + /// The path to the container resource. + /// The resource to create. + /// If the path is a URI template, parameters to use for substitution. + void Post(string path, TResource resource, object pathParameters = null); + + /// + /// Sends a command to a resource at the given URI on the server using the POST verb, and retrieve the response. + /// + /// + /// HTTP 401 or 403: Thrown when the current user's API key was not valid, their + /// account is disabled, or they don't have permission to perform the specified action. + /// + /// + /// If any other error is successfully returned from the server (e.g., a 500 + /// server error). + /// + /// HTTP 400: If there was a problem with the request provided by the user. + /// HTTP 404: If the specified resource does not exist on the server. + /// The path to the container resource. + /// The resource to create. + /// If the path is a URI template, parameters to use for substitution. + TResponse Post(string path, TResource resource, object pathParameters = null); + + /// + /// Sends a command to a resource at the given URI on the server using the POST verb. + /// + /// + /// HTTP 401 or 403: Thrown when the current user's API key was not valid, their + /// account is disabled, or they don't have permission to perform the specified action. + /// + /// + /// If any other error is successfully returned from the server (e.g., a 500 + /// server error). + /// + /// HTTP 400: If there was a problem with the request provided by the user. + /// HTTP 404: If the specified resource does not exist on the server. + /// The path to the container resource. + void Post(string path); + + /// + /// Sends a command to a resource at the given URI on the server using the PUT verb. + /// + /// + /// HTTP 401 or 403: Thrown when the current user's API key was not valid, their + /// account is disabled, or they don't have permission to perform the specified action. + /// + /// + /// If any other error is successfully returned from the server (e.g., a 500 + /// server error). + /// + /// HTTP 400: If there was a problem with the request provided by the user. + /// HTTP 404: If the specified resource does not exist on the server. + /// The path to the container resource. + /// The resource to create. + void Put(string path, TResource resource); + + /// + /// Updates the resource at the given URI on the server using the PUT verb, then performs a fresh GET request to reload + /// the data. + /// + /// + /// HTTP 401 or 403: Thrown when the current user's API key was not valid, their + /// account is disabled, or they don't have permission to perform the specified action. + /// + /// + /// If any other error is successfully returned from the server (e.g., a 500 + /// server error). + /// + /// HTTP 400: If there was a problem with the request provided by the user. + /// HTTP 404: If the specified resource does not exist on the server. + /// The path to the resource to update. + /// The resource to update. + /// If the path is a URI template, parameters to use for substitution. + /// The latest copy of the resource from the server. + TResource Update(string path, TResource resource, object pathParameters = null); + + /// + /// Deletes the resource at the given URI from the server using a the DELETE verb. Deletes in Octopus happen + /// asynchronously via a background task + /// that is executed by the Octopus server. The payload returned by delete will be the task that was created on the + /// server. + /// + /// + /// HTTP 401 or 403: Thrown when the current user's API key was not valid, their + /// account is disabled, or they don't have permission to perform the specified action. + /// + /// + /// If any other error is successfully returned from the server (e.g., a 500 + /// server error). + /// + /// HTTP 400: If there was a problem with the request provided by the user. + /// HTTP 404: If the specified resource does not exist on the server. + /// The path to the resource to delete. + /// If the path is a URI template, parameters to use for substitution. + /// A task resource that provides details about the background task that deletes the specified resource. + void Delete(string path, object pathParameters = null); + + /// + /// Fetches raw content from the resource at the specified path, using the GET verb. + /// + /// + /// HTTP 401 or 403: Thrown when the current user's API key was not valid, their + /// account is disabled, or they don't have permission to perform the specified action. + /// + /// + /// If any other error is successfully returned from the server (e.g., a 500 + /// server error). + /// + /// HTTP 400: If there was a problem with the request provided by the user. + /// HTTP 404: If the specified resource does not exist on the server. + /// The path to the resource to fetch. + /// A stream containing the content of the resource. + Stream GetContent(string path); + + /// + /// Creates or updates the raw content of the resource at the specified path, using the PUT verb. + /// + /// The path to the resource to create or update. + /// A stream containing content of the resource. + void PutContent(string path, Stream contentStream); + + Uri QualifyUri(string path, object parameters = null); + + /// + /// Requests a fresh root document from the Octopus Server which can be useful if the API surface has changed. This can occur when enabling/disabling features, or changing license. + /// + /// A fresh copy of the root document. + RootResource RefreshRootDocument(); + } +} \ No newline at end of file diff --git a/source/Octopus.Client/IOctopusClientFactory.cs b/source/Octopus.Client/IOctopusClientFactory.cs new file mode 100644 index 000000000..447e3e029 --- /dev/null +++ b/source/Octopus.Client/IOctopusClientFactory.cs @@ -0,0 +1,17 @@ +using System; + +namespace Octopus.Client +{ + /// + /// Creates instances of . + /// + public interface IOctopusClientFactory + { + /// + /// Creates an appropriate for the provided . + /// + /// The endpoint to create a client for. + /// The instance. + IOctopusClient CreateClient(OctopusServerEndpoint serverEndpoint); + } +} \ No newline at end of file diff --git a/source/Octopus.Client/IOctopusRepository.cs b/source/Octopus.Client/IOctopusRepository.cs new file mode 100644 index 000000000..674f3c0b0 --- /dev/null +++ b/source/Octopus.Client/IOctopusRepository.cs @@ -0,0 +1,57 @@ +using System; +using Octopus.Client.Repositories; + +namespace Octopus.Client +{ + /// + /// A simplified interface to commonly-used parts of the API. + /// Functionality not exposed by this interface can be accessed + /// using . + /// + public interface IOctopusRepository + { + /// + /// The client over which the repository is run. + /// + IOctopusClient Client { get; } + + IArtifactRepository Artifacts { get; } + ICertificateRepository Certificates { get; } + IBackupRepository Backups { get; } + IBuiltInPackageRepositoryRepository BuiltInPackageRepository { get; } + IDashboardConfigurationRepository DashboardConfigurations { get; } + IDashboardRepository Dashboards { get; } + IDeploymentProcessRepository DeploymentProcesses { get; } + IDeploymentRepository Deployments { get; } + IEnvironmentRepository Environments { get; } + IEventRepository Events { get; } + IFeaturesConfigurationRepository FeaturesConfiguration { get; } + IFeedRepository Feeds { get; } + IInterruptionRepository Interruptions { get; } + ILibraryVariableSetRepository LibraryVariableSets { get; } + ILifecyclesRepository Lifecycles { get; } + IMachineRepository Machines { get; } + IMachineRoleRepository MachineRoles { get; } + IMachinePolicyRepository MachinePolicies { get; } + IProjectGroupRepository ProjectGroups { get; } + IProjectRepository Projects { get; } + IReleaseRepository Releases { get; } + IProxyRepository Proxies { get; } + IServerStatusRepository ServerStatus { get; } + ISchedulerRepository Schedulers { get; } + ITaskRepository Tasks { get; } + ITeamsRepository Teams { get; } + ITagSetRepository TagSets { get; } + ITenantRepository Tenants { get; } + IUserRepository Users { get; } + IUserRolesRepository UserRoles { get; } + IVariableSetRepository VariableSets { get; } + IChannelRepository Channels { get; } + IProjectTriggerRepository ProjectTriggers { get; } + IAccountRepository Accounts { get; } + IRetentionPolicyRepository RetentionPolicies { get; } + IDefectsRepository Defects { get; } + IOctopusServerNodeRepository OctopusServerNodes { get; } + + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Accounts/AccountResource.cs b/source/Octopus.Client/Model/Accounts/AccountResource.cs new file mode 100644 index 000000000..88fa4c17f --- /dev/null +++ b/source/Octopus.Client/Model/Accounts/AccountResource.cs @@ -0,0 +1,33 @@ +using System; + +namespace Octopus.Client.Model.Accounts +{ + public abstract class AccountResource : Resource, INamedResource + { + protected AccountResource() + { + EnvironmentIds = new ReferenceCollection(); + TenantTags = new ReferenceCollection(); + TenantIds = new ReferenceCollection(); + } + + [Writeable] + [Trim] + public string Name { get; set; } + + [Writeable] + public string Description { get; set; } + + [Writeable] + public ReferenceCollection EnvironmentIds { get; set; } + + [Writeable] + public ReferenceCollection TenantIds { get; set; } + + [Writeable] + public ReferenceCollection TenantTags { get; set; } + + [WriteableOnCreate] + public abstract AccountType AccountType { get; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Accounts/AccountType.cs b/source/Octopus.Client/Model/Accounts/AccountType.cs new file mode 100644 index 000000000..7da892fd8 --- /dev/null +++ b/source/Octopus.Client/Model/Accounts/AccountType.cs @@ -0,0 +1,13 @@ +using System; + +namespace Octopus.Client.Model.Accounts +{ + public enum AccountType + { + None, + UsernamePassword, + SshKeyPair, + AzureSubscription, + AzureServicePrincipal + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Accounts/AzureServicePrincipalAccountResource.cs b/source/Octopus.Client/Model/Accounts/AzureServicePrincipalAccountResource.cs new file mode 100644 index 000000000..4acb1a853 --- /dev/null +++ b/source/Octopus.Client/Model/Accounts/AzureServicePrincipalAccountResource.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; + +namespace Octopus.Client.Model.Accounts +{ + public class AzureServicePrincipalAccountResource : AccountResource + { + public override AccountType AccountType { get {return AccountType.AzureServicePrincipal;} } + + [Trim] + [Writeable] + [Required(ErrorMessage = "Please provide an Azure subscription ID.")] + public string SubscriptionNumber { get; set; } + + [Trim] + [Writeable] + [NotDocumentReference] + public string ClientId { get; set; } + + [Trim] + [Writeable] + [NotDocumentReference] + public string TenantId { get; set; } + + [Trim] + [Writeable] + public SensitiveValue Password { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Accounts/AzureSubscriptionAccountResource.cs b/source/Octopus.Client/Model/Accounts/AzureSubscriptionAccountResource.cs new file mode 100644 index 000000000..7285de28c --- /dev/null +++ b/source/Octopus.Client/Model/Accounts/AzureSubscriptionAccountResource.cs @@ -0,0 +1,35 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Octopus.Client.Model.Accounts +{ + public class AzureSubscriptionAccountResource : AccountResource + { + public AzureSubscriptionAccountResource() + { + CertificateBytes = new SensitiveValue(); + } + + public override AccountType AccountType + { + get { return AccountType.AzureSubscription; } + } + + [Trim] + [Writeable] + [Required(ErrorMessage = "Please provide an Azure subscription ID.")] + public string SubscriptionNumber { get; set; } + + [Trim] + [Writeable] + public SensitiveValue CertificateBytes { get; set; } + + [Trim] + [Writeable] + public string CertificateThumbprint { get; set; } + + [Trim] + [Writeable] + public string ManagementEndpoint { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Accounts/SshKeyPairAccountResource.cs b/source/Octopus.Client/Model/Accounts/SshKeyPairAccountResource.cs new file mode 100644 index 000000000..a8395c286 --- /dev/null +++ b/source/Octopus.Client/Model/Accounts/SshKeyPairAccountResource.cs @@ -0,0 +1,28 @@ +using System; + +namespace Octopus.Client.Model.Accounts +{ + public class SshKeyPairAccountResource : AccountResource + { + public SshKeyPairAccountResource() + { + PrivateKeyFile = new SensitiveValue(); + PrivateKeyPassphrase = new SensitiveValue(); + } + + public override AccountType AccountType + { + get { return AccountType.SshKeyPair; } + } + + [Trim] + [Writeable] + public string Username { get; set; } + + [Writeable] + public SensitiveValue PrivateKeyFile { get; set; } + + [Writeable] + public SensitiveValue PrivateKeyPassphrase { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Accounts/SupportedAccountTypesAttribute.cs b/source/Octopus.Client/Model/Accounts/SupportedAccountTypesAttribute.cs new file mode 100644 index 000000000..909fee5b2 --- /dev/null +++ b/source/Octopus.Client/Model/Accounts/SupportedAccountTypesAttribute.cs @@ -0,0 +1,20 @@ +using System; + +namespace Octopus.Client.Model.Accounts +{ + [AttributeUsage(AttributeTargets.Field)] + public class SupportedAccountTypesAttribute : Attribute + { + readonly AccountType[] accountTypes; + + public SupportedAccountTypesAttribute(params AccountType[] accountTypes) + { + this.accountTypes = accountTypes; + } + + public AccountType[] AccountTypes + { + get { return accountTypes; } + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Accounts/UsernamePasswordAccountResource.cs b/source/Octopus.Client/Model/Accounts/UsernamePasswordAccountResource.cs new file mode 100644 index 000000000..d8b8b170b --- /dev/null +++ b/source/Octopus.Client/Model/Accounts/UsernamePasswordAccountResource.cs @@ -0,0 +1,26 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Octopus.Client.Model.Accounts +{ + public class UsernamePasswordAccountResource : AccountResource + { + public UsernamePasswordAccountResource() + { + Password = new SensitiveValue(); + } + + public override AccountType AccountType + { + get { return AccountType.UsernamePassword; } + } + + [Trim] + [Writeable] + [Required(ErrorMessage = "Please provide a username.")] + public string Username { get; set; } + + [Writeable] + public SensitiveValue Password { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/ActionTemplateParameterResource.cs b/source/Octopus.Client/Model/ActionTemplateParameterResource.cs new file mode 100644 index 000000000..2a8143aba --- /dev/null +++ b/source/Octopus.Client/Model/ActionTemplateParameterResource.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace Octopus.Client.Model +{ + public class ActionTemplateParameterResource + { + public ActionTemplateParameterResource() + { + DisplaySettings = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + public string Id { get; set; } + + [Writeable] + [Trim] + public string Name { get; set; } + + [Writeable] + [Trim] + public string Label { get; set; } + + [Writeable] + public string HelpText { get; set; } + + [Writeable] + public string DefaultValue { get; set; } + + [Writeable] + public IDictionary DisplaySettings { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/ActionTemplateResource.cs b/source/Octopus.Client/Model/ActionTemplateResource.cs new file mode 100644 index 000000000..60b7d4bf3 --- /dev/null +++ b/source/Octopus.Client/Model/ActionTemplateResource.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + +namespace Octopus.Client.Model +{ + public class ActionTemplateResource : Resource, INamedResource + { + readonly IDictionary properties = new Dictionary(StringComparer.OrdinalIgnoreCase); + readonly IList parameters = new List(); + + [Required(ErrorMessage = "Please provide a name for the template.")] + [Writeable] + public string Name { get; set; } + + [Writeable] + public string Description { get; set; } + + [Required(ErrorMessage = "Please provide an action type.")] + [WriteableOnCreate] + public string ActionType { get; set; } + + public int Version { get; set; } + + [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Reuse)] + public IDictionary Properties + { + get { return properties; } + } + + [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Reuse)] + public IList Parameters + { + get { return parameters; } + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/ActionTemplateUsageResource.cs b/source/Octopus.Client/Model/ActionTemplateUsageResource.cs new file mode 100644 index 000000000..5589afc8b --- /dev/null +++ b/source/Octopus.Client/Model/ActionTemplateUsageResource.cs @@ -0,0 +1,18 @@ +using System; + +namespace Octopus.Client.Model +{ + public class ActionTemplateUsageResource : Resource + { + public string ActionTemplateId { get; set; } + public string DeploymentProcessId { get; set; } + public string ActionId { get; set; } + public string ActionName { get; set; } + public string StepId { get; set; } + public string StepName { get; set; } + public string ProjectId { get; set; } + public string ProjectName { get; set; } + public string ProjectSlug { get; set; } + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/ActivityElement.cs b/source/Octopus.Client/Model/ActivityElement.cs new file mode 100644 index 000000000..845cfa8ab --- /dev/null +++ b/source/Octopus.Client/Model/ActivityElement.cs @@ -0,0 +1,16 @@ +using System; + +namespace Octopus.Client.Model +{ + public class ActivityElement + { + public string Id { get; set; } + public string Name { get; set; } + public ActivityStatus Status { get; set; } + public ActivityElement[] Children { get; set; } + public bool ShowAtSummaryLevel { get; set; } + public ActivityLogElement[] LogElements { get; set; } + public int ProgressPercentage { get; set; } + public string ProgressMessage { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/ActivityLogElement.cs b/source/Octopus.Client/Model/ActivityLogElement.cs new file mode 100644 index 000000000..76756f407 --- /dev/null +++ b/source/Octopus.Client/Model/ActivityLogElement.cs @@ -0,0 +1,15 @@ +using System; +using Newtonsoft.Json; + +namespace Octopus.Client.Model +{ + public class ActivityLogElement + { + public string Category { get; set; } + public DateTimeOffset? OccurredAt { get; set; } + public string MessageText { get; set; } + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string Detail { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/ActivityStatus.cs b/source/Octopus.Client/Model/ActivityStatus.cs new file mode 100644 index 000000000..cc7b90d4d --- /dev/null +++ b/source/Octopus.Client/Model/ActivityStatus.cs @@ -0,0 +1,15 @@ +using System; + +namespace Octopus.Client.Model +{ + public enum ActivityStatus + { + Pending, + Running, + Success, + Failed, + Skipped, + SuccessWithWarning, + Canceled + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/AlertResource.cs b/source/Octopus.Client/Model/AlertResource.cs new file mode 100644 index 000000000..1897cf20f --- /dev/null +++ b/source/Octopus.Client/Model/AlertResource.cs @@ -0,0 +1,10 @@ +using System; + +namespace Octopus.Client.Model +{ + public class AlertResource : Resource + { + public AlertSeverity Severity { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/AlertSeverity.cs b/source/Octopus.Client/Model/AlertSeverity.cs new file mode 100644 index 000000000..422ec6744 --- /dev/null +++ b/source/Octopus.Client/Model/AlertSeverity.cs @@ -0,0 +1,11 @@ +using System; + +namespace Octopus.Client.Model +{ + public enum AlertSeverity + { + Info, + Warning, + Danger + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/AllowCsvAttribute.cs b/source/Octopus.Client/Model/AllowCsvAttribute.cs new file mode 100644 index 000000000..1560b841f --- /dev/null +++ b/source/Octopus.Client/Model/AllowCsvAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Octopus.Client.Model +{ + [AttributeUsage(AttributeTargets.Property)] + public class AllowCsvAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/ApiConstants.cs b/source/Octopus.Client/Model/ApiConstants.cs new file mode 100644 index 000000000..9a5ad3c53 --- /dev/null +++ b/source/Octopus.Client/Model/ApiConstants.cs @@ -0,0 +1,12 @@ +using System; + +namespace Octopus.Client.Model +{ + public class ApiConstants + { + public const int DefaultClientRequestTimeout = 1000*60*10; + public const string SupportedApiSchemaVersionMin = "3.0.0"; + public const string SupportedApiSchemaVersionMax = "3.0.99"; + public const string ApiKeyHttpHeaderName = "X-Octopus-ApiKey"; + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/ApiKeyResource.cs b/source/Octopus.Client/Model/ApiKeyResource.cs new file mode 100644 index 000000000..3befac5c1 --- /dev/null +++ b/source/Octopus.Client/Model/ApiKeyResource.cs @@ -0,0 +1,14 @@ +using System; + +namespace Octopus.Client.Model +{ + public class ApiKeyResource : Resource + { + [WriteableOnCreate] + public string Purpose { get; set; } + + public string UserId { get; set; } + public string ApiKey { get; set; } + public DateTimeOffset Created { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/ApiPropertyAttribute.cs b/source/Octopus.Client/Model/ApiPropertyAttribute.cs new file mode 100644 index 000000000..13276444c --- /dev/null +++ b/source/Octopus.Client/Model/ApiPropertyAttribute.cs @@ -0,0 +1,8 @@ +using System; + +namespace Octopus.Client.Model +{ + public abstract class ApiPropertyAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/ArtifactResource.cs b/source/Octopus.Client/Model/ArtifactResource.cs new file mode 100644 index 000000000..15411d889 --- /dev/null +++ b/source/Octopus.Client/Model/ArtifactResource.cs @@ -0,0 +1,44 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + +namespace Octopus.Client.Model +{ + /// + /// Artifacts are files like documents and test results that may be stored + /// alongside a release. + /// + public class ArtifactResource : Resource + { + /// + /// Gets or sets the filename of the Artifact to create. An example might be + /// "Performance Test Results.csv". + /// + /// The filename should not include path information. + [WriteableOnCreate] + [Required(ErrorMessage = "Please provide a filename (without path information) for the artifact.")] + [JsonProperty(Order = 2)] + public string Filename { get; set; } + + /// + /// Gets or sets a short summary of the source of this attachment. This will typically be the name of a step/machine, + /// or + /// "Uploaded by [username]" if the attachment was uploaded by a person. + /// + [JsonProperty(Order = 3)] + public string Source { get; set; } + + /// + /// Gets or sets the documents with which this artifact is associated. + /// + [WriteableOnCreate] + [JsonProperty(Order = 4)] + public ReferenceCollection RelatedDocumentIds { get; set; } + + /// + /// Gets or sets the time at which the artifact was created. + /// + [JsonProperty(Order = 5)] + public DateTimeOffset Created { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/AuthenticationMode.cs b/source/Octopus.Client/Model/AuthenticationMode.cs new file mode 100644 index 000000000..8ab046396 --- /dev/null +++ b/source/Octopus.Client/Model/AuthenticationMode.cs @@ -0,0 +1,10 @@ +using System; + +namespace Octopus.Client.Model +{ + public enum AuthenticationMode + { + UsernamePassword, + Domain + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/AutoDeployReleaseOverrideResource.cs b/source/Octopus.Client/Model/AutoDeployReleaseOverrideResource.cs new file mode 100644 index 000000000..52ec8448a --- /dev/null +++ b/source/Octopus.Client/Model/AutoDeployReleaseOverrideResource.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Octopus.Client.Model +{ + public class AutoDeployReleaseOverrideResource + { + public string EnvironmentId { get; } + public string TenantId { get; } + public string ReleaseId { get; } + + public AutoDeployReleaseOverrideResource(string environmentId, string releaseId) + : this(environmentId, null, releaseId) + { + } + + [JsonConstructor] + public AutoDeployReleaseOverrideResource(string environmentId, string tenantId, string releaseId) + { + EnvironmentId = environmentId; + TenantId = tenantId; + ReleaseId = releaseId; + } + + sealed class EnvironmentIdTenantIdEqualityComparer : IEqualityComparer + { + public bool Equals(AutoDeployReleaseOverrideResource x, AutoDeployReleaseOverrideResource y) + { + if (ReferenceEquals(x, y)) return true; + if (ReferenceEquals(x, null)) return false; + if (ReferenceEquals(y, null)) return false; + if (x.GetType() != y.GetType()) return false; + return string.Equals(x.EnvironmentId, y.EnvironmentId) && string.Equals(x.TenantId, y.TenantId); + } + + public int GetHashCode(AutoDeployReleaseOverrideResource obj) + { + unchecked + { + return ((obj.EnvironmentId?.GetHashCode() ?? 0)*397) ^ (obj.TenantId?.GetHashCode() ?? 0); + } + } + } + + public static IEqualityComparer EnvironmentIdTenantIdComparer { get; } = new EnvironmentIdTenantIdEqualityComparer(); + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/AzureRootResource.cs b/source/Octopus.Client/Model/AzureRootResource.cs new file mode 100644 index 000000000..00853255d --- /dev/null +++ b/source/Octopus.Client/Model/AzureRootResource.cs @@ -0,0 +1,7 @@ +namespace Octopus.Client.Model +{ + public class AzureRootResource : Resource + { + + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/BackupConfigurationResource.cs b/source/Octopus.Client/Model/BackupConfigurationResource.cs new file mode 100644 index 000000000..a742bca94 --- /dev/null +++ b/source/Octopus.Client/Model/BackupConfigurationResource.cs @@ -0,0 +1,27 @@ +using System; + +namespace Octopus.Client.Model +{ + public class BackupConfigurationResource : Resource + { + [Writeable] + public string BackupToDirectory { get; set; } + + [Writeable] + public bool IsMasterKeyBackedUp { get; set; } + + [Writeable] + public bool BackupAutomatically { get; set; } + + [Writeable] + public TimeSpan? BackupEvery { get; set; } + + [Writeable] + public TimeSpan? StartingFrom { get; set; } + + [Writeable] + public RetentionPeriod Retention { get; set; } + + public DateTimeOffset? NextDue { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/BuiltInFeedStatsResource.cs b/source/Octopus.Client/Model/BuiltInFeedStatsResource.cs new file mode 100644 index 000000000..60466226d --- /dev/null +++ b/source/Octopus.Client/Model/BuiltInFeedStatsResource.cs @@ -0,0 +1,11 @@ +using System; + +namespace Octopus.Client.Model +{ + public class BuiltInFeedStatsResource : Resource + { + public int TotalPackages { get; set; } + public string SynchronizationStatus { get; set; } + public string IndexingStatus { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/BuiltInRepositoryConfigurationResource.cs b/source/Octopus.Client/Model/BuiltInRepositoryConfigurationResource.cs new file mode 100644 index 000000000..e2493cb54 --- /dev/null +++ b/source/Octopus.Client/Model/BuiltInRepositoryConfigurationResource.cs @@ -0,0 +1,10 @@ +using System; + +namespace Octopus.Client.Model +{ + public class BuiltInRepositoryConfigurationResource : Resource + { + [Writeable] + public int? DeleteUnreleasedPackagesAfterDays { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/BuiltInTasks.cs b/source/Octopus.Client/Model/BuiltInTasks.cs new file mode 100644 index 000000000..7a0849486 --- /dev/null +++ b/source/Octopus.Client/Model/BuiltInTasks.cs @@ -0,0 +1,126 @@ +using System; + +namespace Octopus.Client.Model +{ + public static class BuiltInTasks + { + public static string[] TasksThatCanBeQueuedByUsers() + { + // Everything except "Deploy" and "Delete" + return new[] {Backup.Name, Health.Name, Retention.Name, Upgrade.Name, TestEmail.Name, AdHocScript.Name, UpdateCalamari.Name, TestAzureAccount.Name, SystemIntegrityCheck.Name}; + } + + public static class AutoDeploy + { + public const string Name = "AutoDeploy"; + + public static class Arguments + { + public static string MachineIds = "MachineIds"; + } + } + + public static class Backup + { + public static string Name = "Backup"; + } + + public static class Delete + { + public const string Name = "Delete"; + + public static class Arguments + { + public static string DocumentId = "DocumentId"; + } + } + + public static class Health + { + public const string Name = "Health"; + + public static class Arguments + { + public static string EnvironmentId = "EnvironmentId"; + public static string MachineIds = "MachineIds"; + public static string Timeout = "Timeout"; + public static string MachineTimeout = "MachineTimeout"; + } + } + + public static class AdHocScript + { + public const string Name = "AdHocScript"; + + public static class Arguments + { + public const string EnvironmentIds = "EnvironmentIds"; // and + public const string TargetRoles = "TargetRoles"; // or + public const string MachineIds = "MachineIds"; + public const string ScriptBody = "ScriptBody"; + public const string Syntax = "Syntax"; + } + } + + public static class Retention + { + public const string Name = "Retention"; + } + + public static class Upgrade + { + public const string Name = "Upgrade"; + + public static class Arguments + { + public static string EnvironmentId = "EnvironmentId"; + public static string MachineIds = "MachineIds"; + } + + } + public static class UpdateCalamari + { + public const string Name = "UpdateCalamari"; + + public static class Arguments + { + public static string MachineIds = "MachineIds"; + } + } + + public static class Deploy + { + public const string Name = "Deploy"; + + public static class Arguments + { + public static string DeploymentId = "DeploymentId"; + } + } + + public static class TestEmail + { + public const string Name = "TestEmail"; + + public static class Arguments + { + public static string EmailAddress = "EmailAddress"; + } + } + + public static class TestAzureAccount + { + public const string Name = "TestAzureAccount"; + + public static class Arguments + { + public static string AccountId = "AccountId"; + } + } + + public static class SystemIntegrityCheck + { + public const string Name = "SystemIntegrityCheck"; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/CertificateResource.cs b/source/Octopus.Client/Model/CertificateResource.cs new file mode 100644 index 000000000..76aceae6c --- /dev/null +++ b/source/Octopus.Client/Model/CertificateResource.cs @@ -0,0 +1,10 @@ +using System; + +namespace Octopus.Client.Model +{ + public class CertificateResource : Resource, INamedResource + { + public string Name { get; set; } + public string Thumbprint { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/ChannelResource.cs b/source/Octopus.Client/Model/ChannelResource.cs new file mode 100644 index 000000000..b196fbf3c --- /dev/null +++ b/source/Octopus.Client/Model/ChannelResource.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Octopus.Client.Model +{ + public class ChannelResource : Resource, INamedResource + { + public ChannelResource() + { + TenantTags = new ReferenceCollection(); + Rules = new List(); + } + + [Writeable] + public string Name { get; set; } + + [Writeable] + public string Description { get; set; } + + [WriteableOnCreate] + public string ProjectId { get; set; } + + [Writeable] + public string LifecycleId { get; set; } + + [Writeable] + public bool IsDefault { get; set; } + + [Writeable] + public List Rules { get; set; } + + [Writeable] + public ReferenceCollection TenantTags { get; set; } + + public ChannelResource SetAsDefaultChannel() + { + IsDefault = true; + return this; + } + + public ChannelResource UsingLifecycle(LifecycleResource lifecycle) + { + LifecycleId = lifecycle.Id; + return this; + } + + public ChannelResource ClearRules() + { + Rules.Clear(); + return this; + } + + public ChannelResource AddRule(ChannelVersionRuleResource rule) + { + Rules.Add(rule); + return this; + } + + public ChannelResource AddCommonRuleForAllActions(string versionRange, string tagRegex, DeploymentProcessResource process) + { + var actionsWithPackage = process.Steps.SelectMany(s => s.Actions.Where(a => a.Properties.Any(p => p.Key == "Octopus.Action.Package.PackageId"))).ToArray(); + return AddRule(versionRange, tagRegex, actionsWithPackage); + } + + public ChannelResource AddRule(string versionRange, string tagRegex, params DeploymentActionResource[] actions) + { + Rules.Add(new ChannelVersionRuleResource + { + Actions = new ReferenceCollection(actions.Select(a => a.Id)), + VersionRange = versionRange, + Tag = tagRegex + }); + + return this; + } + + public ChannelResource ClearTenantTags() + { + TenantTags.Clear(); + return this; + } + + public ChannelResource AddOrUpdateTenantTags(params TagResource[] tags) + { + foreach (var tag in tags) + { + TenantTags.Add(tag.CanonicalTagName); + } + + return this; + } + } +} diff --git a/source/Octopus.Client/Model/ChannelVersionRuleResource.cs b/source/Octopus.Client/Model/ChannelVersionRuleResource.cs new file mode 100644 index 000000000..9520a6660 --- /dev/null +++ b/source/Octopus.Client/Model/ChannelVersionRuleResource.cs @@ -0,0 +1,16 @@ +namespace Octopus.Client.Model +{ + public class ChannelVersionRuleResource : Resource + { + [Writeable] + public ReferenceCollection Actions { get; set; } + + [Writeable] + [Trim] + public string VersionRange { get; set; } + + [Writeable] + [Trim] + public string Tag { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/CommunicationStyle.cs b/source/Octopus.Client/Model/CommunicationStyle.cs new file mode 100644 index 000000000..3418f8fa9 --- /dev/null +++ b/source/Octopus.Client/Model/CommunicationStyle.cs @@ -0,0 +1,30 @@ +using System; +using Octopus.Client.Model.Accounts; + +namespace Octopus.Client.Model +{ + public enum CommunicationStyle + { + None = 0, + + /// + /// Listening + /// + [ScriptConsoleSupported] [TentacleUpgradeSupported] TentaclePassive = 1, + + /// + /// Polling + /// + [ScriptConsoleSupported] [TentacleUpgradeSupported] TentacleActive = 2, + + [ScriptConsoleSupported] [SupportedAccountTypes(AccountType.SshKeyPair, AccountType.UsernamePassword)] Ssh = 3, + + OfflineDrop = 4, + + [ScriptConsoleSupported] AzureWebApp = 5, + + [SupportedAccountTypes(AccountType.UsernamePassword)] Ftp = 6, + + [SupportedAccountTypes(AccountType.AzureSubscription)] [ScriptConsoleSupported] AzureCloudService = 7 + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DashboardConfigurationResource.cs b/source/Octopus.Client/Model/DashboardConfigurationResource.cs new file mode 100644 index 000000000..c100b9b3b --- /dev/null +++ b/source/Octopus.Client/Model/DashboardConfigurationResource.cs @@ -0,0 +1,30 @@ +using System; + +namespace Octopus.Client.Model +{ + public class DashboardConfigurationResource : Resource + { + public DashboardConfigurationResource() + { + IncludedProjectIds = new ReferenceCollection(); + IncludedEnvironmentIds = new ReferenceCollection(); + IncludedTenantIds = new ReferenceCollection(); + IncludedTenantTags = new ReferenceCollection(); + } + + [Writeable] + public ReferenceCollection IncludedProjectIds { get; set; } + + [Writeable] + public ReferenceCollection IncludedEnvironmentIds { get; set; } + + [Writeable] + public ReferenceCollection IncludedTenantIds { get; set; } + + [Writeable] + public int? ProjectLimit { get; set; } + + [Writeable] + public ReferenceCollection IncludedTenantTags { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DashboardEnvironmentResource.cs b/source/Octopus.Client/Model/DashboardEnvironmentResource.cs new file mode 100644 index 000000000..5ed435a31 --- /dev/null +++ b/source/Octopus.Client/Model/DashboardEnvironmentResource.cs @@ -0,0 +1,9 @@ +using System; + +namespace Octopus.Client.Model +{ + public class DashboardEnvironmentResource : Resource + { + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DashboardItemResource.cs b/source/Octopus.Client/Model/DashboardItemResource.cs new file mode 100644 index 000000000..4fa73e023 --- /dev/null +++ b/source/Octopus.Client/Model/DashboardItemResource.cs @@ -0,0 +1,27 @@ +using System; + +namespace Octopus.Client.Model +{ + public class DashboardItemResource : Resource + { + public string ProjectId { get; set; } + public string EnvironmentId { get; set; } + public string ReleaseId { get; set; } + public string DeploymentId { get; set; } + public string TaskId { get; set; } + public string TenantId { get; set; } + public string ChannelId { get; set; } + public string ReleaseVersion { get; set; } + public DateTimeOffset Created { get; set; } + public DateTimeOffset QueueTime { get; set; } + public DateTimeOffset? CompletedTime { get; set; } + public TaskState State { get; set; } + public bool HasPendingInterruptions { get; set; } + public bool HasWarningsOrErrors { get; set; } + public string ErrorMessage { get; set; } + public string Duration { get; set; } + public bool IsCurrent { get; set; } + public bool IsPrevious { get; set; } + public bool IsCompleted { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DashboardProjectGroupResource.cs b/source/Octopus.Client/Model/DashboardProjectGroupResource.cs new file mode 100644 index 000000000..5eb3c2f6d --- /dev/null +++ b/source/Octopus.Client/Model/DashboardProjectGroupResource.cs @@ -0,0 +1,17 @@ +using System; + +namespace Octopus.Client.Model +{ + public class DashboardProjectGroupResource : Resource + { + public string Name { get; set; } + public ReferenceCollection EnvironmentIds { get; set; } + + public DashboardProjectGroupResource Copy() + { + var copy = (DashboardProjectGroupResource)this.MemberwiseClone(); + copy.EnvironmentIds = new ReferenceCollection(this.EnvironmentIds); + return copy; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DashboardProjectResource.cs b/source/Octopus.Client/Model/DashboardProjectResource.cs new file mode 100644 index 000000000..52e96921e --- /dev/null +++ b/source/Octopus.Client/Model/DashboardProjectResource.cs @@ -0,0 +1,24 @@ +using System; +using Newtonsoft.Json; + +namespace Octopus.Client.Model +{ + public class DashboardProjectResource : Resource + { + public string Name { get; set; } + public string Slug { get; set; } + public string ProjectGroupId { get; set; } + public ReferenceCollection EnvironmentIds { get; set; } + + [JsonIgnore] + public ProjectTenantedDeploymentMode TenantDeploymentMode { get; set; } + public bool CanPerformUntenantedDeployment { get; set; } + + public DashboardProjectResource Copy() + { + var copy = (DashboardProjectResource)this.MemberwiseClone(); + copy.EnvironmentIds = new ReferenceCollection(this.EnvironmentIds); + return copy; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DashboardResource.cs b/source/Octopus.Client/Model/DashboardResource.cs new file mode 100644 index 000000000..a8f9f9772 --- /dev/null +++ b/source/Octopus.Client/Model/DashboardResource.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace Octopus.Client.Model +{ + public class DashboardResource : Resource + { + public List Projects { get; set; } + public List ProjectGroups { get; set; } + public List Environments { get; set; } + public List Tenants { get; set; } + public List Items { get; set; } + public List PreviousItems { get; set; } + public int? ProjectLimit { get; set; } + public bool IsFiltered { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DashboardTenantResource.cs b/source/Octopus.Client/Model/DashboardTenantResource.cs new file mode 100644 index 000000000..864726187 --- /dev/null +++ b/source/Octopus.Client/Model/DashboardTenantResource.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Octopus.Client.Model +{ + public class DashboardTenantResource : Resource + { + public string Name { get; set; } + public IDictionary ProjectEnvironments { get; set; } + public ReferenceCollection TenantTags { get; set; } + } +} diff --git a/source/Octopus.Client/Model/Defect.cs b/source/Octopus.Client/Model/Defect.cs new file mode 100644 index 000000000..4a858cd8e --- /dev/null +++ b/source/Octopus.Client/Model/Defect.cs @@ -0,0 +1,10 @@ +using System; + +namespace Octopus.Client.Model +{ + public class Defect + { + public string Description { get; set; } + public DefectStatus Status { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DefectResource.cs b/source/Octopus.Client/Model/DefectResource.cs new file mode 100644 index 000000000..cb60b24da --- /dev/null +++ b/source/Octopus.Client/Model/DefectResource.cs @@ -0,0 +1,33 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + +namespace Octopus.Client.Model +{ + public class DefectResource : Resource + { + [JsonConstructor] + public DefectResource() + { + } + + public DefectResource(string description) + : this() + { + Description = description; + } + + public DefectResource(string description, DefectStatus status) + : this() + { + Description = description; + Status = status; + } + + [Required(ErrorMessage = "Please specify the defect description.")] + [WriteableOnCreate] + public string Description { get; set; } + + public DefectStatus? Status { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DefectStatus.cs b/source/Octopus.Client/Model/DefectStatus.cs new file mode 100644 index 000000000..5c1c180df --- /dev/null +++ b/source/Octopus.Client/Model/DefectStatus.cs @@ -0,0 +1,10 @@ +using System; + +namespace Octopus.Client.Model +{ + public enum DefectStatus + { + Unresolved, + Resolved + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DeploymentActionResource.cs b/source/Octopus.Client/Model/DeploymentActionResource.cs new file mode 100644 index 000000000..420844b88 --- /dev/null +++ b/source/Octopus.Client/Model/DeploymentActionResource.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace Octopus.Client.Model +{ + public class DeploymentActionResource : Resource + { + public string Name { get; set; } + public string ActionType { get; set; } + + [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Reuse)] + public ReferenceCollection Environments { get; } = new ReferenceCollection(); + + [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Reuse)] + public ReferenceCollection Channels { get; } = new ReferenceCollection(); + + [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Reuse)] + public ReferenceCollection TenantTags { get; } = new ReferenceCollection(); + + [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Reuse)] + public IDictionary Properties { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public DeploymentActionResource ClearAllConditions() + { + Channels.Clear(); + Environments.Clear(); + TenantTags.Clear(); + return this; + } + + public DeploymentActionResource ForChannels(params ChannelResource[] channels) + { + Channels.ReplaceAll(channels.Select(c => c.Id)); + return this; + } + + public DeploymentActionResource ForEnvironments(params EnvironmentResource[] environments) + { + Environments.ReplaceAll(environments.Select(e => e.Id)); + return this; + } + + public DeploymentActionResource ForTenantTags(params TagResource[] tags) + { + TenantTags.ReplaceAll(tags.Select(t => t.CanonicalTagName)); + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DeploymentApprovalStatus.cs b/source/Octopus.Client/Model/DeploymentApprovalStatus.cs new file mode 100644 index 000000000..bff45e2e9 --- /dev/null +++ b/source/Octopus.Client/Model/DeploymentApprovalStatus.cs @@ -0,0 +1,10 @@ +using System; + +namespace Octopus.Client.Model +{ + public enum DeploymentApprovalStatus + { + Approved, + Rejected + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DeploymentPreviewResource.cs b/source/Octopus.Client/Model/DeploymentPreviewResource.cs new file mode 100644 index 000000000..18f3425a7 --- /dev/null +++ b/source/Octopus.Client/Model/DeploymentPreviewResource.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using Octopus.Client.Model.Forms; + +namespace Octopus.Client.Model +{ + public class DeploymentPreviewResource : Resource + { + public List StepsToExecute { get; set; } + public Form Form { get; set; } + public bool UseGuidedFailureModeByDefault { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DeploymentProcessResource.cs b/source/Octopus.Client/Model/DeploymentProcessResource.cs new file mode 100644 index 000000000..514c9083f --- /dev/null +++ b/source/Octopus.Client/Model/DeploymentProcessResource.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace Octopus.Client.Model +{ + public class DeploymentProcessResource : Resource + { + public DeploymentProcessResource() + { + Steps = new List(); + } + + public string ProjectId { get; set; } + + public IList Steps { get; private set; } + + [Required] + public int Version { get; set; } + + public string LastSnapshotId { get; set; } + + public DeploymentStepResource FindStep(string name) + { + return Steps.FirstOrDefault(s => string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase)); + } + + public DeploymentStepResource AddOrUpdateStep(string name) + { + var existing = FindStep(name); + + DeploymentStepResource step; + if (existing == null) + { + step = new DeploymentStepResource + { + Name = name + }; + + Steps.Add(step); + } + else + { + existing.Name = name; + + step = existing; + } + + // Return the step so you can add actions + return step; + } + + public DeploymentProcessResource RemoveStep(string name) + { + Steps.Remove(FindStep(name)); + return this; + } + + public DeploymentProcessResource ClearSteps() + { + Steps.Clear(); + return this; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DeploymentPromomotionTenant.cs b/source/Octopus.Client/Model/DeploymentPromomotionTenant.cs new file mode 100644 index 000000000..bbe720f1f --- /dev/null +++ b/source/Octopus.Client/Model/DeploymentPromomotionTenant.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace Octopus.Client.Model +{ + public class DeploymentPromomotionTenant : Resource + { + public DeploymentPromomotionTenant() + { + PromoteTo = new List(); + } + + public string Name { get; set; } + public List PromoteTo { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DeploymentPromotionTarget.cs b/source/Octopus.Client/Model/DeploymentPromotionTarget.cs new file mode 100644 index 000000000..a96e49fed --- /dev/null +++ b/source/Octopus.Client/Model/DeploymentPromotionTarget.cs @@ -0,0 +1,9 @@ +using System; + +namespace Octopus.Client.Model +{ + public class DeploymentPromotionTarget : Resource + { + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DeploymentResource.cs b/source/Octopus.Client/Model/DeploymentResource.cs new file mode 100644 index 000000000..1fe566fc1 --- /dev/null +++ b/source/Octopus.Client/Model/DeploymentResource.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + +namespace Octopus.Client.Model +{ + public class DeploymentResource : Resource + { + public DeploymentResource() + { + FormValues = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + } + + [Required(ErrorMessage = "Please specify the release to deploy.")] + [WriteableOnCreate] + public string ReleaseId { get; set; } + + [Required(ErrorMessage = "Please provide a target environment to deploy to.")] + [WriteableOnCreate] + public string EnvironmentId { get; set; } + + [WriteableOnCreate] + public string TenantId { get; set; } + + [WriteableOnCreate] + public bool ForcePackageDownload { get; set; } + + [WriteableOnCreate] + public bool ForcePackageRedeployment { get; set; } + + [WriteableOnCreate] + public ReferenceCollection SkipActions { get; set; } + + /// + /// A collection of machines in the target environment + /// that should be deployed to. If the collection is + /// empty, all enabled machines are deployed. + /// + [WriteableOnCreate] + public ReferenceCollection SpecificMachineIds { get; set; } + + public string DeploymentProcessId { get; set; } + public string ManifestVariableSetId { get; set; } + public string TaskId { get; set; } + public string ProjectId { get; set; } + public string ChannelId { get; set; } + + /// + /// If set to true, the deployment will prompt for manual intervention (Fail/Retry/Ignore) when + /// failures are encountered in activities that support it. May be overridden with the + /// Octopus.UseGuidedFailure special variable. + /// + [WriteableOnCreate] + public bool UseGuidedFailure { get; set; } + + [WriteableOnCreate] + public string Comments { get; set; } + + [WriteableOnCreate] + [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Reuse)] + public Dictionary FormValues { get; set; } + + /// + /// If set this time will be the used to schedule the deployment to a later time, null is assumed to mean the time will + /// be executed immediately. + /// + public DateTimeOffset? QueueTime { get; set; } + + public string Name { get; set; } + public DateTimeOffset Created { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DeploymentStepCondition.cs b/source/Octopus.Client/Model/DeploymentStepCondition.cs new file mode 100644 index 000000000..dad789d79 --- /dev/null +++ b/source/Octopus.Client/Model/DeploymentStepCondition.cs @@ -0,0 +1,11 @@ +using System; + +namespace Octopus.Client.Model +{ + public enum DeploymentStepCondition + { + Success, + Failure, + Always + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DeploymentStepProperties.cs b/source/Octopus.Client/Model/DeploymentStepProperties.cs new file mode 100644 index 000000000..5e00be5c3 --- /dev/null +++ b/source/Octopus.Client/Model/DeploymentStepProperties.cs @@ -0,0 +1,23 @@ +namespace Octopus.Client.Model +{ + public class DeploymentStepProperties + { + public enum HealthCheckType + { + FullHealthCheck, + ConnectionTest + } + + public enum HealthCheckErrorHandling + { + TreatExceptionsAsErrors, + TreatExceptionsAsWarnings + } + + public enum HealthCheckIncludeMachinesInDeployment + { + DoNotAlterMachines, + IncludeCheckedMachines + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DeploymentStepResource.cs b/source/Octopus.Client/Model/DeploymentStepResource.cs new file mode 100644 index 000000000..6d183992d --- /dev/null +++ b/source/Octopus.Client/Model/DeploymentStepResource.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Octopus.Client.Editors.DeploymentProcess; + +namespace Octopus.Client.Model +{ + public class DeploymentStepResource + { + public string Id { get; set; } + public string Name { get; set; } + + /// + /// This flag causes packages to be downloaded before the step runs regardless of whether any + /// of the actions within the step need packages. If the actions need packages, then the step + /// will be scheduled after acquisition regardless of the value of this flag. + /// + public bool RequiresPackagesToBeAcquired { get; set; } + + [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Reuse)] + public IDictionary Properties { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public DeploymentStepCondition Condition { get; set; } + public DeploymentStepStartTrigger StartTrigger { get; set; } + + [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Reuse)] + public List Actions { get; } = new List(); + + public DeploymentStepResource ClearActions() + { + Actions.Clear(); + return this; + } + + public DeploymentStepResource WithCondition(DeploymentStepCondition condition) + { + Condition = condition; + return this; + } + + public DeploymentStepResource WithStartTrigger(DeploymentStepStartTrigger startTrigger) + { + StartTrigger = startTrigger; + return this; + } + + public DeploymentStepResource RequirePackagesToBeAcquired(bool requirePackagesToBeAcquired = true) + { + RequiresPackagesToBeAcquired = requirePackagesToBeAcquired; + return this; + } + + public DeploymentStepResource TargetingRoles(params string[] roles) + { + var targetRoles = roles == null || roles.Length == 0 + ? null + : string.Join(",", roles); + + Properties["Octopus.Action.TargetRoles"] = targetRoles; + return this; + } + + public DeploymentActionResource FindAction(string name) + { + return Actions.FirstOrDefault(a => string.Equals(a.Name, name, StringComparison.OrdinalIgnoreCase)); + } + + public DeploymentStepResource RemoveAction(string name) + { + Actions.Remove(FindAction(name)); + return this; + } + + public DeploymentActionResource AddOrUpdateAction(string name) + { + var existing = FindAction(name); + + DeploymentActionResource action; + if (existing == null) + { + action = new DeploymentActionResource + { + Name = name + }; + + Actions.Add(action); + } + else + { + existing.Name = name; + action = existing; + } + + // Return the action so you can make customizations + return action; + } + + public DeploymentActionResource AddOrUpdateManualInterventionAction(string name, string instructions) + { + var action = AddOrUpdateAction(name); + + action.ActionType = "Octopus.Manual"; + action.Properties.Clear(); + action.Properties["Octopus.Action.Manual.Instructions"] = instructions; + + return action; + } + + public DeploymentActionResource AddOrUpdateScriptAction(string name, ScriptAction scriptAction, ScriptTarget scriptTarget) + { + var action = AddOrUpdateAction(name); + + action.ActionType = "Octopus.Script"; + action.Properties.Clear(); + action.Properties["Octopus.Action.RunOnServer"] = scriptTarget == ScriptTarget.Server ? "true" : "false"; + action.Properties["Octopus.Action.Script.Syntax"] = scriptAction.Syntax.ToString(); + action.Properties["Octopus.Action.Script.ScriptSource"] = scriptAction.Source.ToString(); + + RequiresPackagesToBeAcquired = scriptAction.Source == ScriptSource.Package; + + switch (scriptAction.Source) + { + case ScriptSource.Inline: + string scriptBody = null; + var inlineScript = scriptAction as InlineScriptAction; + if (inlineScript != null) + { + scriptBody = inlineScript.GetScriptBody(); + } + var inlineScriptFromFileInAssembly = scriptAction as InlineScriptActionFromFileInAssembly; + if (inlineScriptFromFileInAssembly != null) + { + scriptBody = inlineScriptFromFileInAssembly.GetScriptBody(); + } + + if (scriptBody == null) throw new NotSupportedException($"{scriptAction.GetType().Name} is not a supported Script Action type yet..."); + + action.Properties.Add("Octopus.Action.Script.ScriptBody", scriptBody); + break; + + case ScriptSource.Package: + var packageScript = (ScriptActionFromFileInPackage) scriptAction; + action.Properties.Add("Octopus.Action.Package.PackageId", packageScript.PackageId); + action.Properties.Add("Octopus.Action.Package.FeedId", packageScript.PackageFeedId); + action.Properties.Add("Octopus.Action.Script.ScriptFileName", packageScript.ScriptFilePath); + break; + } + + return action; + } + + public DeploymentActionResource AddOrUpdatePackageAction(string name, PackageResource package) + { + var action = AddOrUpdateAction(name); + + action.ActionType = "Octopus.TentaclePackage"; + action.Properties["Octopus.Action.Package.PackageId"] = package.PackageId; + action.Properties["Octopus.Action.Package.FeedId"] = package.FeedId; + + return action; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DeploymentStepStartTrigger.cs b/source/Octopus.Client/Model/DeploymentStepStartTrigger.cs new file mode 100644 index 000000000..c91197202 --- /dev/null +++ b/source/Octopus.Client/Model/DeploymentStepStartTrigger.cs @@ -0,0 +1,10 @@ +using System; + +namespace Octopus.Client.Model +{ + public enum DeploymentStepStartTrigger + { + StartAfterPrevious = 0, + StartWithPrevious = 1 + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DeploymentTemplateResource.cs b/source/Octopus.Client/Model/DeploymentTemplateResource.cs new file mode 100644 index 000000000..7cc2f67e6 --- /dev/null +++ b/source/Octopus.Client/Model/DeploymentTemplateResource.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace Octopus.Client.Model +{ + public class DeploymentTemplateResource : Resource + { + public DeploymentTemplateResource() + { + PromoteTo = new List(); + TenantPromotions = new List(); + } + + public bool IsDeploymentProcessModified { get; set; } + + public bool IsVariableSetModified { get; set; } + + public bool IsLibraryVariableSetModified { get; set; } + + public string DeploymentNotes { get; set; } + + public List PromoteTo { get; set; } + + public List TenantPromotions { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DeploymentTemplateStep.cs b/source/Octopus.Client/Model/DeploymentTemplateStep.cs new file mode 100644 index 000000000..8937b78c6 --- /dev/null +++ b/source/Octopus.Client/Model/DeploymentTemplateStep.cs @@ -0,0 +1,21 @@ +using System; + +namespace Octopus.Client.Model +{ + public class DeploymentTemplateStep + { + public string ActionId { get; set; } + public string ActionName { get; set; } + public string ActionNumber { get; set; } + public string[] Roles { get; set; } + + [Obsolete] // TODO: [ObsoleteEx(TreatAsErrorFromVersion = "4.0", RemoveInVersion = "4.0", ReplacementTypeOrMember = "Machines")] + public string[] MachineNames { get; set; } + + public MachineDeploymentPreview[] Machines { get; set; } + public bool CanBeSkipped { get; set; } + public bool HasNoApplicableMachines { get; set; } + public ReferenceDataItem[] UnavailableMachines { get; set; } + public ReferenceDataItem[] ExcludedMachines { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DiscoverableEndpointType.cs b/source/Octopus.Client/Model/DiscoverableEndpointType.cs new file mode 100644 index 000000000..d88d09662 --- /dev/null +++ b/source/Octopus.Client/Model/DiscoverableEndpointType.cs @@ -0,0 +1,11 @@ +using System; + +namespace Octopus.Client.Model +{ + public enum DiscoverableEndpointType + { + TentaclePassive, + TentacleActive, + Ssh + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DocumentIdFormatException.cs b/source/Octopus.Client/Model/DocumentIdFormatException.cs new file mode 100644 index 000000000..a181e4cb7 --- /dev/null +++ b/source/Octopus.Client/Model/DocumentIdFormatException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Octopus.Client.Model +{ + public class DocumentIdFormatException : Exception + { + public DocumentIdFormatException(string message) + : base(message) + { + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/DocumentIdParser.cs b/source/Octopus.Client/Model/DocumentIdParser.cs new file mode 100644 index 000000000..6070aa997 --- /dev/null +++ b/source/Octopus.Client/Model/DocumentIdParser.cs @@ -0,0 +1,17 @@ +using System; + +namespace Octopus.Client.Model +{ + public static class DocumentIdParser + { + public static void Split(string documentId, out string groupPrefix, out string identitySuffix) + { + if (documentId == null) throw new ArgumentNullException("documentId"); + var dash = documentId.LastIndexOf('-'); + if (dash < 0) + throw new DocumentIdFormatException($"The document ID {documentId} doesn't have a dash!"); + groupPrefix = documentId.Substring(0, dash); + identitySuffix = documentId.Substring(dash + 1); + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/EncryptedBytes.cs b/source/Octopus.Client/Model/EncryptedBytes.cs new file mode 100644 index 000000000..191e1e99f --- /dev/null +++ b/source/Octopus.Client/Model/EncryptedBytes.cs @@ -0,0 +1,44 @@ +using System; + +namespace Octopus.Client.Model +{ + public class EncryptedBytes + { + readonly byte[] ciphertext; + readonly byte[] salt; + + public EncryptedBytes(byte[] ciphertext, byte[] salt) + { + if (ciphertext == null) throw new ArgumentNullException("ciphertext"); + if (salt == null) throw new ArgumentNullException("salt"); + this.ciphertext = ciphertext; + this.salt = salt; + } + + public byte[] Ciphertext + { + get { return ciphertext; } + } + + public byte[] Salt + { + get { return salt; } + } + + public string ToBase64() + { + var cipher64 = Convert.ToBase64String(ciphertext); + var salt64 = Convert.ToBase64String(salt); + + return cipher64 + "|" + salt64; + } + + public static EncryptedBytes FromBase64(string base64) + { + var parts = base64.Split('|'); + var cipher = Convert.FromBase64String(parts[0]); + var salt = Convert.FromBase64String(parts[1]); + return new EncryptedBytes(cipher, salt); + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Endpoints/AgentlessEndpointResource.cs b/source/Octopus.Client/Model/Endpoints/AgentlessEndpointResource.cs new file mode 100644 index 000000000..6cee021bf --- /dev/null +++ b/source/Octopus.Client/Model/Endpoints/AgentlessEndpointResource.cs @@ -0,0 +1,8 @@ +using System; + +namespace Octopus.Client.Model.Endpoints +{ + public abstract class AgentlessEndpointResource : EndpointResource + { + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Endpoints/AzureWebAppEndpointResource.cs b/source/Octopus.Client/Model/Endpoints/AzureWebAppEndpointResource.cs new file mode 100644 index 000000000..3dbb5beb3 --- /dev/null +++ b/source/Octopus.Client/Model/Endpoints/AzureWebAppEndpointResource.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Octopus.Client.Model.Endpoints +{ + public class AzureWebAppEndpointResource : AgentlessEndpointResource + { + public override CommunicationStyle CommunicationStyle + { + get { return CommunicationStyle.AzureWebApp; } + } + + [Trim] + [Writeable] + public string AccountId { get; set; } + + [Trim] + [Writeable] + public string WebSpaceName { get; set; } + + [Trim] + [Writeable] + public string WebAppName { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Endpoints/CloudRegionEndpointResource.cs b/source/Octopus.Client/Model/Endpoints/CloudRegionEndpointResource.cs new file mode 100644 index 000000000..a1345cd0c --- /dev/null +++ b/source/Octopus.Client/Model/Endpoints/CloudRegionEndpointResource.cs @@ -0,0 +1,7 @@ +namespace Octopus.Client.Model.Endpoints +{ + public class CloudRegionEndpointResource : AgentlessEndpointResource + { + public override CommunicationStyle CommunicationStyle => CommunicationStyle.None; + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Endpoints/CloudServiceEndpointResource.cs b/source/Octopus.Client/Model/Endpoints/CloudServiceEndpointResource.cs new file mode 100644 index 000000000..0fa1f9c6e --- /dev/null +++ b/source/Octopus.Client/Model/Endpoints/CloudServiceEndpointResource.cs @@ -0,0 +1,38 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Octopus.Client.Model.Endpoints +{ + public class CloudServiceEndpointResource : AgentlessEndpointResource + { + public override CommunicationStyle CommunicationStyle + { + get { return CommunicationStyle.AzureCloudService; } + } + + [Trim] + [Writeable] + [Required(ErrorMessage = "Please specify an account.")] + public string AccountId { get; set; } + + [Trim] + [Writeable] + [Required(ErrorMessage = "Please specify the cloud service name.")] + public string CloudServiceName { get; set; } + + [Trim] + [Writeable] + [Required(ErrorMessage = "Please specify a storage acccount.")] + public string StorageAccountName { get; set; } + + [Trim] + [Writeable] + public string Slot { get; set; } + + [Writeable] + public bool SwapIfPossible { get; set; } + + [Writeable] + public bool UseCurrentInstanceCount { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Endpoints/EndpointResource.cs b/source/Octopus.Client/Model/Endpoints/EndpointResource.cs new file mode 100644 index 000000000..c85876dde --- /dev/null +++ b/source/Octopus.Client/Model/Endpoints/EndpointResource.cs @@ -0,0 +1,9 @@ +using System; + +namespace Octopus.Client.Model.Endpoints +{ + public abstract class EndpointResource : Resource + { + public abstract CommunicationStyle CommunicationStyle { get; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Endpoints/ListeningTentacleEndpointResource.cs b/source/Octopus.Client/Model/Endpoints/ListeningTentacleEndpointResource.cs new file mode 100644 index 000000000..a7277f770 --- /dev/null +++ b/source/Octopus.Client/Model/Endpoints/ListeningTentacleEndpointResource.cs @@ -0,0 +1,19 @@ +using System; + +namespace Octopus.Client.Model.Endpoints +{ + public class ListeningTentacleEndpointResource : TentacleEndpointResource + { + public override CommunicationStyle CommunicationStyle + { + get { return CommunicationStyle.TentaclePassive; } + } + + [Trim] + [Writeable] + public string Uri { get; set; } + + [Writeable] + public string ProxyId { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Endpoints/OfflineDropEndpointResource.cs b/source/Octopus.Client/Model/Endpoints/OfflineDropEndpointResource.cs new file mode 100644 index 000000000..55df21734 --- /dev/null +++ b/source/Octopus.Client/Model/Endpoints/OfflineDropEndpointResource.cs @@ -0,0 +1,32 @@ +using System; + +namespace Octopus.Client.Model.Endpoints +{ + public class OfflineDropEndpointResource : AgentlessEndpointResource + { + public OfflineDropEndpointResource() + { + SensitiveVariablesEncryptionPassword = new SensitiveValue(); + } + + public override CommunicationStyle CommunicationStyle + { + get { return CommunicationStyle.OfflineDrop; } + } + + [Trim] + [Writeable] + public string DropFolderPath { get; set; } + + [Writeable] + public SensitiveValue SensitiveVariablesEncryptionPassword { get; set; } + + [Trim] + [Writeable] + public string ApplicationsDirectory { get; set; } + + [Trim] + [Writeable] + public string OctopusWorkingDirectory { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Endpoints/PollingTentacleEndpointResource.cs b/source/Octopus.Client/Model/Endpoints/PollingTentacleEndpointResource.cs new file mode 100644 index 000000000..751db759d --- /dev/null +++ b/source/Octopus.Client/Model/Endpoints/PollingTentacleEndpointResource.cs @@ -0,0 +1,16 @@ +using System; + +namespace Octopus.Client.Model.Endpoints +{ + public class PollingTentacleEndpointResource : TentacleEndpointResource + { + public override CommunicationStyle CommunicationStyle + { + get { return CommunicationStyle.TentacleActive; } + } + + [Trim] + [Writeable] + public string Uri { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Endpoints/SshEndpointResource.cs b/source/Octopus.Client/Model/Endpoints/SshEndpointResource.cs new file mode 100644 index 000000000..0ddbe4451 --- /dev/null +++ b/source/Octopus.Client/Model/Endpoints/SshEndpointResource.cs @@ -0,0 +1,39 @@ +using System; + +namespace Octopus.Client.Model.Endpoints +{ + public class SshEndpointResource : AgentlessEndpointResource + { + public override CommunicationStyle CommunicationStyle + { + get { return CommunicationStyle.Ssh; } + } + + [Trim] + [Writeable] + public string AccountId { get; set; } + + [Trim] + [Writeable] + public string Host { get; set; } + + [Writeable] + public int Port { get; set; } + + [Trim] + [Writeable] + public string Fingerprint { get; set; } + + public string Uri + { + get + { + var uri = new UriBuilder("ssh", Host, Port); + return uri.ToString(); + } + } + + [Writeable] + public string ProxyId { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Endpoints/TentacleDetailsResource.cs b/source/Octopus.Client/Model/Endpoints/TentacleDetailsResource.cs new file mode 100644 index 000000000..2e811d88c --- /dev/null +++ b/source/Octopus.Client/Model/Endpoints/TentacleDetailsResource.cs @@ -0,0 +1,13 @@ +using System; + +namespace Octopus.Client.Model.Endpoints +{ + public class TentacleDetailsResource + { + [Writeable] + public bool UpgradeLocked { get; set; } + public string Version { get; set; } + public bool UpgradeSuggested { get; set; } + public bool UpgradeRequired { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Endpoints/TentacleEndpointResource.cs b/source/Octopus.Client/Model/Endpoints/TentacleEndpointResource.cs new file mode 100644 index 000000000..02a689ca6 --- /dev/null +++ b/source/Octopus.Client/Model/Endpoints/TentacleEndpointResource.cs @@ -0,0 +1,16 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Octopus.Client.Model.Endpoints +{ + public abstract class TentacleEndpointResource : EndpointResource + { + [Required(ErrorMessage = "Please provide a thumbprint for this machine.")] + [Trim] + [Writeable] + public string Thumbprint { get; set; } + + [Writeable] + public TentacleDetailsResource TentacleVersionDetails { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/EnvironmentResource.cs b/source/Octopus.Client/Model/EnvironmentResource.cs new file mode 100644 index 000000000..69cde684d --- /dev/null +++ b/source/Octopus.Client/Model/EnvironmentResource.cs @@ -0,0 +1,41 @@ +using System; + +namespace Octopus.Client.Model +{ + /// + /// Represents an environment. Environments are user-defined and map to real world deployment environments + /// such as development, staging, test and production. Projects are deployed to environments. + /// + public class EnvironmentResource : Resource, INamedResource + { + /// + /// Gets or sets the name of this environment. This should be short, preferably 5-20 characters. + /// + [Writeable] + [Trim] + public string Name { get; set; } + + /// + /// Gets or sets a short description of this environment that can be used to explain the purpose of + /// the environment to other users. This field may contain markdown. + /// + [Writeable] + [Trim] + public string Description { get; set; } + + /// + /// Gets or sets a number indicating the priority of this environment in sort order. Environments with + /// a lower sort order will appear in the UI before items with a higher sort order. + /// + [Writeable] + public int SortOrder { get; set; } + + /// + /// If set to true, deployments will prompt for manual intervention (Fail/Retry/Ignore) when + /// failures are encountered in activities that support it. May be overridden with the + /// Octopus.UseGuidedFailure special variable. + /// + [Writeable] + public bool UseGuidedFailure { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/EventReference.cs b/source/Octopus.Client/Model/EventReference.cs new file mode 100644 index 000000000..bd47cbebe --- /dev/null +++ b/source/Octopus.Client/Model/EventReference.cs @@ -0,0 +1,33 @@ +using System; + +namespace Octopus.Client.Model +{ + public class EventReference + { + readonly string referencedDocumentId; + readonly int startIndex; + readonly int length; + + public EventReference(string referencedDocumentId, int startIndex, int length) + { + this.referencedDocumentId = referencedDocumentId; + this.startIndex = startIndex; + this.length = length; + } + + public string ReferencedDocumentId + { + get { return referencedDocumentId; } + } + + public int StartIndex + { + get { return startIndex; } + } + + public int Length + { + get { return length; } + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/EventResource.cs b/source/Octopus.Client/Model/EventResource.cs new file mode 100644 index 000000000..8e9cf5d21 --- /dev/null +++ b/source/Octopus.Client/Model/EventResource.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; + +namespace Octopus.Client.Model +{ + /// + /// Events are automatically created when significant actions take place within Octopus by users. Examples are adding + /// environments, modifying projects, + /// deploying releases, canceling tasks, and so on. Events can be used to provide an audit trail of what has happened + /// in the system. The HTTP API *cannot* + /// be used to add, modify or delete events. + /// + public class EventResource : Resource + { + /// + /// Gets or sets a collection of document ID's that this event relates to. Note that the document ID's may no longer + /// exist. + /// + public ReferenceCollection RelatedDocumentIds { get; set; } + + /// + /// Gets or sets the event category. + /// + public string Category { get; set; } + + /// + /// Gets or sets the ID of the user who created the event. + /// + public string UserId { get; set; } + + /// + /// Gets or sets the name of the user who created the event. + /// + public string Username { get; set; } + + /// + /// Gets or sets a description of how the user performing the event + /// identified themselves to Octopus. + /// + public string IdentityEstablishedWith { get; set; } + + /// + /// Gets or sets the date/time that the event took place. + /// + public DateTimeOffset Occurred { get; set; } + + /// + /// Gets or sets the message text that summarizes the event. + /// + public string Message { get; set; } + + /// + /// Gets or sets the message text that summarizes the event, HTML formatted with links to the related documents. + /// + public string MessageHtml { get; set; } + + /// + /// Gets or sets an array of document ID's and indexes where they are mentioned in the message text. + /// + public List MessageReferences { get; set; } + + /// + /// Gets or sets any user-provided comments that were recorded with the event. + /// + public string Comments { get; set; } + + /// + /// Gets or sets the details of the event. For events representing a modification to a document this will provide a + /// HTML-formatted diff of the original and new document. + /// + public string Details { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/EventScope.cs b/source/Octopus.Client/Model/EventScope.cs new file mode 100644 index 000000000..303764a59 --- /dev/null +++ b/source/Octopus.Client/Model/EventScope.cs @@ -0,0 +1,12 @@ +using System; + +namespace Octopus.Client.Model +{ + public enum EventScope + { + Application = 0, + Project = 1, + Release = 2, + Deployment = 3 + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/ExternalSecurityGroupResource.cs b/source/Octopus.Client/Model/ExternalSecurityGroupResource.cs new file mode 100644 index 000000000..573f563cc --- /dev/null +++ b/source/Octopus.Client/Model/ExternalSecurityGroupResource.cs @@ -0,0 +1,9 @@ +using System; + +namespace Octopus.Client.Model +{ + public class ExternalSecurityGroupResource : Resource + { + public string DisplayName { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/FeaturesConfigurationResource.cs b/source/Octopus.Client/Model/FeaturesConfigurationResource.cs new file mode 100644 index 000000000..f3ecb754f --- /dev/null +++ b/source/Octopus.Client/Model/FeaturesConfigurationResource.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Octopus.Client.Model +{ + public class FeaturesConfigurationResource : Resource + { + [Writeable] + public bool IsMultiTenancyEnabled { get; set; } + } +} diff --git a/source/Octopus.Client/Model/FeedResource.cs b/source/Octopus.Client/Model/FeedResource.cs new file mode 100644 index 000000000..d59777b5f --- /dev/null +++ b/source/Octopus.Client/Model/FeedResource.cs @@ -0,0 +1,24 @@ +using System; + +namespace Octopus.Client.Model +{ + public class FeedResource : Resource, INamedResource + { + public FeedResource() + { + Password = new SensitiveValue(); + } + + [Writeable] + public string Name { get; set; } + + [Writeable] + public string FeedUri { get; set; } + + [Writeable] + public string Username { get; set; } + + [Writeable] + public SensitiveValue Password { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/FileUpload.cs b/source/Octopus.Client/Model/FileUpload.cs new file mode 100644 index 000000000..a1e09dc4e --- /dev/null +++ b/source/Octopus.Client/Model/FileUpload.cs @@ -0,0 +1,12 @@ +using System; +using System.IO; + +namespace Octopus.Client.Model +{ + public class FileUpload + { + public Stream Contents { get; set; } + + public string FileName { get; set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Forms/Button.cs b/source/Octopus.Client/Model/Forms/Button.cs new file mode 100644 index 000000000..ddd94075d --- /dev/null +++ b/source/Octopus.Client/Model/Forms/Button.cs @@ -0,0 +1,22 @@ +using System; + +namespace Octopus.Client.Model.Forms +{ + /// + /// A button is essentially an 'option' that may be associated + /// with other form elements. + /// + public class Button + { + public Button(string text, string value = null) + { + if (text == null) throw new ArgumentNullException("text"); + + Value = value ?? text; + Text = text; + } + + public string Text { get; private set; } + public object Value { get; private set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Forms/Checkbox.cs b/source/Octopus.Client/Model/Forms/Checkbox.cs new file mode 100644 index 000000000..5b10b8c64 --- /dev/null +++ b/source/Octopus.Client/Model/Forms/Checkbox.cs @@ -0,0 +1,28 @@ +using System; + +namespace Octopus.Client.Model.Forms +{ + /// + /// A Boolean option. + /// + public class Checkbox : Control + { + public Checkbox(string text) + { + if (text == null) throw new ArgumentNullException("text"); + Text = text; + } + + public string Text { get; private set; } + + public override object CoerceValue(string value) + { + return bool.Parse(value); + } + + public override Type GetNativeValueType() + { + return typeof (bool); + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Forms/Control.cs b/source/Octopus.Client/Model/Forms/Control.cs new file mode 100644 index 000000000..4648e0b6c --- /dev/null +++ b/source/Octopus.Client/Model/Forms/Control.cs @@ -0,0 +1,32 @@ +using System; + +namespace Octopus.Client.Model.Forms +{ + /// + /// An visual component of a . + /// + public abstract class Control + { + /// + /// Convert a string into the native type supported + /// by the control. Only supported if + /// returns a non-null type. + /// + /// The value to coerce. Must not be null or whitespace. + /// The value. + public virtual object CoerceValue(string value) + { + throw new InvalidOperationException("The control does not support values"); + } + + /// + /// Get the native value type supported by the control. + /// If the value returned is null, the control does not support values. + /// + /// The native type, or null. + public virtual Type GetNativeValueType() + { + return null; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Forms/Form.cs b/source/Octopus.Client/Model/Forms/Form.cs new file mode 100644 index 000000000..f8b16f523 --- /dev/null +++ b/source/Octopus.Client/Model/Forms/Form.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace Octopus.Client.Model.Forms +{ + /// + /// A form is a set of form elements, and the values that apply or may be provided for those elements. + /// + public class Form + { + [JsonConstructor] + public Form() + { + Elements = new List(); + Values = new Dictionary(); + } + + public Form(IEnumerable elements = null, IDictionary values = null) + { + Values = values != null + ? new Dictionary(values, StringComparer.OrdinalIgnoreCase) + : new Dictionary(StringComparer.OrdinalIgnoreCase); + + Elements = elements != null ? elements.ToList() : new List(); + } + + /// + /// Values supplied for the form elements. + /// + [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Reuse)] + public Dictionary Values { get; private set; } + + /// + /// Elements of the form. + /// + [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Reuse)] + public List Elements { get; private set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Forms/FormElement.cs b/source/Octopus.Client/Model/Forms/FormElement.cs new file mode 100644 index 000000000..6f88155c0 --- /dev/null +++ b/source/Octopus.Client/Model/Forms/FormElement.cs @@ -0,0 +1,33 @@ +using System; + +namespace Octopus.Client.Model.Forms +{ + /// + /// An item displayed or retrieved from a . + /// + public class FormElement + { + public FormElement(string name, Control control, bool isValueRequired = false) + { + Name = name; + Control = control; + IsValueRequired = isValueRequired; + } + + /// + /// The name of the element. Must be unique within the form. + /// + public string Name { get; private set; } + + /// + /// A control used to render the element. + /// + public Control Control { get; private set; } + + /// + /// If true, the receiver of the form expects that a value will be + /// provided for the element. + /// + public bool IsValueRequired { get; private set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Forms/Paragraph.cs b/source/Octopus.Client/Model/Forms/Paragraph.cs new file mode 100644 index 000000000..05c75eca2 --- /dev/null +++ b/source/Octopus.Client/Model/Forms/Paragraph.cs @@ -0,0 +1,17 @@ +using System; + +namespace Octopus.Client.Model.Forms +{ + /// + /// A block of instructive text. + /// + public class Paragraph : Control + { + public Paragraph(string text) + { + Text = text; + } + + public string Text { get; private set; } + } +} \ No newline at end of file diff --git a/source/Octopus.Client/Model/Forms/SubmitButtonGroup.cs b/source/Octopus.Client/Model/Forms/SubmitButtonGroup.cs new file mode 100644 index 000000000..08687317b --- /dev/null +++ b/source/Octopus.Client/Model/Forms/SubmitButtonGroup.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace Octopus.Client.Model.Forms +{ + /// + /// A group of options, represented as buttons. The value of the element is the + /// value of the selected button. + /// + public class SubmitButtonGroup : Control + { + public SubmitButtonGroup(List