Enforcing Policy with Microsoft Visual Studio Team System 2008 Team Foundation Server

Ryan Frazier

Microsoft Corporation

December 2007

Applies to:

   Microsoft Visual Studio Team System 2008 Team Foundation Server

   Microsoft Visual Studio 2005 Team Foundation Server

Summary: This article describes a mechanism for policy deployment and versioning (for enterprise-development organizations that might consist of hundreds or thousands of development workstations), and outlines a sample solution that might be used to enforce policy. (16 printed pages)

Contents

Introduction

A Deployment Strategy

A Look at Automated Policy Deployment

Workflow at the Application Tier of Team Foundation Server

The Deployment Service

The Client

Conclusion

Introduction

A common requirement for a development organization is the ability to enforce policy on a developer’s workstation that manipulates the workflow that is involved with integrating source-control changes with the source-control store. Such policy might enforce coding standards, conformance to organizational procedures, or even elaborate rule-driven workflow.

Microsoft Visual Studio Team System 2008 Team Foundation Server (along with Microsoft Visual Studio 2005 Team Foundation Server) provides a mechanism to enforce such policy by the use of custom check-in policies. Check-in policies prevent developers from checking in code to the source-control tree unless that code passes applicable, enabled policies. Team Foundation Server ships with a base set of policies, and additionally allows developers to create and configure custom policies that can be executed in the source-control environment.

Moreover, the larger the development organization is, the larger the need to include governance of the policies themselves into the implementation. Topics of concern might include how to manage the life cycle of an individual policy or how to enforce the policy on each developer workstation.

A Deployment Strategy

There are two interfaces that are implemented in a custom check-in policy in order to plug in and communicate with the policy framework in Team Foundation Server. The first interface, IPolicyDefinition, provides the necessary metadata for configuring the policy in the Check-In Policy tab of the Source Control Settings dialog box in the Microsoft Visual Studio Team Explorer (see Figure 1). The second interface, IPolicyEvaluation, is called to perform the actual evaluation of the policy on the client workstation. In the event that a policy is enabled for a team project in Team Foundation Server, but the client workstation does not contain the policy installation, the policy is considered as failed, and an attempt to check in will therefore fail.

Bb980626.0e1a4fce-fbcd-4aec-bf02-c64f415f6c81(en-us,VS.90).jpg

Figure 1. Source Control Settings dialog box in Team Explorer

For that reason, a robust mechanism for policy deployment and versioning might be needed for enterprise-development organizations that potentially consist of hundreds or thousands of development workstations. The following article describes one such mechanism and outlines a sample solution that might be used to enforce policy. In the sample solution, out-of-box Team Foundation Server functionality was utilized wherever possible in order to leverage existing functionality and tools, as well as to minimize the custom development and maintenance surface.

A Look at Automated Policy Deployment

Given that policy implies mandatory execution, a manual (that is, user-initiated) request and installation of enabled policies could potentially be a futile strategy. On the other hand, a mechanism that automatically requests and installs enabled policies on a workstation would satisfy the implicit obligation of a policy—that is, to be enforced.

The topology that is indicated in Figure 2 is a multipart client-and-server sample solution to provide automated detection, installation, and versioning of custom policies.

Bb980626.8a5dcff6-72e2-4d20-8ca9-c6c587a2cd83(en-us,VS.90).png

Figure 2. Sample-solution topology

Workflow at the Application Tier of Team Foundation Server

Work-item tracking, a feature of Team Foundation Server, provides flexible and customizable workflow for tracking conventional units of work (tasks, bugs, and so forth), along with the necessary extensibility from which to create deployment and versioning work-item types and related workflow.

Additionally, work-item types and their workflow can be localized to a specific team project. An extensive discussion on team-project concepts is beyond the scope of this article. However, suffice it to say that a team project serves as a logical container of linked elements within an overall unit of work: a major software release or maintenance project to enhance a specific feature set of a prior software release. It also serves as the logical boundary between typically unrelated elements that are contained within separate overall units of work.

