Saturday, November 30, 2013

Windows Azure Active Directory Graph: Setting up a Tenant Domain, Application Principal ID and Application Password

Introduction

This posting demonstrates how to create the elements needed to programmatically access the RESTful API associated with Windows Azure Active Directory Graph. The following definitions are useful in understanding what is required to develop such applications:
  • Tenant: owns or manages an instance of a cloud service
  • Windows Azure Tenant: each Microsoft cloud service is associated with its own instance of Windows Azure Active Directory (Windows Azure AD). This AD instance is associated with an organization's cloud service and serves said service's tenant.
  • Service Principal: this is an instance of an application within the AD tenant. Policies including permissions are applied to a service principal. For example: 
    • An Address Book application's service principal might have read-only access to the AD instance associated with a Windows cloud service. 
    • An HR application's service principal might have read-write access to the AD instance associated with a Windows cloud service.
An application that accesses Windows Azure Active Directory does so by accessing its service principal. A service principal is accessible by an application instance (a program instances) by the tenant domain (the URL associated with the application), a principal ID (analogous to a username) and principal password (the password associated with the principal ID).

There are a large number of excellent examples (Windows Azure AD Graph Code Samples Index) demonstrating the development of applications that use "Windows Azure Active Directory Graph." Each of these samples access AD Graph and therefore must make use of a service principal. The samples each utilize an app.config or web.config file containing elements corresponding to what is required to access a service principal. An example from "Sample App for Windows Azure AD Graph Differential Query" taken from an app.config is as follows:

<add key="TenantDomainName" value="BoostWIP365.onmicrosoft.com"/>
<add key="AppPrincipalId" value="9221e5fc-f3df-4337-8403-928abeb7af4e"/>
<add key="Password" value="G7Jo9Mis0DSyFXVdbEaA" />

The problem with the Windows Azure samples and documentation (including those related to setting up a service principal) is that Azure is a rapidly evolving platform. Microsoft is adding features at a remarkable pace so that documentation that is two years old may be obsolete. Documentation that is only six months old may be similarly obsolete. This posting attempts to be current with respect to setting up a service principal given the release of Windows 8.1.

Setting up an Azure Service Principal

These steps should be performed on a development machine (the machine on which you have Visual Studio 2013 installed or on which PowerShell scripts are developed). To create the tenant information perform the following steps:

1. Install Microsoft Online Services Sign-In Assistant version 7.0 or greater (there is a separate version for Windows 7/Windows 2008R2 and a newer version for Windows 8/8.1):

If you are running Windows 7 or Windows 2008/2008 R2 install the version of Microsoft Online Services Sign-In Assistant  found at: Microsoft Online Services Sign-In Assistant for IT Professionals RTW 

Note the system requirements (a.k.a. do not install on Windows 8 or Windows 8.1):


If you are running Windows 8 or Windows 8.1 install the version of Microsoft Online Services Sign-In Assistant  found at:Microsoft Online Services Sign-In Assistant for IT Professionals BETA

Note the system requirements below:


2. "Install the Windows Azure AD Module" based on your operating system (32-bit or 64-bit):


3. When prompted (see below) run the downloaded application:


3.1 It is useful to allow the installer to place a shortcut to the Windows Azure AD module for Windows PowerShell on the desktop.
4. Download the sample application "Sample App for Windows Azure AD Graph Differential Query".
5. Unzip the sample in order to gain access to CreateServicePrincipal.ps1. This PowerShell script lives up to its name sake; the script creates a service principal.
6. Run the Windows Azure AD module for Windows PowerShell by clicking on the shortcut previously installed on the desktop:



7. The CreateServicePrincipal.ps1 PowshellScript should be run within the console, Windows Azure AD module for Windows PowerShell:

Note: if an error is encountered "CreateServicePrincipal.ps1 cannot be loaded because the execution of scripts is disabled on this system" please see "Running Windows PowerShell Scripts"

8. Hit return to run the script. The CreateServicePrincipal.ps1 script when run is prone to prompt prolifically. After CreateServicePrincipal.ps1 is run, the user is prompted as follows:


9. Enter R and hit return to run the script:


10. When prompted enter Y and hit return to continue execution:


11. The previous screen show the user being prompted to "Enter a Service Principal Name" to which the name "Order Muffins" was entered. When return is hit the following is displayed:


12. To continue the user presses any key (unless they are worn out by excessive prompting) followed by entering their credentials:
  • username: this is in the form of <username>@<domain>.onmicrosoft.com
  • password: the password associated with the domain account

13. After the user successfully enters their credentials they are prompted again (as follows):  


14. Pressing any key continues the prompts deluge. The user is ask whether they want their service principal to have read-only or read-write permissions as an AD tenant:


15. After entering R (read-only) or W (read-write) the script will run for several seconds creating the service principal:


16. The previous screen shot obviously removes the application principal ID and the application principal password. The principal ID is in the form of a GUID (e,g, d266d7cc-13c7-4e89-aaac-8c699cf6aff2) and the password is in the form of a case-sensitive text string (e,g, 8K+0OX6pvUZtaGo4YdUogT9xiF15aqyx1HbSvEg8Sec=).

With an application principal ID and the application principal password, a user can now write their own application that exercises Windows Azure's graph API or they can run any of the samples provided by Microsoft.

