AtlassianPS / JiraPS

PowerShell module to interact with Atlassian JIRA
https://AtlassianPS.org/module/JiraPS
MIT License
323 stars 131 forks source link

New Server & Session handling #194

Open lipkau opened 6 years ago

lipkau commented 6 years ago

Preamble

I have been annoyed with the module's behavior that the server to be used is stored in a file (in the module's root).

This keeps me from being able to have 2 powershell consoles running stuff on different servers.
This can be fixed by cloning the module's folder and importing the "different module" into the consoles

Proposition

TL;DR

Have the Server and the Session (and Server Alias) stored in a private variable of the module (1). And allow this to be exported and imported (except for the session)

Setting up Server connection

Import-Module JiraPS
Set-JiraInfo -BaseURI 'http://...'

Set-JiraInfo would be harmonized with ConfluencePS's Set-ConfluenceInfo. Meaning:

Set-JiraInfo [[-BaseURi] <Uri>] [[-ServerAlias] <String>] [[-Server] <String>]

Set-JiraInfo -BaseURI 'http://'    # Set the address of the server
Set-JiraInfo -URi 'http://'       # Set the address of the server (alias)
Set-JiraInfo -Url 'http://'       # Set the address of the server (alias)
Set-JiraInfo -BaseURI 'http://' -ServerName 'foo'   # Set the address of the server and give it a "friendly name" 
Set-JiraInfo -URi 'http://' -Name 'foo'                # Set the address of the server and give it a "friendly name"  (alias)
Set-JiraInfo -Url 'http://' -Alias 'foo'                   # Set the address of the server and give it a "friendly name" (alias)
Set-JiraInfo -Server 'foo'                                    # Set the default value of `-Server` to 'foo' in all function of the module (in this instance)

Set-JiraInfo would also be able to set default values to other parameters that are common to the module's functions (eg: the PageSize, once implemented) I also intend for every

Saving the configuration

Import-Module JiraPS
Set-JiraInfo -BaseURI 'http://...'
Export-JiraConfiguration   # with alias `Export-JiraConfig`

This would use the Configuration Module to persist the Server URL and Alias to disk. It also supports custom paths and Scopes

The module shall execute Import-JiraConfiguration when it is imported

Reusing previous configuration

Import-Module JiraPS
Import-JiraConfiguration  # with alias `Import-JiraConfig`

This would use the Configuration Module to retrive the Server URL and Alias from disk.

If the user is using session for authentication, this would require him to create new session for them to be associated with the Server Configuration. Eg:

Import-Module JiraPS
Import-JiraConfiguration
New-JiraSession -Server "cloud" -Credential (Get-Credential)

[JiraPS.Server]

The data to be stored in the JiraPS internals would be a custom class, that has the following properties:

This can be stored as an array in a local variable of the module. eg:

PS> $script.Configuration

returns

 [JiraPS.Server]@{
   BaseURi = "https://f.atlassian.net"
   ServerName = "cloud"
   Session = $websession1
 }
 [JiraPS.Server]@{
   BaseURi = "https://jira.corp.com"
   ServerName = "OnPremise"
   Session = $websession2
 }
)

Using JiraPS commands

The all public JiraPS commands will support the parameter -Server. This could be the BaseURi or the ServerName of a [JiraPS.Server] object. If the Server has a session, this session will be used - unless -Credential was provided by the user

Additional Data

The user should be able to define configuration data to the module. Candidates are:

Get|Set|Remove-ServerConfiguration


Motivation

This would fix or make the following issues obsolete:


[1] Currently $MyInvocation.MyCommand.Module.PrivateData is used. But this extends the PrivateData of the module's manifest

https://github.com/AtlassianPS/AtlassianPS/issues/4

Jaykul commented 6 years ago

My suggestion would be that you:

  1. Should call Import-JiraConfiguration within the module at module load time (so the module is configured at startup).
  2. Consider using the same Noun for Set as for Export and Import ... that is, choose one of JiraInfo or JiraConfiguration.
  3. Consider adding a Get command to return that object (pipeable to Set) or hasthable (splattable to Set) so people can temporarily swap? In this case, you might include the Session...

As far as where to keep that in your module:

A script scope variable will be available to all functions in your module, and is as simple as $Script:Configuration = ... and $Script:Configuration.BaseUri or whatever. That's the best way to handle it.

MyCommand.Module.PrivateData is pre-populated with the PrivateData from your psd1. Unless you want to use your PrivateData as a place to pre-configure things, it's not particularly useful.