In this sample solution, a separate team project is utilized out-of-band from development team projects in order to centralize work-item types, workflow, and security for deliverables that are related to policy. A real-world example might involve a cross-functional cross-section of IT members who are involved with application life-cycle management (project management, release management, architecture, development, test, and so forth) and who utilize a single team project to collaborate on organizational standards and the methods by which to enforce those standards.

Custom work-item types can be created to handle separate workflows—in this case, a Check-In Policy type. This custom work-item type—which is local to the team project in which it is created—contains all of the custom workflow and state that are associated with creating, approving, developing, testing, and releasing a policy into the organization.

The policy workflow in this example includes the development of custom check-in policy components; therefore, the source-control tree in the containing team project is a logical place to store the code. However, the retail binary of the policy must be deployed to development workstations. Enter work-item attachments.

Typically, attachments are used to link related files to individual work items. There are no restrictions on the type of file (for example, document, picture, executable, or MSI package). In this case, our retail binary (or an installation executable or MSI package) is added as an attachment as part of the manual workflow when Check-In Policy work items are ready to be released to production. Adding the retail binary as an attachment to the work item could even be automated as part of the check-in process or an automated build, if desired.

At this point, we should have a space to collaborate on and develop custom check-in policies for the organization. Additionally, we have created a custom workflow that will track the life cycle of our policies. Finally, we have exposed our policy binaries where they can be manipulated via the Team Foundation Server SDK API for use in deployment.

The Deployment Service

In this example, we will utilize Microsoft Windows Communication Foundation (WCF) to provide an application service on top of Team Foundation Server. The service interface will provide the ability for clients (for example, development workstations) to install policies (and any other items) from the application tier of Team Foundation Server. WCF was chosen as the technology to implement the service in order to provide multiplatform support with .NET, Java, and other client codebases. Although the client that is discussed later in this article would require a separate implementation if utilized outside of Team Foundation Server, the WCF service implementation could be reused.

As with any service-oriented relationship, we need a formal agreement between the service provider and the consumer that abstractly describes the data to be exchanged: a data contract. This contract precisely defines, for each parameter and/or return type, what data is transmitted across the wire in order to receive client updates. In our case, we want the contact to be platform-neutral—that is, not coupled to a specific technology—in order to have a broader client reach (see Listing 1).

/// <summary>
/// Represents an individual deployable package that consist of 1:N updates
/// </summary>
[DataContract]
public class DeploymentPackage
{
    private List<ClientUpdate> _updates;

    public DeploymentPackage() { }

    public DeploymentPackage(ClientUpdate[] updates)
    {
        _updates = new List<ClientUpdate>(updates);
    }

    [DataMember]
    public ClientUpdate[] AvailableUpdates
    {
        get
        {
            if (_updates != null)
            {
                ClientUpdate[] dest = new ClientUpdate[_updates.Count];
                _updates.CopyTo(dest, 0);
                return dest;
            }
            return null;
        }
        set
        {
            _updates = new List<ClientUpdate>(value);
        }
    }

    public void Add(ClientUpdate item)
    {
        if (_updates == null)
            _updates = new List<ClientUpdate>();
        _updates.Add(item);
    }

    public bool Remove(ClientUpdate item)
    {
        if (_updates == null || _updates.Count == 0)
            return false;
        else
            return _updates.Remove(item);
    }
}
/// <summary>
/// Represents a collection of updates, all part of a work item
/// </summary>
[DataContract]
public class ClientUpdate
{
    private List<Update> _updateItems;
    private int _workItemId;

    public ClientUpdate(int workItemId)
    {
        _workItemId = workItemId;
    }

    public ClientUpdate(int workItemId, Update[] items)
        : this(workItemId)
    {
        _updateItems = new List<Update>(items);
    }

    [DataMember]
    public int WorkItemId
    {
        get { return _workItemId; }
        set { _workItemId = value; }
    }

