Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Binali Rustamov committed Dec 20, 2019
0 parents commit d05edc5
Show file tree
Hide file tree
Showing 27 changed files with 724 additions and 0 deletions.
37 changes: 37 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
; EditorConfig to support per-solution formatting.
; Use the EditorConfig VS add-in to make this work.
; http://editorconfig.org/
;
; Here are some resources for what's supported for .NET/C#
; https://kent-boogaart.com/blog/editorconfig-reference-for-c-developers
; https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference?view=vs-2017
;
; Be **careful** editing this because some of the rules don't support adding a severity level
; For instance if you change to `dotnet_sort_system_directives_first = true:warning` (adding `:warning`)
; then the rule will be silently ignored.

; This is the default for the codeline.
root = true

[*]
indent_style = space
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.cs]
indent_size = 4
dotnet_sort_system_directives_first = true

[*.{xml,config,*proj,nuspec,props,resx,targets,yml,tasks}]
indent_size = 2

[*.json]
indent_size = 2

[*.{ps1,psm1}]
indent_size = 4

[*.sh]
indent_size = 4
end_of_line = lf
33 changes: 33 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
*.swp
*.*~
project.lock.json
.DS_Store
*.pyc

# Visual Studio Code
.vscode

# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
msbuild.log
msbuild.err
msbuild.wrn

# Visual Studio 2015
.vs/
.idea/
16 changes: 16 additions & 0 deletions PortainerClient.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PortainerClient", "PortainerClient\PortainerClient.csproj", "{CA90FC5B-18FB-413B-A000-D75D4E52FC93}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CA90FC5B-18FB-413B-A000-D75D4E52FC93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CA90FC5B-18FB-413B-A000-D75D4E52FC93}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CA90FC5B-18FB-413B-A000-D75D4E52FC93}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CA90FC5B-18FB-413B-A000-D75D4E52FC93}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
35 changes: 35 additions & 0 deletions PortainerClient/Api/Base/BaseApiService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using PortainerClient.Helpers;
using RestSharp;
using RestSharp.Authenticators;

namespace PortainerClient.Api
{
public abstract class BaseApiService
{
private RestClient _client;

private RestClient ResolveClient()
{
if (_client != null)
return _client;
var config = ConfigModel.Load();
_client ??= new RestClient(config.Url);
_client.Authenticator = new JwtAuthenticator(config.Token);
return _client;
}

protected T Get<T>(string resource, params (string paramName, object paramValue)[] parameters) where T : new()
{
var request = new RestRequest(resource, Method.GET);
foreach (var (paramName, paramValue) in parameters) request.AddParameter(paramName, paramValue);
var response = ResolveClient().Execute<T>(request);
if (!response.IsSuccessful)
{
throw new InvalidOperationException($"Request {resource} error: {response.Content}");
}

return response.Data;
}
}
}
8 changes: 8 additions & 0 deletions PortainerClient/Api/Model/AuthInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace PortainerClient.Api.Model
{
public class AuthInfo
{
public string Username { get; set; }
public string Password { get; set; }
}
}
8 changes: 8 additions & 0 deletions PortainerClient/Api/Model/StackEnv.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace PortainerClient.Api.Model
{
public class StackEnv
{
public string Name { get; set; }
public string Value { get; set; }
}
}
17 changes: 17 additions & 0 deletions PortainerClient/Api/Model/StackInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Collections;
using System.Collections.Generic;

namespace PortainerClient.Api.Model
{
public class StackInfo
{
public string Id { get; set; }
public string Name { get; set; }
public StackType Type { get; set; }
public int EndpointId { get; set; }
public string EntryPoint { get; set; }
public string SwarmId { get; set; }
public string ProjectPath { get; set; }
public IEnumerable<StackEnv> Env { get; set; }
}
}
8 changes: 8 additions & 0 deletions PortainerClient/Api/Model/StackType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace PortainerClient.Api.Model
{
public enum StackType
{
Swarm = 1,
Compose = 2
}
}
7 changes: 7 additions & 0 deletions PortainerClient/Api/Model/TokenInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace PortainerClient.Api.Model
{
public class TokenInfo
{
public string Jwt { get; set; }
}
}
11 changes: 11 additions & 0 deletions PortainerClient/Api/StacksApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Collections.Generic;
using PortainerClient.Api;
using PortainerClient.Api.Model;

namespace PortainerClient.Command.Stack
{
public class StacksApiService : BaseApiService
{
public IEnumerable<StackInfo> GetStacks() => Get<List<StackInfo>>("stacks");
}
}
75 changes: 75 additions & 0 deletions PortainerClient/Command/Auth/AuthCmd.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Net;
using McMaster.Extensions.CommandLineUtils;
using PortainerClient.Api.Model;
using PortainerClient.Config;
using PortainerClient.Helpers;
using RestSharp;