Running Sample App for Windows Azure AD Graph Differential Query

When the sample application, Sample App for Windows Azure AD Graph Differential Query, is run it displays the following:

Clicking on the User Management link causes the application to invoke the Windows Azure AD Graph 'API using the sample's domain, principal ID and principal password. The results are as follows:

Change the domain, principal ID and principal password to the values created previously results in the following being displayed:


Obviously the AD repository of a newly created AD tenant contains fewer values than a Microsoft sample AD tenant but the code did behave correctly.

Appendix A: Creating a Windows Azure AD Tenant

This section is only for developers that need to create a Windows Azure Active Directory Tenant. All Windows cloud services are associated with an AD tenant. To create such a trial AD tenant a developer could sign up for free trial of Azure at "Try it out. For free" or sign up for a free trial of cloud-based Office 365 at www,Office365.com The approach taken here is to sign up directly for an AD Tenant at:  https://account.windowsazure.com/organization

The previous URL displays the following in Internet Explorer:



The domain being registered below is shown to be friendlybakery@onmicrosoft.com:


When the "check availability" green button has been pressed the web page prompts a user to provide their login and country information:


The "continue" green button on the previous screen is not enabled until the phone number associated with the account is validated. To begin the validation process select "send text message" which will send a text message containing a validation code to the phone number. The page will update as shown below to include a text box in which the verification code can be entered and a "verify code" button. 

When the code is entered (as shown below) click on the "verify code" button:


Once the code has been verified click on green button labeled "continue".

Thursday, November 28, 2013

Why is it Windows PowerShell and not simply PowerShell?

Logitech  (the peripherals company) just released a hardware device, Logitech PowerShell (Logitech Unveils First-of-its-Kind Console on the Go – PowerShell Controller + Battery). The Logitech PowerShell has nothing to do with Windows PowerShell. Logitech's device is a case for an IPhone 5 that contains a joy stick (okay a joy-stick-like-disk) and four buttons (A, B, X and Y). The Logitech PowerShell coverts an IPhone into a mobile game device (the image below was taken from a Logitech web site):



This begs the obvious question, "Why does Microsoft always say 'Windows PowerShell' and never simply 'PowerShell'?"

Power Shell is a trademark of Sutherland Golf, Inc. from 1999 and likely refers to the outer coating placed on golf balls. This conjecture as to the name PowerShell is based on Sutherland's 1999 patent filing "Golf ball with perforated barrier shell, US 6102815."


Friday, November 15, 2013

Visual Studio Online (cloud based TFS) Labels, what to name them so you don't get burned

This blog posting demonstrates the correct ways to name TFS labels. A good background on label behavior is Brian Harry's blog "Why TFS labels aren't like SourceSafe labels." The previous blog posting does not cover the correct way to name labels.

Every example, I have read as to how to use TFS labels is incorrect and has no basis in real-world software development. Case in point, consider the MSDN article, "Use labels to take a snapshot of your files" which presents how to use labels with Source Control Explorer within Visual Studio 2013. The following sentence is used in this article and the same article for Visual Studio 2005 through 2012: The following strings are a few examples of label names: "Sprint 5", "M1", "Beta2", and "Release Candidate 0".

To demonstrate why labels such as "Sprint 5", "M1", "Beta2", and "Release Candidate 0" are impractical consider the following TFS repository, project "MarkI" with two folders representing two separate applications: the Inventory App and the Mapping App:



The team working on the Inventory App has reached "Sprint 5" so their build engineer right clicks on the Inventory App folder and selects Apply Label from the Advanced sub-menu:



The build engineer for the Inventory App creates the label, "Sprint 5".

A few weeks later the Mapping App also reaches milestone "Sprint 5" so their build engineer attempts to apply the label "Sprint 5" to folder Mapping App resulting in the following error message:


The label "Sprint 5" has to be unique to a TFS project (the collection root, e.g. MarkI) and not a folder such as Inventory App or Mapping App. One firm I have worked with checked in their SVN source tree into a TFS project that include ten applications so requiring labels to be per-TFS-project unique rather than per-folder unique means that labels such as  "Sprint 5", "M1", "Beta2", and "Release Candidate 0" are useless.

The correct way to label is to include the folder name and enough information to make the label unique. A more logical label would be: Inventory App V3 Sprint 5. The previous label is project unique and will not clash with labels prefixed with Mapping App. The previous label would also not conflict with versions V4 and V5 of the Inventory App.




Thursday, November 14, 2013

The cost of Visual Studio Online (cloud based TFS) and 10 Visual Studio Premium Licences for a great price

On November 13, 2013, Microsoft renamed their hosted TFS to Visual Studio Online. The basic idea is the first five users are free (user type Basic) and after that the cost per-user, per-month is (all prices in this article are USD):
  • Visual Studio Online Basic: $10 introductory price, $20 regular price
  • Visual Studio Online Professional: $22.50 introductory price, $45 regular price
  • Visual Studio Online Advanced: $30 introductory price, $60 regular price

The pricing is a bit more complex. For example: Visual Studio Online Professional includes per-user use of Visual Studio Profession 2013 but this flavor of subscription is limited to ten users. Each Visual Studio Premium with MSDN user gets access to any Visual Studio Online project so they are not charged as an additional user. This is a fourth tier of user type. A complete breakdown of features can be found at Visual Studio Online.