    [DataMember]
    public Update[] Binaries
    {
        get
        {
            if (_updateItems != null)
            {
                Update[] dest = new Update[_updateItems.Count];
                _updateItems.CopyTo(dest, 0);
                return dest;
            }
            return null;
        }
        set
        {
            _updateItems = new List<Update>(value);
        }
    }

    public void Add(Update item)
    {
        if (_updateItems == null)
            _updateItems = new List<Update>();
        _updateItems.Add(item);
    }

    public bool Remove(Update item)
    {
        if (_updateItems == null || _updateItems.Count == 0)
            return false;
        else
            return _updateItems.Remove(item);
    }
}

/// <summary>
/// Represents an individual installable update, part of a work item
/// </summary>
[DataContract]
public class Update
{
    private byte[] _content;
    private string _name;
    private string _fileName;
    private string _extension;

    public Update(string name, string fileName, string extension, byte[] content)
    {
        _name = name;
        _fileName = fileName;
        _content = content;
        _extension = extension;
    }

    [DataMember]
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }

    [DataMember]
    public string FileName
    {
        get { return _fileName; }
        set { _fileName = value; }
    }

    [DataMember]
    public string Extension
    {
        get { return _extension; }
        set { _extension = value; }
    }

    [DataMember]
    public byte[] Content
    {
        get { return _content; }
        set { _content = value; }
    }
}

Listing 1. Data contract for deployment packages

The sample data contract gives us the ability to represent each downloadable item (a policy, in our case) separately within a package of updates. As soon as we have defined our contract, we must focus on the implementation of the service and what information is required from each client in order to package available updates.

By utilizing the Team Foundation Server’s work-item tracking framework, we automatically have a unique identifier for each policy that has been released to production: the work-item ID. We’ll discuss the client implementation shortly; but, for now, assume that the client maintains the state of the latest policies that have been installed. With that said, the client can send the unique work-item ID of the last package of updates that were applied, and the service can utilize the ID to query the work-item store (see Listing 2) when determining if any updates are required (for example, a new policy released to production).

[ServiceContract]
public interface IDeploymentService
{
    [OperationContract()]
    DeploymentPackage RequestUpdates(string teamFoundationServer, int lastSuccessfulWorkItemId);
}

public class DeploymentService : IDeploymentService
{
    public DeploymentPackage RequestUpdates(string teamFoundationServer, 
int lastSuccessfulWorkItemId)
    {
        bool addToPackage = false;

        // Connect to the team foundation server and hook up the work item store
        // Using the TeamFoundationServerFactory will cache the instance after we connect
        // for usage on future requests
        TeamFoundationServer server = 
TeamFoundationServerFactory.GetServer(teamFoundationServer);
        server.EnsureAuthenticated();
        WorkItemStore workItems = (WorkItemStore)server.GetService(typeof(WorkItemStore));


// Set up the query to get all work items from our collaboration project that are of the 
// CheckInPolicy
        // work item type and are in an appropriate state to be considered for download
        Dictionary<string, object> queryContext = new Dictionary<string, object>();
        queryContext.Add("TeamProject", "PolicyCollaborationProject"); //
        queryContext.Add("WorkItemType", "CheckInPolicy");
        queryContext.Add("LastApplied", lastSuccessfulWorkItemId);
        queryContext.Add("AvailableWorkItemState", "Released");


        // Create and execute the work item query
        Query workItemQuery = new Query(workItems, "SELECT System.Id, System.Title FROM WorkItems" +
                                                   " WHERE System.TeamProject = @TeamProject" +
                                                   " AND System.WorkItemType = @WorkItemType" +
                                                   " AND System.Id > @LastApplied" +
                                                   " AND System.State = @AvailableWorkItemState" +
                                                   " ORDER BY System.Id ASC", queryContext);
        ICancelableAsyncResult asyncResult = workItemQuery.BeginQuery();
        WorkItemCollection availableUpdates = workItemQuery.EndQuery(asyncResult);

        // Check the results and package them for delivery to the client
        if (availableUpdates != null)
        {
            WebClient downloadUtility = new WebClient();
            downloadUtility.Credentials = server.Credentials;
            DeploymentPackage package = new DeploymentPackage();
            
            // Package each work item as a separate client update within the deployment package
            foreach (WorkItem item in availableUpdates)
            {
                ClientUpdate update = null;
                addToPackage = false;
                foreach (Attachment file in item.Attachments)
                {
                    // Do a little validation here
                    if (string.Compare("EXE", file.Extension.TrimStart(new char[] { '.' }), StringComparison.InvariantCultureIgnoreCase) == 0)
                    {
                        // Download the file using the WebClient
                        if (update == null) update = new ClientUpdate(item.Id);
                        update.Add(new Update(file.Comment, file.Name, file.Extension, downloadUtility.DownloadData(file.Uri)));
                        addToPackage = true;
                    }
                }
                if (addToPackage) package.Add(update);
            }
            return package;
        }
        return null;
    }
}