namespace PortainerClient.Command
{
[Command(Name = "auth", Description = "Authorize this client in Portainer (required before using)")]
public class AuthCmd : ICommand
{
[Option("--url", "Portainer url", CommandOptionType.SingleValue)]
[Required]
public string PortainerUrl { get; set; }

[Option("--user", "User name", CommandOptionType.SingleValue)]
[Required]
public string User { get; set; }

[Option("--password", "Password", CommandOptionType.SingleValue)]
[Required]
public string Password { get; set; }

public AuthCmd()
{
}

private static void Authorize(string url, string user, string password)
{
var client = new RestClient(url + "/api");
IRestResponse<TokenInfo> tokenInfoResponse;
try
{
var request = new RestRequest("auth", Method.POST, DataFormat.Json);
request.AddJsonBody(new AuthInfo {Username = user, Password = password});
tokenInfoResponse = client.Execute<TokenInfo>(request);
}
catch (Exception e)
{
throw new Exception($"Authorization error. Detailed message: {e.Message}");
}

if (tokenInfoResponse.StatusCode != HttpStatusCode.OK)
{
throw new Exception($"Authorization error. Detailed message: {tokenInfoResponse.Content}");
}

var configModel = new ConfigModel
{
Url = url + "/api",
Token = tokenInfoResponse.Data.Jwt
};
configModel.Save();
}

public int OnExecute(CommandLineApplication app, IConsole console)
{
try
{
Authorize(PortainerUrl, User, Password);
console.WriteLine("Authorized. Now you can execute any commands");
return 0;
}
catch (Exception ex)
{
console.ForegroundColor = ConsoleColor.Red;
console.Error.WriteLine($"Login error: {ex.Message}");
return 1;
}
}
}
}
31 changes: 31 additions & 0 deletions PortainerClient/Command/BaseApiCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using McMaster.Extensions.CommandLineUtils;
using PortainerClient.Helpers;

namespace PortainerClient.Command
{
public abstract class BaseApiCommand<T> : ICommand where T : class, new()
{
protected T ApiClient;

protected BaseApiCommand()
{
ApiClient = new T();
}

public virtual int OnExecute(CommandLineApplication app, IConsole console)
{
try
{
Do(app, console);
return 0;
}
catch (Exception ex)
{
return console.WriteError(ex);
}
}

public abstract void Do(CommandLineApplication app, IConsole console);
}
}
9 changes: 9 additions & 0 deletions PortainerClient/Command/ICommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using McMaster.Extensions.CommandLineUtils;

namespace PortainerClient.Command
{
public interface ICommand
{
int OnExecute(CommandLineApplication app, IConsole console);
}
}
22 changes: 22 additions & 0 deletions PortainerClient/Command/Stack/StackCmd.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using McMaster.Extensions.CommandLineUtils;
using PortainerClient.Command.Stack;
using PortainerClient.Helpers;

namespace PortainerClient.Command
{
[Command(Name = "stack", Description = "Docker Stack management commands")]
[Subcommand(
typeof(StackLsCmd)
// ,
// typeof(StackGetFileCmd),
// typeof(StackDeployCmd),
// typeof(StackUpdateCmd),
// typeof(StackRmCmd),
// typeof(StackInspectCmd)
)]
public class StackCmd : ICommand
{
public int OnExecute(CommandLineApplication app, IConsole console) =>
CmdHelpers.SpecifyCommandResult(app, console);
}
}
56 changes: 56 additions & 0 deletions PortainerClient/Command/Stack/StackDeployCmd.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using IO.Swagger.Api;
using McMaster.Extensions.CommandLineUtils;
using Newtonsoft.Json;
using PortainerClient.Helpers;

namespace PortainerClient.Command.Stack
{
[Command("deploy", "Deploy new Swarm stack from file")]
public class StackDeployCmd : BaseApiCommand<StacksApi>
{
[Option("--file", "Docker Swarm stack definition file path", CommandOptionType.SingleValue, ShortName = "f")]
[Required]
public string FilePath { get; set; }

[Option("--endpoint-id", "ID of endpoint used to deploy (get it from Portainer)",
CommandOptionType.SingleValue, ShortName = "eid")]
[Required]
public int EndpointId { get; set; }

[Option("--swarm-id", "Swarm cluster id", CommandOptionType.SingleValue, ShortName = "sid")]
[Required]
public string SwarmId { get; set; }

[Option("--env",
"Environment variable used in definition file (format -e NAME1=VALUE1 -e NAME2=VALUE2 -e ...) (optional)",
CommandOptionType.MultipleValue, ShortName = "e")]
public string[] Envs { get; set; }

[Argument(0, "stackName", "Name for new stack")]
[Required]
public string StackName { get; set; }


public override void Do(CommandLineApplication app, IConsole console)
{
if (!File.Exists(FilePath))
{
throw new Exception("Stack definition file is not found. Please, give correct path to file.");
}

var stackEnvs = CmdHelpers.ParseEnvs(Envs);

var fileStream = File.OpenRead(FilePath);
// 1 - swarm stack
console.WriteLine("Sending deploy request to Portainer...");
var result = ApiClient.StackCreate(type: 1, method: "file", endpointId: EndpointId, body: null,
name: StackName,
endpointID: EndpointId.ToString(), swarmID: SwarmId, file: fileStream,
env: JsonConvert.SerializeObject(stackEnvs));
console.WriteLine("Stack deployed.");
}
}
}
Loading

0 comments on commit d05edc5

Please sign in to comment.