Using the Microsoft Partner program for 2014 it is possible to acquire Visual Studio 2013 Premium with MSDN licenses that are extremely inexpensive ($235 per-user compared to the retail prices which is over $5000). The simplest way for a small development shop to acquire such licenses is to sign up to be a Microsoft Partner and complete the Application Development competency at a total cost of $2349.

The license breakdown is as follows for a Microsoft Silver Partner with the Application Development competency:
  • 5 Microsoft Visual Studio 2013 Premium with MSDN as part of Microsoft Silver Partner core benefits (see Core benefits and requirements)
  • 5 Microsoft Visual Studio 2013 Premium with MSDN as part of the Application Development competency (see Application Development)

The cost of becoming a Microsoft Silver Partner is $1850. The simplest way to acquire the Application Developer competency is to develop an application that passes one of the following application tests (all other application tests have been retired for 2014):
  • Silver Competency Test for Windows 8
  • Silver Competency Test for Windows Server 2012
  • Silver Competency Test for Windows Azure

The previous tests cost $499 each (see Partner Network: Microsoft Platform Ready). The alternative to platform testing is for an organization to employ or contract two Microsoft Certified Professionals (MCPs). The MCPs must each have met one of the following exam or certification requirements:
  • Exam 70-480: Programming in HTML5 with JavaScript and CSS3
  • Exam 70-481: Essentials of Developing Windows Store Apps using HTML5 and JavaScript
  • Exam 70-482: Advanced Store App Development using HTML5 and JavaScript
  • Exam 70-483: Programming in C#
  • Exam 70-484: Essentials of Developing Windows Store Apps in C#
  • Exam 70-485: Advanced Store App Development using C#
  • Exam 70-486: Developing ASP.NET MVC Web Applications
  • Exam 70-487: Developing Windows Azure and Web Services

The total cost for ten Microsoft Visual Studio Premium with MSDN ($1850 plus $499) is $2349. So a small development shop gets five free Basic licenses to hosted TFS (a.k.a Visual Studio Online) plus the ten licenses that come with being a Microsoft Silver Partner with the Application Development competency.

There are alternatives to this approach to gaining cost effective development license, namely signing up for Microsoft's Action Pack for Development and Design. The cost of this is $429 and is targeted at organizations that develop (including testing and design) web solutions and applications. There are are a large number of licenses associated with this package including three Visual Studio 2013 Professional with MSDN licenses. These three licenses added to the five free Basic user license for hosted TFS (a.k.a Visual Studio Online) give an organization access to eight users.

Becoming a Microsoft Partner (including the Application Development competency) or signing up for Action Pack for Development and Design is not solely about acquiring inexpensive Visual Studio licenses. Microsoft makes thousands of dollars in software available (SQL Server 2012, Windows 2012, etc.) as part of these plans and includes perks such as Bing Ads credits and free product support incidents. 

Thursday, November 7, 2013

Office Web Apps (free online Office) now Support Collaboration

This post discusses the latest feature of Microsoft's free Office Web Apps and how they convinced me to defect from Google Docs after being a loyal Google Docs user for nearly four years.

As a Microsoft Gold Partner, I've had access to Office's online versions since their inception in 2011. I have never cared to use this online Office suite for one specific reason, there was no built in collaboration. As of today, Microsoft has added real-time co-authoring to its Office Web App versions of Word, Excel and PowerPoint (to see the full write up, see the Office 365 blog, "Collaboration just got easier: Real-time co-authoring now available in Office Web Apps"). These new features allow users to modify documents simultaneously.

Signing up for Microsoft's free Office Web App versions is simple. Navigate to office.com (which will redirect you to http://office.microsoft.com). The following screen will be displayed:



There are lots of "for pay" options on the previous screen but the key to free Office Web Apps is to click on MY OFFICE which displays the following screen:



Sign in with your Microsoft Live ID and you have access to free online versions of Word, Excel and PowerPoint:



Up until today, I have been a huge proponent of Google Docs because Google Docs allows sharing and collaboration. Case in point, I was the trustee of a trust and had to provide yearly accounting. I simply shared a Google spreadsheet with the other beneficiaries of the trust. I was able to provide real-time access to the trust accounting without getting near an envelope or stamp.

What I disliked about Google Docs was how it changed document formatting. Case in point, I uploaded my resume in Word format to Google Docs. When I exported it again the formatting was changed. This is not he only limitation in Google Docs. There was a learning curve with Google Docs -- I had spent years working with Office's desktop versions. The Google Docs interface was fairly easy to use but I was still faster at using Microsoft Office for desktop.

What makes Microsoft Office Web Apps so elegant is how the user interface of each Office Web application mirrors the user interface of their desktop counterpart. An example of this is as follows where Microsoft Office Web Word contains the same layout and familiar ribbon as desktop Word:


The interface to access the word-processor associated with Google Docs is not nearly so familiar:



The final perk of using Office Web Apps is that the documents retain the same formatting as the desktop version of Office, hence I've become a Office Web Apps convert.

QED

Sunday, November 3, 2013

Build Automation: How to Correctly pull labels from TFS using PowerShell