Listing 2. WCF deployment service contract and implementation

This approach prevents the service implementation from maintaining any state regarding client installations, which could potentially become quite cumbersome. Additionally, the service implementation is stateless, delegates the majority of work to the Team Foundation Server API, and can scale-out with the application tier of Team Foundation Server.

The Client

To close the loop on this sample solution, we must have a client component that:

  1. Checks for available updates at well-defined events.
  2. Manages the installation of any updates.
  3. Restarts the client environment for necessary updates to take effect.

Microsoft Visual Studio provides multiple integration options with which to create the client component. One extensibility option would be to create an automation add-in that loads when Visual Studio starts. However, add-ins can be disabled; therefore, the requirement for policy enforcement can be circumvented. With that said, a Visual Studio Package (VSPackage) would be the preferred approach.

A VSPackage is a software module that extends the Visual Studio integrated development environment (IDE) by providing UI elements, services, projects, editors, and/or designers. It can also be a unit of deployment, licensing, and security. However, in order for Visual Studio to load a release VSPackage, a valid package load key (PLK) is required. (You can obtain a PLK from the Microsoft Visual Studio Industry Partner (VSIP) Web site.)

With either approach, installation of our client component onto the developer workstation should be performed via a policy-based installation mechanism, such as Microsoft System Center Configuration Manager 2007.

After the integration approach has been chosen, we can focus on the client logic, which essentially must hook into the Team Explorer client, and then communicate to the deployment service at specific state changes. In order to wire-up the client with the Team Explorer, we’ll use the TeamFoundationServerExt class, which is part of the Microsoft.VisualStudio.TeamFoundation namespace. (See the Team Foundation Server SDK for examples on how to retrieve an instance of this object from within the Visual Studio shell.)

As soon as we have the object, we can subscribe for notifications that tell us when a developer has changed the active team-project context in Team Explorer. We can then retrieve the new Team Foundation server and team project to which they have connected. These notifications automatically fire when the Team Explorer first loads and subsequently thereafter, so that we always have up-to-date information. In the event that the active team project changes, we will communicate with our deployment service provider to detect if any updates are required, based on the new server and/or team project.

Requesting Updates

For the solution to perform accurate detection of updates, the client must communicate to the server some form of persisted state. As previously mentioned, this state will be the work-item ID of the last successfully installed package of updates (or nothing, if it is a first connection attempt). Additional information could be utilized, depending on the requirements of the organization, to specify context (that is, team project) that accurately and uniquely identifies the state of the client. For instance, separate development organizations might own separate Team Foundation Server application tiers and potentially separate policy mandates. The ID might be stored at any uniform location on the client file system, and it might be secured (that is, encrypted) on disk if an additional level of obfuscation is required.

This state is then communicated to the deployment service when update detection occurs. If updates are received by the client, the client has the responsibility of installing them on the local workstation, and should also log audit information of failed installs for later troubleshooting.