ebekker commented 6 years ago

So if I understand correctly, this will support having distinct connection profiles (i.e. server URL, credentials) in each PS session. But you would not be able to use different profiles within the same session, correct?

This might be a uncommon use case, but I was thinking about a case where you may want to talk to two different instances of JIRA (say cloud vs hosted) or under two different identities (accessing JIRA under the context of different usernames or API keys). This is something I think the named/persistent "profile" concept of #45 would be able to support, but perhaps it's not a common situation.

Either way, I think the concepts above are definitely an improvement in flexibility and usability.

ebekker commented 6 years ago

Maybe add credentials to JiraPS.Server class? But then be sure to either securely store the file when exported (DataProtect) or use a SecureString for password (or just the PSCredential for both username and password).

lipkau commented 6 years ago

Not quite, @ebekker

As mentioned in Using JiraPS commands, the user is able to tell each command he executes what server to use. eg:

Get-JiraIssue "Foo-1234" -Server "cloud"
Get-JiraIssue "Foo-1234" -Server "OnPremise"

In this example, each command would run against a different server. This example assumes both "-Server" entries in [JiraPS.Server] have a valid Session stored. eg:

@(
  [JiraPS.Server]@{
    BaseURi = "https://f.atlassian.net"
    ServerName = "cloud"
    Session = $websession1
  }
  [JiraPS.Server]@{
    BaseURi = "https://jira.corp.com"
    ServerName = "OnPremise"
    Session = $websession2
  }
)

Regarding the exporting of credentials: I don't want the credentials to be persisted by the module. However, I will write a documentation page where I will suggest using BetterCredentials for that

ebekker commented 6 years ago

Is there any reason to have distinct configs and associated cmdlets across the different AtlassianPS tools? Would it be better to just have a unified set, and all the tools can reference the same effective config?

ebekker commented 6 years ago

@lipkau, that makes sense now -- sounds good.

Just as you had questioned up above, I'm not sure if a WebRequestSession is naturally exportable, but you may be able to fake it enough, by simply exporting the cookies and any default headers defined in the session, and then recreating a session with those details when a new session is needed (e.g. start of PS session).

lipkau commented 6 years ago

@ebekker wrote: Is there any reason to have distinct configs and associated cmdlets across the different AtlassianPS tools

Yes: we don't have it yet, and we need to start somewhere. Pretty much all I am suggesting here can be transfere to other modules once we know how we want this to look like.

There are 2 "complications:

lipkau commented 6 years ago
function Get-Item {
    # Public function
    param(
        # Server address
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [URi] $ApiURi = (Get-JiraConfiguration -Server $Server).BaseURi,

        # InputObject
        [Parameter(Mandatory = $true)]
        [PSObject] $InputObject,

        # Server from config
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [String] $Server,

        # Credential
        [Parameter()]
        [PSCredential] $Credential
    )

    $restResource = "/rest/item/$(InputObject.Id)"

    Invoke-Stuff -Resource $restResource -ApiURi $ApiURi -Credential $Credential
}

function Get-JiraConfiguration {
    # Public/Private function -> not sure yet
    param(
        # Server URL or Alias
        [Parameter(Mandatory = $true)]
        [String] $Server
    )

    if ($script:Configuration | Where {$_.ServerName -like $Server}) {
        $script:Configuration | Where {$_.ServerName -like $Server}
    }
    elseif ($script:Configuration | Where {$_.BaseURi -like $Server}) {
        $script:Configuration | Where {$_.BaseURi -like $Server}
    }
}

function Invoke-Stuff {
    # Private function
    param(
        # Base URL of the server
        [Parameter(Mandatory = $true)]
        [URi] $ApiURi,

        # Relative path of the REST resource
        [Parameter(Mandatory = $true)]
        [String] $Resource,

        # Credentials
        [Parameter()]
        [PSCredential] $Credential
    )

    if (!($ApiURi)) { Throw "Failed to identify the server URL" }

    $splat = @{
        Uri = "$ApiURi$Resource"
    }

    if (!($Credential)) {
        $splat["WebSession"] = (Get-JiraConfiguration -Server $ApiURi).Session
    }
    else {
        $splat["Headers"] = @{Authentication = "Basic $(EncodeBase64 -Credential $Credential)"}
    }

    invoke-webrequest @splat
}
lipkau commented 6 years ago

This is kinda implemented in https://github.com/AtlassianPS/BitbucketPS/issues/20