Previously it was demonstrated how to use TF (TFS's command-line utility) in order to pull code via multiple TFS labels ("Build Automation: Getting multiple labels from TFS using TF GET (without deleting the files associated with the previous TF GET)"). The previously mentioned write up showed pulling labels using the command-line or ultimately a batch script. This article takes build automation to the next obvious level, namely how to pull source code from TFS via a label using PowerShell.

Pulling a single label is simple. Pulling multiple means that per-label directories need to be created and used for each invocation of TF GET.

The steps required to pull a label from TFS as part of an automated build are as follows:
1) Specify the following as input parameters:
  Disk location where label is pulled (source code directory)
  TFS work space and folder from which code is pulled
  Label name associated with TS work space and folder

2) Delete existing code from the source code directory

3) Create the source code directory

4) Change the current-working directory to the source code directory
The reason for this is outlined in: "Build Automation: Getting multiple labels from TFS using TF GET (without deleting the files associated with the previous TF GET)"

5) Delete the TFS work space if it exists
a.k.a. tf workspace /delete

6) Create the TFS work space
a.k.a. tf workspace /new 

7) Map the source code folder to the collection folder and associate this mapping with the work space
a.k.a. tf workfold /map

8) Get the label from TFS
a.k.a. tf get /version

1) Input Parameters and Setup

The input parameters will be as follows corresponding to the location on disk, the collection\folder within TFS and the label associated with the aforementioned collection\folder:

$TFSDiskLocation = 'C:\MarkI\Inventory App';
$Collection = 'MarkI\Inventory App';
$Label = 'InventoryApp1.2.3.4';


GetCode $TFSDiskLocation $Collection $Label;


The three parameters are passed to the GetCode method which is as follows:

function GetCode
{
    [CmdletBinding()]
    param            
    (            
        [parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]             
        [string] $TFSDiskLocation,

        [parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]             
        [string] $Collection,

        [parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]             
        [string] $Label
    )
        
    process
    {
        $Workspace = $Collection;
        $Workspace = CleanWorkspaceName $Workspace;
        TFSGetLabel $TFSDiskLocation $Workspace $Collection $Label;
    }
}

The previous code simply creates a TFS work space name based on the collection containing the code. The work space name is then passed to the TFSGetLabel function that actually performs all the steps required to get the code.

The function CleanWorkspaceName converts a collection into a legal work space name since it creates a work space name based on the restrictions TFS. The function CleanWorkspaceName uses a global parameter that insures the work space name does not exceed TFS' work space name length limitation as specified in "Naming Restrictions in Team Foundation":
$gTFSWorkspaceNameLength = 64;

The character restrictions are also presented in "Naming Restrictions in Team Foundation" which (to quote) are as follows:
  • Must not include the following printable characters: "/ \ [ ] : | < > + = ; ? *
  • Must not include nonprintable characters in the ASCII value range of 1-31
  • Must not end in a period (.)
  • Must not include commas (,)
The function CleanWorkspaceName is as follow where the code simply adheres to the previously discussed name length and character restrictions:

function CleanWorkspaceName
{
    [CmdletBinding()]
    param            
    (            
        [parameter(Mandatory=$true)]                                                               
        [ValidateNotNullOrEmpty()]             
        [string] $WorkspaceName
    )
        
    process
    {
        # Append computer name to workspace to aid debugging.
        # Remove trailing and leading spaces with Trim.
        $WorkspaceName = $env:computername + $WorkspaceName.Trim();
        # invalid workspace name characters: /:<>\|*?;
        $WorkspaceName = $WorkspaceName.Replace('/', '');
        $WorkspaceName = $WorkspaceName.Replace(':', '');
        $WorkspaceName = $WorkspaceName.Replace('<', '');
        $WorkspaceName = $WorkspaceName.Replace('>', '');
        $WorkspaceName = $WorkspaceName.Replace('\', '');
        $WorkspaceName = $WorkspaceName.Replace('|', '');
        $WorkspaceName = $WorkspaceName.Replace('*', '');
        $WorkspaceName = $WorkspaceName.Replace('?', '');
        $WorkspaceName = $WorkspaceName.Replace(';', '');
        $WorkspaceName = $WorkspaceName.Replace(',', '');
        $WorkspaceName = $WorkspaceName.Replace('.', '');
        # change spaces to underscore to avoid naming issues
        $WorkspaceName = $WorkspaceName.Replace(' ', '_'); 
        if ($WorkspaceName.Length -gt $gTFSWorkspaceNameLength)
        {
            $WorkspaceName = $WorkspaceName.Substring(0, 
                                     $gTFSWorkspaceNameLength);
        }

        return $WorkspaceName;
    }
}

2) Delete existing code from source code directory

In order to clean up the build location from a previous build, the source code location is deleted. This step is handled by the DeleteDirectory function which behaves as follows:

1) List all files in directory
1.1) If the file is read-only, clear the read-only flag
1.2) Delete the file
2) List all sub-directories
2.1) Invoke the DeleteDirectory on the sub-directory
2.2) Delete the sub-directory

