Friday, May 9, 2008

Voyages with BlackPearl - 5. Environment Variables and Deployment

I have to admit that I just don’t understand Environments in K2 [blackpearl]. The first time I wanted to create an Environment variable of my own I did what I guess most people would do and added it to the Development environment. I didn’t think it through. My mistake became obvious when I wanted to deploy it to a Test environment on a customer site. The Test environment was just like Production and did not have Visual Studio installed, which is fine because that is what the MSBUILD files are for.

If you’re not familiar with these files they are created when you use ‘Deploy’ from Visual Studio. A new directory is created under ‘obj\Debug’ called Deployment and in there you will see the .msbuild file and your compiled workflow(s) and ExtenderProjects in the Bin directory. So if your Test server doesn’t have Visual Studio installed you just need to copy the contents of the Deployment directory over to the server and then use the MSBUILD.EXE to do the deployment. MSBUILD.EXE is a utility found in the directory C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727 so you may want to add this path to the Environment Settings of the server.

The syntax for deployment is as follows:
MSBUILD.EXE [k2proj].msbuild /p:Environment=[environment]

For example
MSBUILD MyK2Proj.msbuild /p:Environment=Development

Now what the /p:Environment switch is doing is determining which settings to use within the .msbuild file. You need to open the .msbuild file to understand what is going on.
Under the root node ‘Project’ there is a collection of nodes which look like this:
[PropertyGroup condition="$(Environment) == 'Development'"]
Beneath that is the [EnvironmentFields] node which contains all of the [Fields] in the template.

When the .msbuild file was created it created an Environment node for all of the environments on the server (the two defaults are Development and Production) and then you use the /p:Environment switch to tell MSBUILD which node to use.

But when you execute the command it will fail and that is because MSBUILD uses one of the built-in connection strings (I think it’s the ‘Workflow Server’ one) to make a connection to the K2 server. It fails because the Test environment has a different K2 Server name and if you are not using Integrated authentication it has a different Administrator account, a different domain and/or a different password.

The only solution is to fix the connection strings in the Environment fields to match what they should be in this Test environment. If you are doing it manually then you can copy the values from the K2 Workspace. I wanted to automate the process so I wrote some utilities that will parse the XML and replace each of the attributes (K2 Server, Domain etc) with the correct values (see below).
I call these utilities within an MSI file but I’ll describe that in more detail in the next post.

It was about this time I realised my mistake. I tend to use the same development image for several projects and I realised the .msbuild file contained all the Environment variables that I had added, not just the ones for this project. This will get in a right mess because you won’t be able to tell which environment variables apply to which workflow. What about if you have two workflow processes that each require different ‘FromEmailAddress’ values?

The approach I now adopt is to totally ignore the default Environment libraries and create a brand new library of the same name as my K2PROJ file. That way I don’t get confused with the environment variables. I toyed with the idea of creating development, test and production versions but in the end decided I would have just one environment library per project and amend it for each target server.

The other thing to note about the MSBUILD deployment is what happens when you do successfully deploy the workflows using your custom Environment library. You go to the K2 workspace, open the K2 Server and open the Environment Library section. You drill down to Templates, Default Template and Environments and, GULP!, your newly created Environment library is not there. No worries because you don’t need a template library. The template is just a step towards creating the String Table and it’s the String Table that the workflow uses. I get so used to looking at Environments that I realised I was looking in the wrong place. The String Table is nestled away under the Process.

The two utilties are below (note the first is an extract, just repeat the section that I’ve marked).
In order to call them I load the .msbuild file into an XmlDocument using the Load method then pass the XmlDocument to the functions. I then use the Save method to save it back to its original location.

I’m still not sure that I understand Environments but I have something that works for me.
// replaces values in connection string
public XmlDocument ModifyMSBuild(XmlDocument xmldom, string Environment, string OldDomain, string NewDomain, string OldK2Server, string NewK2Server,string OldAdminAccount, string NewAdminAccount, string OldPassword, string NewPassword)
{
try
{
XmlNodeList nodeList = null;
XmlNode node = null;

node = xmldom.SelectSingleNode("//Root[@Name='" + Environment + "']");
// Replace the Domain Name
nodeList = node.SelectNodes("./Field[contains(@Value,'" + OldDomain + "')]");
string strAttribValue = "";
foreach (XmlNode domNode in nodeList)
{
string x = domNode.OuterXml;
if (domNode.Attributes.Count > 0)
{
strAttribValue = domNode.Attributes["Value"].Value.ToString();
{
strAttribValue = strAttribValue.Replace(OldDomain, NewDomain);
domNode.Attributes["Value"].Value = strAttribValue;
}
}
}
// repeat for the Host Server, Admin Account and Password like this:
// nodeList = node.SelectNodes("./Field[contains(@Value,'" + OldK2Server + "')]");
// foreach loop again

return xmldom;
}
catch (Exception ex)
{
// log.WriteToLog(ex.Message, "CustomError");
return xmldom;
}
}

// updates values in a specific environment variable
public XmlDocument ModifyEnvironmentField(XmlDocument xmldom, string Environment, string EnvironmentField, string newValue)
{
try
{
XmlNode nodeField = null;
XmlNode node = null;

node = xmldom.SelectSingleNode("//Root[@Name='" + Environment + "']");

nodeField = node.SelectSingleNode("./Field[@Name= '" + EnvironmentField + "']");
if (nodeField != null)
{
nodeField.Attributes["Value"].Value = newValue;
}

return xmldom;
}
catch (Exception ex)
{
// log.WriteToLog(ex.Message, "CustomError");
return xmldom;
}

}

1 comment:

Dan Shookowsky said...

thanks for the post...it was news to me