Now, the client has the responsibility of managing the installation; yet, the implementation might vary. Updates can consist of new binaries or a new version of existing binaries. It would be prudent for both the Check-In Policy workflow that was mentioned earlier and the client-installation logic to participate in and/or share the responsibility of validating installation bits. For example, if the client component utilizes a System.Diagnostics.Process object to kick-off an executable file that performs the installation, it might be beneficial for the server-side workflow (mentioned previously) to prevent the work item from moving into a release state if invalid file types are part of the attachment list. Alternatively, the work-item query that scans for updates as part of the client request to the deployment service could be tied to a client-installer type (again, part of that client state) and select only attachments that are valid for the specific installer.

Listing 3 shows an example of client-side validation that uses a System.Diagnostics.Process object to kick-off an executable installer.

private static void OnErrorDataReceived(object sender, DataReceivedEventArgs args)
{
    // Audit the error information produced by a failed update
}

public void CheckForUpdates(string teamFoundationServer, int lastSuccessfulWorkItemId)
{
    IDeploymentService client = null;
    int currentWorkItemId = lastSuccessfulWorkItemId;

    try
    {
        bool updateApplied = false;

        // Create our WCF client proxy channel
        ChannelFactory<IDeploymentService> factory = new ChannelFactory<IDeploymentService>();
        client = factory.CreateChannel();

        // Check if an update is available
        DeploymentPackage package = client.RequestUpdates(teamFoundationServer, lastSuccessfulWorkItemId);

        // If we received any updates from the deployment service, apply them
        if (package != null && package.AvailableUpdates != null && package.AvailableUpdates.Length > 0)
        {
            foreach (ClientUpdate update in package.AvailableUpdates)
            {
                updateApplied = false;
                foreach (Update item in update.Binaries)
                {
                    // ...
                    // Copy the file to disk using the properties on the Update class (code not shown)
                    // ...

                    // Execute the package and wait for a valid completion code
                    using (Process process = new Process())
                    {
                        process.StartInfo.FileName = Path.GetTempPath() + @"\" + item.FileName; // This assumes the file was copied to the process temp path
                        process.StartInfo.CreateNoWindow = true;
                        process.StartInfo.UseShellExecute = false;
                        process.StartInfo.RedirectStandardError = true;
                        process.ErrorDataReceived += new DataReceivedEventHandler(OnErrorDataReceived);
                        process.Start();
                        process.BeginErrorReadLine();
                        process.WaitForExit();
                        process.Refresh();
                        updateApplied = process.ExitCode == 0;
                    }

                    // If this portion of the update was not successfully applied, break out of this update
                    if (!updateApplied)
                        break;

                }
                if (updateApplied)
                {
                    currentWorkItemId = update.WorkItemId;
                }
            }
        }

        // Close our Deployment service connection
        ((IClientChannel)client).Close();
    }
    catch (TimeoutException timeEx)
    {
        // Audit then abort

        ((IClientChannel)client).Abort();
    }
    catch (FaultException unknownFaultEx)
    {
        // Audit then abort

        ((IClientChannel)client).Abort();
    }
    catch (CommunicationException commEx)
    {
        // Audit then abort

        ((IClientChannel)client).Abort();
    }
    catch (Exception ex)
    {
        // Audit then abort

        ((IClientChannel)client).Abort();
    }
    finally
    {
        if (currentWorkItemId != lastSuccessfulWorkItemId)
        {
            // If we successfully applied any updates we want to update the local state of the client
            // to reflect the new work item id of the last successfully-applied update.
        }
    }
}

Listing 3. Client-side installation

The next step in the process is to update the local state of the client per the updates that were installed, or to provide visual feedback to the developer if updates failed. In the event that updates failed to install, the state should not be modified; therefore, the client will receive the same updates at the next request—presumably, after the issue that caused the installation to fail has been resolved.

Finally, if any updates are installed successfully, it might be advisable to require a restart of Visual Studio.

Conclusion

Microsoft Visual Studio Team System 2008 Team Foundation Server and custom check-in policies provide a flexible and extensible mechanism to enforce policy on development workstations. In larger organizations, structured process and governance might be required for managing the life cycle of the actual policy. Again, Team Foundation Server provides a solution through a combination of the extensible work-item tracking framework and integration with the Visual Studio IDE.