function DeleteDirectory
{
    [CmdletBinding()]
    param            
    (   
        [parameter(Mandatory=$true)]                                                               
        [ValidateNotNullOrEmpty()]             
        [String] $directory
    )
   
    process            
    {   
        if (![System.IO.Directory]::Exists($directory))
        {
            return ;
        }

        $files = @();
        $files += [System.IO.Directory]::GetFiles($directory);
        foreach ($file in $files)
        {
            $attributes = [System.IO.File]::GetAttributes($file);
            if ($attributes -band 
                  [System.IO.FileAttributes]::ReadOnly)
            {
                $attributes = [int]$attributes - 
                   [int][System.IO.FileAttributes]::ReadOnly;
                [System.IO.File]::SetAttributes($file, 
                                     $attributes);
            }
            
            # Write-Host "[System.IO.File]::Delete($file)";
            [System.IO.File]::Delete($file);        
        }
        
        $directories = @();
        $directories += [System.IO.Directory]::GetDirectories(
                                                    $directory);
        foreach ($subDirectory in $directories)
        {
            DeleteDirectory $subDirectory;
        }
        
        # Write-Host "[System.IO.Directory]::Delete($directory)";
        [System.IO.Directory]::Delete($directory);
    }
}

The previous code is self-explanatory (basic file I/O).

The DeleteDirectory function is invoked from TFSGetLabel as follows which is the first action taken as part of the source code retrieval process:

function TFSGetLabel
{
    [CmdletBinding()]
    param            
    (   
        [parameter(Mandatory=$true)]                                                               
        [ValidateNotNullOrEmpty()]             
        [String] $SourceFolder,

        [parameter(Mandatory=$true)]                                                               
        [ValidateNotNullOrEmpty()]             
        [String] $WorkSpace,

        [parameter(Mandatory=$true)]                                                               
        [ValidateNotNullOrEmpty()]             
        [String] $Collection,

        [parameter(Mandatory=$true)]                                                               
        [ValidateNotNullOrEmpty()]             
        [String] $Label
)

    process            
    {   
        try            
        {
            Write-Host 'TFSGetLabel:SourceFolder $SourceFolder';
            Write-Host 'TFSGetLabel:Label $Label';            
            DeleteDirectory $SourceFolder;
            $DirectoryInfo = [System.IO.Directory]::CreateDirectory(
                                 $SourceFolder);
            [System.IO.Directory]::SetCurrentDirectory(
                                 $SourceFolder);
            $cwd = [System.IO.Directory]::GetCurrentDirectory();

            Write-Host "CWD: $cwd";            

The invocation of DeleteDirectory is accented in boldface above.

3) Create the source code directory

The creation of the source code directory in TFSGetLabel as follows where the action is indicated by the line in boldface:

Write-Host 'TFSGetLabel:SourceFolder $SourceFolder';
Write-Host 'TFSGetLabel:Label $Label';            
DeleteDirectory $SourceFolder;
$DirectoryInfo = [System.IO.Directory]::CreateDirectory(
$SourceFolder);
[System.IO.Directory]::SetCurrentDirectory(
$SourceFolder);
$cwd = [System.IO.Directory]::GetCurrentDirectory();
Write-Host "CWD: $cwd";        
    

4) Change the current-working directory to the source code directory

Changing the current working directory to be the same as the source code directory in TFSGetLabel as follows where the action is indicated by the line in boldface:

Write-Host 'TFSGetLabel:SourceFolder $SourceFolder';
Write-Host 'TFSGetLabel:Label $Label';            
DeleteDirectory $SourceFolder;
$DirectoryInfo = [System.IO.Directory]::CreateDirectory(
 $SourceFolder);
[System.IO.Directory]::SetCurrentDirectory(
 $SourceFolder);
$cwd = [System.IO.Directory]::GetCurrentDirectory();
Write-Host "CWD: $cwd";            

5) Delete the TFS work space if it exists

The code to delete the existing TFS work space relies on the function DoesTFSWorkspaceExist to determine if the work space already exists. The work space is only deleted if it already exists.

The DoesTFSWorkspaceExist function using the following global parameter corresponding to TFS's TF function:

$gTFUtility = 'tf';
    
The DoesTFSWorkspaceExist function:
1) Invokes the tf workspaces using .NET's System.Diagnostics.Process class as invoked via PowerShell.
1.1) The tf workspaces command lists all existing work spaces.
2) Read the output from the tf workspaces command to determine if the work space already exists.
2.1) The output is accessed via the System.Diagnostics.Process class' StandardOutput property.

The DoesTFSWorkspaceExist function is implemented as follows:

function DoesTFSWorkspaceExist
{
    [CmdletBinding()]
    param            
    (   
        [parameter(Mandatory=$true)]                                                               
        [ValidateNotNullOrEmpty()]             
        [String] $Workspace
    )

    process            
    {
        Write-Host 
          "DoesTFSWorkspaceExist -- workspace: $Workspace";
        $ProcessStartInfo = New-Object 
             System.Diagnostics.ProcessStartInfo;
        $ProcessStartInfo.FileName = $gtfUtility;
        $ProcessStartInfo.RedirectStandardError = $True;
        $ProcessStartInfo.RedirectStandardOutput = $True;
        $ProcessStartInfo.UseShellExecute = $False;
        # we are invoking: tf workspaces
        $ProcessStartInfo.Arguments = "workspaces";
        $process = New-Object System.Diagnostics.Process;
        $process.StartInfo = $ProcessStartInfo;
        # ignore return value with Out-Null
        $process.Start() | Out-Null; 
        $process.WaitForExit();
        $ExitCode = $process.ExitCode;
        if ($ExitCode -ne 0)
        {
            $ErrorText = $process.StandardError.ReadToEnd();
            Write-Host "Error: $ErrorText.";

            throw $ErrorText;
        }

        $stdout = $process.StandardOutput;
        while ($True)
        {
          $line = $stdout.ReadLine();
          # check for null
          if (!$line)
          {
              break;
          }

          if ($line.ToUpper().StartsWith($Workspace.ToUpper()))
          {
                Write-Host "Workspace exists: $Workspace";
                return $True;
          }
        }

        return $False;
    }
}

The DoesTFSWorkspaceExist function is invoked by TFSGetLabel as follows where the action is indicated by the line in boldface:

Write-Host 'TFSGetLabel:SourceFolder $SourceFolder';
Write-Host 'TFSGetLabel:Label $Label';            
DeleteDirectory $SourceFolder;
$DirectoryInfo = [System.IO.Directory]::CreateDirectory(
                                             $SourceFolder);
[System.IO.Directory]::SetCurrentDirectory($SourceFolder);
$cwd = [System.IO.Directory]::GetCurrentDirectory();
Write-Host "CWD: $cwd";            
# protect against space in name
$SourceFolder = '"' + $SourceFolder + '"'; 
# protect against space in name
$Collection = '"' + $Collection + '"'; 
$Label = '"' + $Label + '"';
if (DoesTFSWorkspaceExist($WorkSpace))
{
    # e.g. tf workspace /delete WorkSpaceATest /noprompt
    $commandLine = 'workspace /delete ' + $WorkSpace + 
                                               ' /noprompt';
    InvokeTF $commandLine $SourceFolder;
}

The code following DoesTFSWorkspaceExist deletes the work space (if it exists) using tf workspace /delete. which is invoked by the InvokeTF function.


function InvokeTF
{
    [CmdletBinding()]
    param            
    (   
        [parameter(Mandatory=$true)]                              
        [ValidateNotNullOrEmpty()]             
        [String] $CommandLine,

        [parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]             
        [String] $WorkingDirectory
    )

    process            
    {
        Write-Host "Working Directory: $WorkingDirectory";
        Write-Host "Invoking: $gtfUtility $commandLine";
        $ProcessStartInfo = New-Object 
                         System.Diagnostics.ProcessStartInfo;
        $ProcessStartInfo.FileName = $gtfUtility;
        $ProcessStartInfo.RedirectStandardError = $True;
        $ProcessStartInfo.RedirectStandardOutput = $False;
        $ProcessStartInfo.UseShellExecute = $False;
        $ProcessStartInfo.Arguments = $CommandLine;
        $process = New-Object System.Diagnostics.Process;
        $process.StartInfo = $ProcessStartInfo;
        # ignore return value with Out-Null
        $process.Start() | Out-Null; 
        $process.WaitForExit();
        $ExitCode = $process.ExitCode;
        if ($ExitCode -ne 0)
        {
            $ErrorText = $process.StandardError.ReadToEnd();
            Write-Host "Error: $ErrorText.";

            throw $ErrorText;
        }
    }
}

The InvokeTF function calls the TF utility using the command-line supplied by the aptly named $CommandLine parameter. The TF utility is run in the same working directory as the source code folder. The source code folder is passed in as the parameter $WorkingDirectory.

The code that called InvokeTF passed as a parameter with the command-line required to delete a work space (workspace /delete <workspacename> /noprompt) which is demonstrated below:

if (DoesTFSWorkspaceExist($WorkSpace))
{
    # e.g. tf workspace /delete WorkSpaceATest /noprompt
    $commandLine = 'workspace /delete ' + $WorkSpace + 
                                               ' /noprompt';
    InvokeTF $commandLine $SourceFolder;

}

6) Create the TFS work space 

The TFSGetLabel function contains the code to handle the work space delete, work space creation, folder to work space mapping and getting the label. The work space creation is highlighted below in boldface:

# e.g. tf workspace /new WorkSpaceATest /noprompt
$commandLine = 'workspace /new ' + $WorkSpace + ' /noprompt';
InvokeTF $commandLine $SourceFolder;
# e.g. tf workfold /map $/ATest D:\ATest /WorkSpace:WorkSpaceATest
$commandLine = 'workfold /map $/' + $Collection + ' ' + $SourceFolder + ' /WorkSpace:' + $WorkSpace;
InvokeTF $commandLine $SourceFolder;
# e.g. tf get /version:LBTest01
# note the L in /Version:L means a label is specified
$commandLine = 'get /Version:L' + $Label + ' /noprompt';
InvokeTF $commandLine $SourceFolder;

The previously highlight code invokes  tf workspace /new <workspacename>.


7) Map the source code folder to the collection folder and associate this mapping with the work space

The TFSGetLabel function handling the folder to work space mapping is highlighted below in boldface:

# e.g. tf workspace /new WorkSpaceATest /noprompt
$commandLine = 'workspace /new ' + $WorkSpace + ' /noprompt';
InvokeTF $commandLine $SourceFolder;
# e.g. tf workfold /map $/ATest D:\ATest /WorkSpace:WorkSpaceATest
$commandLine = 'workfold /map $/' + $Collection + ' ' + $SourceFolder + ' /WorkSpace:' + $WorkSpace;
InvokeTF $commandLine $SourceFolder;
# e.g. tf get /version:LBTest01
# note the L in /Version:L means a label is specified
$commandLine = 'get /Version:L' + $Label + ' /noprompt';
InvokeTF $commandLine $SourceFolder;

The previously highlight code invokes:
tf workfold /map <collection> <folder> /Workspace:<workspacename>


8) Get label from TFS

The TFSGetLabel function handling retrieving the label is highlighted below in boldface:

# e.g. tf workspace /new WorkSpaceATest /noprompt
$commandLine = 'workspace /new ' + $WorkSpace + ' /noprompt';
InvokeTF $commandLine $SourceFolder;
# e.g. tf workfold /map $/ATest D:\ATest /WorkSpace:WorkSpaceATest
$commandLine = 'workfold /map $/' + $Collection + ' ' + $SourceFolder + ' /WorkSpace:' + $WorkSpace;
InvokeTF $commandLine $SourceFolder;
# e.g. tf get /version:LBTest01
# note the L in /Version:L means a label is specified
$commandLine = 'get /Version:L' + $Label + ' /noprompt';
InvokeTF $commandLine $SourceFolder;

The previously highlight code invokes:
tf get /map /Version:L<label name> /noprompt

The capital L after the /Version: option is crucial so that the label is interpreted as a label name and not a version number. 

Entire Script

The script in its entirety is as follows:

$gTFUtility = 'tf';
$gTFSWorkspaceNameLength = 64;
    
function DeleteDirectory
{
    [CmdletBinding()]
    param            
    (   
        [parameter(Mandatory=$true)]                                                               
        [ValidateNotNullOrEmpty()]             
        [String] $directory
  )
   
    process            
    {   
        if (![System.IO.Directory]::Exists($directory))
        {
            return ;
        }

        $files = @();
        $files += [System.IO.Directory]::GetFiles($directory);
        foreach ($file in $files)
        {
            $attributes = [System.IO.File]::GetAttributes($file);
            if ($attributes -band 
                    [System.IO.FileAttributes]::ReadOnly)
            {
                $attributes = [int]$attributes - 
                   [int][System.IO.FileAttributes]::ReadOnly;
                [System.IO.File]::SetAttributes($file, 
                                                $attributes);
            }
            
            # Write-Host "[System.IO.File]::Delete($file)";
            [System.IO.File]::Delete($file);        
        }
        
        $directories = @();
        $directories += 
           [System.IO.Directory]::GetDirectories($directory);
        foreach ($subDirectory in $directories)
        {
            DeleteDirectory $subDirectory;
        }
        
        # Write-Host "[System.IO.Directory]::Delete($directory)";
        [System.IO.Directory]::Delete($directory);
    }
}

function DoesTFSWorkspaceExist
{
    [CmdletBinding()]
    param            
    (   
        [parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]             
        [String] $Workspace
    )

    process            
    {
        Write-Host 
            "DoesTFSWorkspaceExist -- workspace: $Workspace";
        $ProcessStartInfo = New-Object 
                    System.Diagnostics.ProcessStartInfo;
        $ProcessStartInfo.FileName = $gtfUtility;
        $ProcessStartInfo.RedirectStandardError = $True;
        $ProcessStartInfo.RedirectStandardOutput = $True;
        $ProcessStartInfo.UseShellExecute = $False;
        # we are invoking: tf workspaces
        $ProcessStartInfo.Arguments = "workspaces";
        $process = New-Object System.Diagnostics.Process;
        $process.StartInfo = $ProcessStartInfo;
        # ignore return value with Out-Null
        $process.Start() | Out-Null; 
        $process.WaitForExit();
        $ExitCode = $process.ExitCode;
        if ($ExitCode -ne 0)
        {
            $ErrorText = $process.StandardError.ReadToEnd();
            Write-Host "Error: $ErrorText.";

            throw $ErrorText;
        }

        $stdout = $process.StandardOutput;
        while ($True)
        {
          $line = $stdout.ReadLine();
          # check for null
          if (!$line)
          {
              break;
          }

          if ($line.ToUpper().StartsWith($Workspace.ToUpper()))
          {
                Write-Host "Workspace exists: $Workspace";
                return $True;
          }
        }

        return $False;
    }
}

function InvokeTF
{
    [CmdletBinding()]
    param            
    (   
        [parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]             
        [String] $CommandLine,

        [parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]             
        [String] $WorkingDirectory
    )

    process            
    {
        Write-Host "Working Directory: $WorkingDirectory";
        Write-Host "Invoking: $gtfUtility $commandLine";
        $ProcessStartInfo = New-Object 
                   System.Diagnostics.ProcessStartInfo;
        $ProcessStartInfo.FileName = $gtfUtility;
        $ProcessStartInfo.RedirectStandardError = $True;
        $ProcessStartInfo.RedirectStandardOutput = $False;
        $ProcessStartInfo.UseShellExecute = $False;
        $ProcessStartInfo.Arguments = $CommandLine;
        $process = New-Object System.Diagnostics.Process;
        $process.StartInfo = $ProcessStartInfo;
        # ignore return value with Out-Null
        $process.Start() | Out-Null; 
        $process.WaitForExit();
        $ExitCode = $process.ExitCode;
        if ($ExitCode -ne 0)
        {
            $ErrorText = $process.StandardError.ReadToEnd();
            Write-Host "Error: $ErrorText.";

            throw $ErrorText;
        }
    }
}

function TFSGetLabel
{
    [CmdletBinding()]
    param            
    (   
        [parameter(Mandatory=$true)]                                                               
        [ValidateNotNullOrEmpty()]             
        [String] $SourceFolder,

        [parameter(Mandatory=$true)]                                                               
        [ValidateNotNullOrEmpty()]             
        [String] $WorkSpace,

        [parameter(Mandatory=$true)]                                                               
        [ValidateNotNullOrEmpty()]             
        [String] $Collection,

        [parameter(Mandatory=$true)]                                                               
        [ValidateNotNullOrEmpty()]             
        [String] $Label
)
    process            
    {   
        try            
        {
            Write-Host 'TFSGetLabel:SourceFolder $SourceFolder';
            Write-Host 'TFSGetLabel:Label $Label';            
            DeleteDirectory $SourceFolder;
            $DirectoryInfo = 
                 [System.IO.Directory]::CreateDirectory(
                                      $SourceFolder);
            [System.IO.Directory]::SetCurrentDirectory(
                       $SourceFolder);
            $cwd = [System.IO.Directory]::GetCurrentDirectory();
            Write-Host "CWD: $cwd";            
            # protect against space in name
            $SourceFolder = '"' + $SourceFolder + '"'; 
            # protect against space in name
            $Collection = '"' + $Collection + '"'; 
            $Label = '"' + $Label + '"';
            if (DoesTFSWorkspaceExist($WorkSpace))
            {
                # e.g. tf workspace /delete WorkSpaceATest /noprompt
                $commandLine = 'workspace /delete ' + 
                     $WorkSpace + ' /noprompt';
                InvokeTF $commandLine $SourceFolder;
            }

            # e.g. tf workspace /new WorkSpaceATest /noprompt
            $commandLine = 'workspace /new ' + $WorkSpace + 
                                 ' /noprompt';
            InvokeTF $commandLine $SourceFolder;
            # e.g. tf workfold /map $/ATest D:\ATest 
            # /WorkSpace:WorkSpaceATest
            $commandLine = 'workfold /map $/' + $Collection + 
               ' ' + $SourceFolder + ' /WorkSpace:' + $WorkSpace;
            InvokeTF $commandLine $SourceFolder;
            # e.g. tf get /version:LBTest01
            # note the L in /Version:L means a label is specified
            $commandLine = 'get /Version:L' + $Label + 
                            ' /noprompt';
            InvokeTF $commandLine $SourceFolder;
        }            
        catch
        {
            Write-Error "Build Error $SolutionFilePath, $_";
            Exit 1;     
        }
    }
}

function CleanWorkspaceName
{
    [CmdletBinding()]
    param            
    (            
        [parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]             
        [string] $WorkspaceName
    )
        
    process
    {
        # Add computer name to work space to aid in debugging.
        # Remove trailing and leading spaces with Trim
        $WorkspaceName = $env:computername + 
                                 $WorkspaceName.Trim(); 
        # invalid workspace name characters: /:<>\|*?;
        $WorkspaceName = $WorkspaceName.Replace('/', '');
        $WorkspaceName = $WorkspaceName.Replace(':', '');
        $WorkspaceName = $WorkspaceName.Replace('<', '');
        $WorkspaceName = $WorkspaceName.Replace('>', '');
        $WorkspaceName = $WorkspaceName.Replace('\', '');
        $WorkspaceName = $WorkspaceName.Replace('|', '');
        $WorkspaceName = $WorkspaceName.Replace('*', '');
        $WorkspaceName = $WorkspaceName.Replace('?', '');
        $WorkspaceName = $WorkspaceName.Replace(';', '');
        $WorkspaceName = $WorkspaceName.Replace('.', '');
        $WorkspaceName = $WorkspaceName.Replace(',', '');
        # change spaces to underscore
        $WorkspaceName = $WorkspaceName.Replace(' ', '_'); 
        if ($WorkspaceName.Length -gt $gTFSWorkspaceNameLength)
        {
            $WorkspaceName = $WorkspaceName.Substring(0, 
                               $gTFSWorkspaceNameLength);
        }

        return $WorkspaceName;
}
}

function GetCode
{
    [CmdletBinding()]
    param            
    (            
        [parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]             
        [string] $TFSDiskLocation,

        [parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]             
        [string] $Collection,

        [parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]             
        [string] $Label
    )
        
    process
    {
        $Workspace = $Collection;
        $Workspace = CleanWorkspaceName $Workspace;
        TFSGetLabel $TFSDiskLocation $Workspace 
                           $Collection $Label;
}
}

$TFSDiskLocation = 'C:\MarkI\Inventory App';
$Collection = 'MarkI\Inventory App';
$Label = 'InventoryApp1.2.3.4';

GetCode $TFSDiskLocation $Collection $Label;