English – HoNoSoFt https://blog.honosoft.com Blog & Roll Mon, 02 Nov 2020 02:19:26 +0000 en-CA hourly 1 https://wordpress.org/?v=6.8.12 https://blog.honosoft.com/wp-content/uploads/2018/06/logo.png English – HoNoSoFt https://blog.honosoft.com 32 32 Observability of a platform with Instana https://blog.honosoft.com/2020/11/01/observability-of-a-platform-with-instana/?utm_source=rss&utm_medium=rss&utm_campaign=observability-of-a-platform-with-instana https://blog.honosoft.com/2020/11/01/observability-of-a-platform-with-instana/#respond Sun, 01 Nov 2020 19:56:33 +0000 https://blog.honosoft.com/?p=679 Continue Reading]]> Recently, I was investigating for a good APM solution. I came across many and tried only a few of them in the end. Instana was one of those solution that I wanted to go a bit deeper in my evaluation.

In this article, I will be using Azure Kubernetes Service (AKS) with a simple Dotnet core application. The Ingress controller is more or less the default one (Nginx Ingress Controller).

The flow below gives an overview of how everything gets pushed to Instana. Keep in mind that the instana-agent is also taking information from the infrastructure itself (Kubernetes, nodes, …) and do also some auto-discovery. The view below is quite simplified.

General idea of the flow

What is the meaning of APM?

APM stands for Application Performance Monitoring (or sometimes Management). Basically, it helps to understand how your platform is doing and help you investigates more deeply when an error occurs. This is a part of the observability of a platform.

If you wish to know more about observability, I suggest you to go read also the following articles:

Installation of Instana

As I was going to use it with a Kubernetes Infrastructure, I decided to follow the step by step from the website. It worked great. That being said, you should probably use the Helm 3 chart instead in order to make it easier for integration and update of the system. I discovered the Helm chart a bit after I installed it using the generated YAML from the website. Keep in mind that the repository is actually in the “general” helm repository and will be moved within the Instana site repository.

Once the installation completed, you will start seeing your entire infrastructure in Instana without having doing much. So the namespaces, pods, services, etc. will be displayed. In case you have a non dotnet service, you might as well be already seeing some details from your applications within the screens.

Sample of K8s auto-discovery (Top Status Bar)

Deploying/Using a Dotnet application

Basically, to get the things working, you simply have to follow the documentation on the site (Instana Doc). Basically, instead of adding a package in your Dockerfile, you use a library. The idea is pretty much the same, but at least it’s more predictable and it’s a no brainer.

NuGet:

  • Instana.Tracing.Core – Basic thing to add. This communicate with the agent on the infrastructure
  • Instana.Tracing.Core.Sdk – If you want to create custom spans and better tracing
  • Instana.Tracing.Core.Rewriter.Linux (Linux, not Alpine) – This is the package adding the tracing C library (.so) and creates a folder for some settings and all. Just use the default, it will work just fine. A different package exist for Alpine or other (see the doc.)

Probably, most people will use only the Core and the Rewriter in order to send the basic data. The SDK is if you wish to let’s say, add the CustomerId as part of information within the request tracing.

Once running, your dashboard will start receiving the requests:

Dashboard of FakeApp Service

You can also easily see the communication from your service to other services by having an automatic-dependency graph (Below you can see my 2 services interaction, if you had a database, it would also show):

Service dependency call graph

In Instana it’s also easy to see our performances metrics since they also show based on the services. You can also track the frontend application by adding a small snippet.

Instana also offer some automatic monitoring and generate alerts based on some metrics. Such alerts can be reconfigured. It can also send message to your MS Teams group/channel. Which mean, that you could even add some flow to your issues (Microsoft Flow/Power Automate).

Single agent per node

It’s important to note that we have 1 agent per node. So our application require to call our “actual” node agent in order to send the data. In Kubernetes you will probably have a deployment file with something similar to this:

        env:
        - name: INSTANA_AGENT_HOST
          valueFrom:
            fieldRef:
              fieldPath: status.hostIP

This is a different model than, let’s say, New-Relic. In New-Relic, we have 1 agent per host AND our containers (dotnet) will be also having an agent sending data directly to New-Relic.

Pricing model

The pricing model can be quite attractive for most company. In my case, it was not attractive enough since we would need agents on multiple nodes and server (on premise). If your infrastructure is well-known it will be easy to calculate the cost since it’s not based on the consumption of logs nor users. It is solely based on the number of hosts. So if you have a cluster of 5, you do 79$*5.

This mean that you won’t lose any logs and won’t be afraid to simply ship everything over there. That’s a big win if you compare to the complex pricing of DataDog.

Some UI bugs or improvement to be made at the moment of writing

(Bug) The “Live” update was not always working for me and I had to refresh the page. Maybe this was due to my browser, who knows. The refresh was also sometimes removing the live option and I had to push it back for some reason.

(Improvement) I was searching how to create a custom dashboard. The main dashboard is easy to find. It’s the first button in the menu on the left. However, once there, we need to click on the Instana button (top left) in order to see that we can create our own dashboard. I wish it was clearer to add a new dashboard. I must have skipped the welcome screen, so that’s why I didn’t notice at first.

Conclusion

Depending on your need, this solution could be a game changer in terms of pricing, observability and much more.

]]>
https://blog.honosoft.com/2020/11/01/observability-of-a-platform-with-instana/feed/ 0
How to have more fun with a SAFE Stack https://blog.honosoft.com/2020/03/17/how-to-have-more-fun-with-a-safe-stack/?utm_source=rss&utm_medium=rss&utm_campaign=how-to-have-more-fun-with-a-safe-stack https://blog.honosoft.com/2020/03/17/how-to-have-more-fun-with-a-safe-stack/#respond Tue, 17 Mar 2020 12:01:34 +0000 https://blog.honosoft.com/?p=649 Continue Reading]]> While watching some video from London NDC’20 on YouTube, some older video appeared within the suggestion box and I clicked on it due to my interest in the matter. Basically, it was a video talking about how to create a Office 365 Excel like F# |> Elmish |> React app. It’s quite interesting and the guy build something incredible in only 1 hour. (Link here)

By watching that video I started to search what he used and then stumbled on the official fable elmish repository, but also I found out an another one which is more to my liking and it was SAFE Stack (SAFE stands for Saturn Azure Fable and Elmish). A nice article have been written not long ago by Isaac Abhrams (MSDN Link), so I won’t try to repeat what he already wrote.

In this article, we will simply explore how to create your first application.

Why is it so interesting

Basically, if we use the SAFE Stack, everything is more or less working together. You write your back-end in F#, your front-end in F# and then you can share domain objects and basic functions between the two since all is compiled (or transpiled depending where you look at). If you run all within VS Code, it’s going to be a nice experience since you can save in back or front and then see the new results on the interface (back-end needs a page refresh in such a case or a new call to the server).

When putting breakpoints in the F# file, you can debug your front or backend directly. Also, if you know React JS and it’s plugin in Chrome, you know that you have a time machine for your front-end. It’s also working out of the box, so it’s great to debug or receive an “how to reproduce” script.

Requirements

  • NodeJs
  • Yarn
  • Dotnet Core 3.1
  • Crome with Redux extension (Didn’t work with Brave Browser due to some linking from VS Code)

More details at https://safe-stack.github.io/docs/quickstart/

Let’s install SAFE Stack

We’re in March 2020 and it’s .Net Core 3.1 era.

Add a new template to your dotnet new

This will allow you to scaffold template project. The project being quite active, I suggest you to update once in a while by retyping the same command. It will download the latest version while re-installing. (It’s based on a NuGet template package which you can follow).

dotnet new -i SAFE.Template

Create a new folder and then scaffold a first app

Details on command line arguments at https://safe-stack.github.io/docs/template-overview/

dotnet new SAFE --layout fulma-cover --deploy docker

Restore the whole app (takes a bit of time)

dotnet tool restore

Run without VS Code (No nice debugging experience)

Use the following to see if all is working as expected (Takes time on the first launch since it downloads everything).

dotnet fake build --target run

Run within VS Code (Integrated and nice experience)

Open VS Code in the current folder code .. If the extension required to run are not installed it will pop-up the following message. Simply agree

Once installed, if not already go on the debug icon on the left menu bar and look at the targets:

Click on Debug SAFE App. This will start both front-end and back-end server and watch/attach process in VS Code. This will allow to do live debug. After you click and wait, you should have 2 process attached

If you look at your source code from the F# view (default it after you have installed the plugin and opened a folder with FS files). You will see that you have only a few files, while in reality you have many. Simply go in the folder view to see everything. You will also see there’s a shared folder with a file. That file is shared through the FSProj file.

Default view after starting the application

Application created, let’s explore quickly the files

Front-end

All the front-end is build using react/redux. Since it’s the case and it’s quite nice, you can use the time machine to play around.

The code to generate the UX is something that looks like bellow:

let containerBox (model : Model) (dispatch : Msg -> unit) =
    Box.box' [ ]
        [ Field.div [ Field.IsGrouped ]
            [ Control.p [ Control.IsExpanded ]
                [ Input.text
                    [ Input.Disabled true
                      Input.Value (show model) ] ]
              Control.p [ ]
                [ Button.a
                    [ Button.Color IsPrimary
                      Button.OnClick (fun _ -> dispatch Increment) ]
                    [ str "+" ] ]
              Control.p [ ]
                [ Button.a
                    [ Button.Color IsPrimary
                      Button.OnClick (fun _ -> dispatch Decrement) ]
                    [ str "-" ] ] ] ]

Most of the UX is created using Fulma functions (Control.p, Field.div, etc.). In case you would like to have a line break, you could add a line with br []. It’s not because it’s using most likely Fulma that you cannot use the Fable.React.Standard html controls.

The first [] are for the style (class and stuff like that) and the second [] is the content.

The default project put all within one file, but nothing does not allow you to split across multiple file. Even the tutorial SAFE-Dojo is built using a bit more separation of concerns.

Back-end

Like the front-end, I would have loved to have a better separation within the template scaffolding. Right now, only one file is created and it’s up to you to know what to do.

Since the back-end is built around Saturn and Giraffe, you can see at the doc to understand more how to split your files (Which I highly recommend).

Otherwise, for the back-end, it’s the usual. You use your code and favorite libraries. Don’t forget that you can share some logic and types between your back and front.

Next step

I will try to come back on this, but, it will be about moving the file around and going a bit deeper with a bigger scenario. At the moment, the samples on the website are not all up to date and you might experience some issue by running them. That being said, enjoy playing around until the next post.

Thank you for reading!

]]>
https://blog.honosoft.com/2020/03/17/how-to-have-more-fun-with-a-safe-stack/feed/ 0
kubeadm – How to “upgrade” (update) your configuration https://blog.honosoft.com/2020/01/31/kubeadm-how-to-upgrade-update-your-configuration/?utm_source=rss&utm_medium=rss&utm_campaign=kubeadm-how-to-upgrade-update-your-configuration https://blog.honosoft.com/2020/01/31/kubeadm-how-to-upgrade-update-your-configuration/#respond Fri, 31 Jan 2020 03:57:37 +0000 https://blog.honosoft.com/?p=628 Continue Reading]]> As you may know, you can easily configure a Kubernetes master or high availability (HA) cluster using kubeadm. All the instruction are on the kubernetes.io website. However, there’s no details about how to play with the configuration or how to apply a modification to the configuration. After a bit of searching on google I was not able to find how to update my current cluster configuration “the proper way” using kubeadm and it’s configuration.

After you run the kubeadm tool in order to install your cluster (or server) generate a kubeadm-config within the ConfigMap on the kube-system namespace. If you edit that ConfigMap, the changes won’t be propagated on the master nodes. If you want to apply the changes, you will have to apply the configuration yourself master by master.

In this article, I will explain how to apply such changes.

Get your configuration in kubeadm format

The command is simple and easy to find all over the internet. Basically you want to view the current configuration and save it into a file. In linux, you simply type the following command:

kubeadm config view > kubeadm-config.yaml

Now that you have the configuration applied during the kubeadm init (or join) phase you can now edit and do your changes. I would absolutely not recommend to change your CIDR here. In my case, the kube-apiserver does not run using the first network interface. This bring some issues since kubeadm take for granted that you will be using your first network interface. For example, in vagrant, it means that it can access the kubernetes network using the enp0s8 which is the second NIC. The first NIC is used by vagrant itself during the installation and to connect to it using the NAT on the enp0s3. To ensure that the reconfiguration will be working, I absolutely need to inject the advertise-address in the configuration.

Edit your configuration

Bellow, you will see that I’ve added some legacy API (deprecated) used by let’s say Gitlab-ce and the advertise-address. Beware that the advertise-address should be different on each master you reconfigure.

apiServer:
  extraArgs:
    advertise-address: 192.168.5.102
    authorization-mode: Node,RBAC
    runtime-config: apps/v1beta1=true,apps/v1beta2=true,extensions/v1beta1/daemonsets=true,extensions/v1beta1/deployments=true,extensions/v1beta1/replicasets=true,extensions/v1beta1/networkpolicies=true,extensions/v1beta1/podsecuritypolicies=true
  timeoutForControlPlane: 4m0s
apiVersion: kubeadm.k8s.io/v1beta2
certificatesDir: /etc/kubernetes/pki
clusterName: kubernetes
controlPlaneEndpoint: kube-lb:6443
controllerManager: {}
dns:
  type: CoreDNS
etcd:
  local:
    dataDir: /var/lib/etcd
imageRepository: k8s.gcr.io
kind: ClusterConfiguration
kubernetesVersion: v1.17.2
networking:
  dnsDomain: cluster.local
  serviceSubnet: 10.96.0.0/12
scheduler: {}

Validate your changes

To verify if your configuration match what you want to do, you can do a diff with the current server.

kubeadm upgrade diff --config kubeadm-config.yaml

Apply your changes

Once you’re happy with your adjustments, you can then apply them. In the following command I’ve enabled the verbose mode to give all the output of what it is doing. You can skip it if you wish. I personally like to see what’s happening.

kubeadm upgrade apply --config kubeadm-config.yaml --ignore-preflight-errors all --force --v=5

In case you are not so sure about what you’re going to do, you might want to do a –dry-run before applying it.

Once applied, the ConfigMap will be updated. The configuration there only represent the last “kubeadm” configuration you’ve applied. So, each time you re-apply a new version of that configuration will be shown.

Upgrade other master node

The upgrade does not update all your “master” on the cluster, it only upgrade the current one where you are. You will need to apply the config on each master by proceeding the last part of this article (change IP of advertise address in my case and then run the upgrade apply).

That’s it for today 😉

Featured image by: Fletcher Pride
]]>
https://blog.honosoft.com/2020/01/31/kubeadm-how-to-upgrade-update-your-configuration/feed/ 0
Dotnet Core : What can you do in case you wish to authorize without [Authorize] ? https://blog.honosoft.com/2019/07/09/dotnet-core-what-can-you-do-in-case-you-wish-to-authorize-without-authorize/?utm_source=rss&utm_medium=rss&utm_campaign=dotnet-core-what-can-you-do-in-case-you-wish-to-authorize-without-authorize https://blog.honosoft.com/2019/07/09/dotnet-core-what-can-you-do-in-case-you-wish-to-authorize-without-authorize/#comments Tue, 09 Jul 2019 12:47:21 +0000 https://blog.honosoft.com/?p=586 Continue Reading]]> In some cases, the [Authorize] attribute is not all you want. Sometimes, you wish that something else existed and that’s where the IAuthorizationFilter (or IAsyncAuthorizationFilter) come into play. Before going there, ensure that you don’t simply need a new Policy (See MSDN article or doc.). For more details about the Filter I will let you read the MSDN page which is always updated.

What we will cover here:

Ordering of the filters within Dotnet Core

The request is processed through Authorization Filters, Resource Filters, Model Binding, Action Filters, Action Execution and Action Result Conversion, Exception Filters, Result Filters, and Result Execution. On the way out, the request is only processed by Result Filters and Resource Filters before becoming a response sent to the client.
Image from this MSDN article

Basically, most of the time we want to add some behavior within the ActionFilter area. But when it comes to authorization, the best is to stop the entire pipeline as soon as possible. That’s where the IAuthorizationFilter comes into play.

Sample of creating an IAuthorizationFilter

The IAuthorizationFilter only require us one method to implement (i.e.: OnAuthorization). There is also no need of doing any dependency injection for such attribute in order to activate it. The following code is only an example doing nothing special, but you should grasp the idea.

    public sealed class SpecialAuthorizeAttribute : Attribute, IAuthorizationFilter  // or IAsyncAuthorizationFilter  ...
    {
        private const string HEADER_SPECIAL_STUFF = "specialHeaderStuff";

        public void OnAuthorization(AuthorizationFilterContext context)
        {
            if (context != null)
            {
                var authCode = context.HttpContext.Request.Headers[HEADER_SPECIAL_STUFF];
                // Write your logic here.
                if (string.IsNullOrEmpty(authCode))
                {
                    context.Result = new UnauthorizedObjectResult($"{HEADER_SPECIAL_STUFF} is required");
                    return;
                }

                if (!authCode.Equals("IAmValid"))
                {
                    context.Result = new UnauthorizedObjectResult($"{HEADER_SPECIAL_STUFF} is invalid");
                    return;
                }

                // ===== if you're here, it's a success =====
                // DO STUFF
                //
                // ===== you can also use registered services =====
                // var someService = context.HttpContext.RequestServices.GetService<ISomeService>();
            }
        }
    }

Sample of using our custom IAuthorizationFilter

The following code is based on a new WebApi project.

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{

    // GET api/values/5
    [SpecialAuthorize]
    [HttpGet("{id}")]
    public ActionResult<string> Get(int id)
    {
        return "value";
    }

    // ... other stuff
}

In order to execute, you can simply do a HTTP call using Curl.

nordes@Something:~$ curl https://localhost:5001/api/values/1 --insecure && echo ''
specialHeaderStuff is required
nordes@Something:~$ curl https://localhost:5001/api/values/1 -H 'specialHeaderStuff:IAmInvalid' --insecure && echo ''
specialHeaderStuff is invalid
nordes@Something:~$ curl https://localhost:5001/api/values/1 -H 'specialHeaderStuff:IAmValid' --insecure && echo ''
value

As you can see, when I use a Valid value, it is accepted and then process the Controller-Action. The attribute could also be put on the Controller and in such case will apply that rule to every actions.

Conclusion

As you can see, it’s not something complex to do. What you should ask yourself is if you need to use it? In my case, I had to use it for specific scenario, but most people will probably simply use the policy over the official Authorize attribute given by the framework.

]]>
https://blog.honosoft.com/2019/07/09/dotnet-core-what-can-you-do-in-case-you-wish-to-authorize-without-authorize/feed/ 1
Dotnet and Reflection – Exploration By Example https://blog.honosoft.com/2019/03/06/dotnet-and-reflection-exploration/?utm_source=rss&utm_medium=rss&utm_campaign=dotnet-and-reflection-exploration https://blog.honosoft.com/2019/03/06/dotnet-and-reflection-exploration/#respond Wed, 06 Mar 2019 14:21:00 +0000 https://blog.honosoft.com/?p=458 Continue Reading]]> Today, I will be talking about how to use reflection in order to generate code. The reflection in C# is quite strong and useful for many scenario. In this example I will simply use a dictionary created from a JSON object and then based on the “key” of that dictionary, I will create a special object if it applies, including the value. This could be useful if you receive a lot of data in from a third party, but where you would be interested only in a few keys in order to generate data somewhere in your system. Here we will not go through the performance part, but, simply know that you can alter the algorithm in order to cache the key.

What are we building?

The example will be simple and will be doing the following:

  • Take JSON input
  • Transform that JSON into an object (In this case, an object containing a dictionary)
  • Pass through all the items from the dictionary and match the key against an enum which will be used to generate code
  • Use reflection on the dictionary key enum (if it exists) and create a new special object with the dictionary value
  • Print out the generated data using JSON prettify option

Building block #1 – JSON input

The JSON input will be quite simple, it will be an object containing a simple dictionary.

{ "MyDictionary": { "Key1": 1, "Key2": 2, "Key3": 3.1416 } }

Building block #2 – Basic objects structure

We now know the JSON structure, so let’s create our basic objects. Here we go a bit further than simply creating objects, we also create the Attribute which will be used during the “Reflection”.

class SampleObject
{
    public Dictionary<string, decimal> MyDictionary { get; set; }
}
public class CustomAttribute : Attribute
{
    public string CustomValue { get; set; }

    public CustomAttribute(string customValue)
    {
        CustomValue = customValue;
    }
}
 enum MyDictionarySpecialKeys
 {
     [Custom("Simple Value")]
     Key1,
     [Custom("Another Simple Value")]
     Key3
 }
 class MySpecialItem
 {
     public string Special { get; set; }
     public string OriginalKey { get; set; }
     public string Value { get; set; }
 }

Dive in the reflection

Now that we have all the basic object, let’s talk about the Reflection. I will not repeat what you can find on the MSDN, but rather show you directly a possible usage. The Reflection, as the name indicate, can go and read through the assembly and see how the objects are made and give you useful information. Recently, I used it in order to take a JSON configuration (appsettings section) and then generate code to be executed in the background based on that. I will not explain further that application.

Let’s begin, we will be taking our “Attribute” from the enum value and use the value provided. Then from that, instead of simply returning a simple string, let’s return a new Object. In this implementation, I will be using an extension method for simplicity. The same could be done using a simple method.

public static class CustomAttributeExtension
{
    internal static MySpecialItem GenerateEvent(this MyDictionarySpecialKeys myKey, decimal val)
    {
        // Get the member from the Enum
        var memberInfo = typeof(MyDictionarySpecialKeys)
            .GetMember(myKey.ToString())
            .FirstOrDefault();

        // If the enum member exists, then let's get the attribute
        if (memberInfo != null)
        {
            var attr = (CustomAttribute)memberInfo
                    .GetCustomAttributes(typeof(CustomAttribute), false)
                    .FirstOrDefault();

            if (attr == null)
            {
                // Should skip, if not exists
                return null;
            }

            // If enum contains the attribute let's create an object
            return new MySpecialItem
            {
                Special = attr.CustomValue,
                OriginalKey = myKey.ToString(),
                Value = val.ToString(CultureInfo.InvariantCulture)
            };
        }

        // Should skip
        return null;
    }
}

Let’s put all together in a program

If we take all of what we’ve done so far and put this into a program. We will have a JSON having 3 items in a dictionary. However, that dictionary will have only two entries mapped on the strongly typed enum. From that enum, the two enum members will be decorated with a custom attribute giving a special extra meaning to the value.

That put into code will give you something like this:

public class Program
{
    public static void Main()
    {
        var jsonInputSample = "{ \"MyDictionary\": { \"Key1\": 1, \"Key2\": 2, \"Key3\": 3.1416 } }";
        Console.WriteLine($"Input: {jsonInputSample}");

        var deserializedObject = JsonConvert.DeserializeObject<SampleObject>(jsonInputSample);
        var generatedSpecialItems = new List<MySpecialItem>();

        Console.WriteLine();
        Console.WriteLine("Content of [MyDictionary]:");

        foreach (var dictionaryItem in deserializedObject.MyDictionary)
        {
            Console.WriteLine($"\tDictionary [Key: {dictionaryItem.Key}] [Value: {dictionaryItem.Value}]");
            if (Enum.TryParse(dictionaryItem.Key, true, out MyDictionarySpecialKeys specialKey)
                && Enum.IsDefined(typeof(MyDictionarySpecialKeys), specialKey))
            {
                var specialItem = specialKey.GenerateEvent(dictionaryItem.Value);

                if (specialItem != null)
                {
                    generatedSpecialItems.Add(specialItem);
                }
            }
        }

        Console.WriteLine();
        Console.WriteLine("Result (Could be special items to be inserted in a DB):");
        Console.WriteLine("-----------------------------------------");
        Console.WriteLine(JsonConvert.SerializeObject(generatedSpecialItems, Formatting.Indented));
        Console.WriteLine("-----------------------------------------");
        Console.WriteLine("Completed");
        // Console.ReadKey(); // If you wish to wait when you run from Visual Studio and not command line.
    }
}

And the output result will be:

Input: { "MyDictionary": { "Key1": 1, "Key2": 2, "Key3": 3.1416 } }

Content of [MyDictionary]:
        Dictionary [Key: Key1] [Value: 1]
        Dictionary [Key: Key2] [Value: 2]
        Dictionary [Key: Key3] [Value: 3.1416]

Result (Events to be created):
-----------------------------------------
[
  {
    "Special": "Simple Value",
    "OriginalKey": "Key1",
    "Value": "1"
  },
  {
    "Special": "Another Simple Value",
    "OriginalKey": "Key3",
    "Value": "3.1416"
  }
]
-----------------------------------------
Completed
]]>
https://blog.honosoft.com/2019/03/06/dotnet-and-reflection-exploration/feed/ 0
Canvas Chart ➡️ Snapshot ➡️ Manipulate ➡️ Send it to your confluence page (or elsewhere) https://blog.honosoft.com/2019/01/11/canvas-chart-%e2%9e%a1%ef%b8%8f-snapshot-%e2%9e%a1%ef%b8%8f-manipulate-%e2%9e%a1%ef%b8%8f-send-it-to-your-confluence-page-or-elsewhere/?utm_source=rss&utm_medium=rss&utm_campaign=canvas-chart-%25e2%259e%25a1%25ef%25b8%258f-snapshot-%25e2%259e%25a1%25ef%25b8%258f-manipulate-%25e2%259e%25a1%25ef%25b8%258f-send-it-to-your-confluence-page-or-elsewhere https://blog.honosoft.com/2019/01/11/canvas-chart-%e2%9e%a1%ef%b8%8f-snapshot-%e2%9e%a1%ef%b8%8f-manipulate-%e2%9e%a1%ef%b8%8f-send-it-to-your-confluence-page-or-elsewhere/#respond Fri, 11 Jan 2019 12:39:37 +0000 https://blog.honosoft.com/?p=426 Continue Reading]]> Today, let’s create a connection between one of your app using ChartJs + Confluence and send or synchronize a chart. The flow with Confluence server used will be with the basic authentication. In the case you want to build a real plugin/extension to Confluence, please follow the proper guide about it. In this blog post, the basic authentication is simply a mean to an end for quick prototyping.

A complete example of a standalone project can be found on Github (https://github.com/Nordes/HoNoSoFt.PushChartToConfluence.Sample).

Introduction

Like I said, we will have an application serving some charts using the Canvas (HTML5) technology. We will then resize, take a snapshot and then transmit this file to our backend server. Then it will connect to confluence, look at the existing attachments and if it exists it will update using a comment or in the case where the file does not exists, we will be posting a new file. It is nice to update the file, but why not add the file (image) to the page if it is not already there. 🙂

Proposal using a chart

Note that we’re going to add the chart, but the same flow could be used to also update some comment/text. As long as you are familiar with html syntax, you should be able to do something good.

Pre-requisites before starting

  1. If you already have a confluence server or follow the official installation guide from Confluence, skip to #3 (Create a small application)
  2. For the docker users, let’s go towards the evaluation “just because”
    1. docker pull atlassian/confluence-server:latest
      1. About 700mo
    2. While it download, you can start by requesting a KEY
      1. https://my.atlassian.com/license/evaluation
      2. Select a Confluence server license and keep that page open until the docker image is ready.
    3. Start the docker image
      • Command: docker run -v $volume$:/var/atlassian/application-data/confluence --name="confluence" -d -p 8090:8090 -p 8091:8091 atlassian/confluence-server
        • $volume$: C:/demo/your/confluence/home
      • Open site: http://localhost:8090/
    4. During installation, fetch the Server ID and input it in the evaluation page.
      1. Follow the instructions
        1. Select trial
        2. Get the Server ID
        3. Go on your atlassian page already open and paste it
        4. Generate your key
        5. Copy/paste your key in the form within the confluence page
        6. Click next (wait a little) and voilà
        7. Setup a user or two and then let’s start
  3. Create a small application with ChartJs (or use the one built in the previous post )

Back-End

In the back-end, we will first be receiving the image to at least test our local upload between front-end and back-end. After, we will start integrating the Confluence connexion.

Dotnet ImagesController: Receive Images (Part 1)

Within Dotnet Core, we can receive files or forms controls using the IFormCollection. I don’t think it is common to receive such thing, but when talking about receiving files, this comes handy. The client (JavaScript) will then create a collection and send it using a multipart/form-data. In there we can find many things, however what we will only using here are the files (((IFormCollection)MyFormCollection).Files).

Let’s create a controller named ImagesController we will receive the files as stream and then copy those files to a temporary folders. If you already played around with file streaming you can probably skip this part.

[Route("api/[controller]")]
[ApiController]
public ImagesController : ControllerBase {
    // .. some constructor stuff

    /// <summary>
    /// This will save the image in your local "temp" folder.
    /// </summary>
    [HttpPost]
    [ProducesResponseType((int)HttpStatusCode.Created)]
    public async Task<IActionResult> PostAsync(IFormCollection formCollection)
    {
        var files = formCollection.Files;
        long size = files.Sum(f => f.Length);
        List<string> fileList = new List<string>();

        foreach (var formFile in files)
        {
            // full path to file in temp location
            var filePath = Path.GetTempFileName();
            if (formFile.Length > 0)
            {
                using (var stream = new FileStream(filePath, FileMode.Create))
                {
                    // Save to file... We could remove the await, have a List<Task<..>> and then do a Task.WhenAll(myList)
                    await formFile.CopyToAsync(stream);
                    fileList.Add(filePath);
                }
            }
        }

        // Example of processed uploaded files returning details of the new file + original request.
        // You shouldn't rely on (or) trust the FileName property without validation.
        return Ok(new { count = files.Count, size, fileList });
    }
}

This is named Part 1 since we will come back in that code in order to add some code to send to confluence.

Front-End

Do the snapshot

From JavaScript, we have two methods that are quite handy to take snapshot of a Canvas. The first one is “toBlob(…)” and the second is “toDataUrl(…)“. While “toBlob” is not supported everywhere, I find it more useful for the demo, otherwise feel free to use “toDataUrl” for all browsers supports except Edge and then convert that base64 into a binary data (image/png). In case you go towards the “toDataUrl” method, don’t forget that you will have to transform the data once it’s server side for something readable (binary) as an image/png.

You will see in the next sub-section how to start from a ChartJs chart which you’d like to push to your back-end having a resolution of 1200px wide. Don’t forget that if you use an adaptive screen UX, it will not be displayed as 1200px until you do a snapshot for a fraction of a second.

How does the resizing work?

As you already know, a ChartJs canvas does not resize automatically at your wishes and if you want a snapshot, you are required to hack your way. Next, we are going to have the following flow:

Interesting part in the JS, I will skip the trivial step of generating a chart since you’re I suspect that you’re already able to do so.

// Some code (I used VueJs, but other language should look alike)

snapshot: function () {
  // Resize to desired size, bigger it is, more heavy will be the blob file.
  this.$refs.chartContainer.style.width = '1200px'
  this.chart.resize()
  var ctx = this

  // Do a snapshot
  this.$refs.chart.toBlob(function (blob) {
    // Resize back to original size
    ctx.$refs.chartContainer.style.width = ""
    ctx.chart.resize()

    // Prepare the form post
    // The file name used will be the chart title, but depending on your case, you
    // might want to have something more precise based on parameter (hash, encoding, something). 
    var filename = `${ctx.chartConfig.options.title.text}.png`
    var data = new FormData()
    data.append('file', blob, filename)

    const config = {
        headers: { 'content-type': 'multipart/form-data' }
    }

    // Post the file to your backend
    ctx.$http.post('/api/images', data, config)
    // Later in the article it should become as
    // ctx.$http.post('/api/images/confluence/{desiredPageId}', data, config)
  }, "image/png", 0.95);
}

// Some code

As you can see, I resize to 1200px => snapshot (toBlob) => resize back to original => send the file as multipart/form-data to the api.

The file should normally be created within your temp folder and the exact location will come back through the API. It is also part of the current data contract. Now that you have that, you can consider yourself ready for the next step, which is to transfer that buffered binary data directly to confluence using their API’s.

Back-end Part 2 – Use confluence API’s

Now that we know we can receive file and save it, we now simply need to use the Confluence API’s from Atlassian. The related documentation for that is:

API’s that we’re going to use for this demo are:

  • [GET] api/content/{pageId}/child/attachment?filename={formFile.FileName}&expand=version
    • Search/Retrieve the existing attachment. In case it does not exists, it will sends back an empty array.
  • [POST] api/content/{pageId}/child/attachment/{attachmentData.Id}/data
    • Update in case of existing attachment
  • [POST] api/content/{pageId}/child/attachment
    • Create the attachment resource if it was not already existing
  • [GET] api/content/{pageId}?expand=version,body.storage
    • Retrieve the details on the current page, especially the body specified in readable/editable way (storage). The version is also mandatory when you want to update the page.
  • [PUT] api/content/{pageId}
    • Api used in order to update the confluence page.

Update the ImagesController to forward to Confluence

There’s maybe more code than required in the controller. The proper approach would be to use a IDataProvider injected (for UT) and then implement the provider using the IHttpClientFactory. That way, all would be testable and also it would also put the logic where it should be. However, let’s put all for now in one place and please adapt for your needs.

// some code in the controller

        /// <summary>
        /// Receive 1 or more images to be sent to Confluence server.
        /// </summary>
        /// <param name="formCollection">The form data (only files are being consumed)</param>
        /// <param name="pageId">The confluence page Id</param>
        /// <remarks>
        /// More details can be found at https://developer.atlassian.com/server/confluence/confluence-rest-api-examples/
        /// </remarks>
        [HttpPost("confluence/{pageId}")]
        [ProducesResponseType((int)HttpStatusCode.Created)]
        public async Task<IActionResult> PostToConfluence(IFormCollection formCollection, int pageId)
        {
            var files = formCollection.Files;
            long size = files.Sum(f => f.Length);
            List<string> fileList = new List<string>();
            var forwardAttachmentTasks = new List<Task<FileTransferResult>>();

            foreach (var formFile in files)
            {
                if (formFile.Length > 0)
                {
                    forwardAttachmentTasks.Add(ForwardFileToConfluence(pageId, formFile));
                }
            }

            // In case we had multiple tasks at the same time.
            await Task.WhenAll(forwardAttachmentTasks.ToArray()).ConfigureAwait(false);
            await UpdatePage(pageId, forwardAttachmentTasks).ConfigureAwait(false);

            return StatusCode((int)HttpStatusCode.InternalServerError, new { count = files.Count, size, fileList });
        }

        private async Task UpdatePage(int pageId, List<Task<FileTransferResult>> forwardTasks)
        {
            if (forwardTasks.Any())
            {
                // Update the page
                var pageContentResult = await _confluenceHttpClient.GetAsync($"content/{pageId}?expand=version,body.storage");
                var pageContentData = JsonConvert.DeserializeObject<Models.Confluence.Content.ContentStorage>(await pageContentResult.Content.ReadAsStringAsync());

                string newContent = string.Empty;
                foreach (var sentAttachmentTask in forwardTasks)
                {
                    var sentAttachment = await sentAttachmentTask;
                    if (sentAttachment != null && sentAttachment.Results.Any())
                    {
                        // Add the file if not present on the page.
                        var fileName = sentAttachment.Results.First().Title;
                        if (pageContentData.Body.Storage.Value.IndexOf($"<ri:attachment ri:filename=\"{fileName}\" />") == -1)
                        {
                            newContent += $"<h2>You've just pushed: {fileName}</h2><p><ac:image><ri:attachment ri:filename=\"{fileName}\" /></ac:image></p>";
                        }
                        // Else: Nothing to do, it's already on the page somewhere.
                    }
                }

                // Update object (camelCase mandatory, so use a proper serializer in real life scenario)
                var updateQuery = new
                {
                    id = pageContentData.Id,
                    title = pageContentData.Title,
                    status = pageContentData.Status,
                    type = pageContentData.Type,
                    version = new { number = pageContentData.Version.Number + 1 },
                    body = new
                    {
                        storage = new
                        {
                            value = pageContentData.Body.Storage.Value + newContent,
                            representation = "storage"
                        }
                    }
                };

                var result = await _confluenceHttpClient.PutAsJsonAsync($"content/{pageId}", updateQuery);
            }
        }

        private async Task<FileTransferResult> ForwardFileToConfluence(int pageId, IFormFile formFile)
        {
            // Start getting if attachment exists
            var getIfAttachmentExists = _confluenceHttpClient.GetAsync($"content/{pageId}/child/attachment?filename={formFile.FileName}&expand=version").ConfigureAwait(false);
            // While previous request goes on, let's get the file.
            byte[] data;
            using (var br = new BinaryReader(formFile.OpenReadStream()))
            {
                data = br.ReadBytes((int)formFile.OpenReadStream().Length);
            }

            ByteArrayContent bytes = new ByteArrayContent(data);
            MultipartFormDataContent multipartContent = new MultipartFormDataContent();
            multipartContent.Add(bytes, "file", formFile.FileName);

            var attachmentRequestData = await getIfAttachmentExists;
            if (attachmentRequestData.IsSuccessStatusCode && attachmentRequestData.StatusCode == HttpStatusCode.OK)
            {
                // Page exists and no errors...
                var attachmentRequestContent = await attachmentRequestData.Content.ReadAsStringAsync().ConfigureAwait(false);
                var attachmentData = JsonConvert.DeserializeObject<FileSearch>(attachmentRequestContent);

                HttpResponseMessage putAttachmentResponse;
                // Update existing data.
                if (attachmentData.Size == 1)
                {
                    multipartContent.Add(new StringContent($"Automatic update/upload from TestApplication ;)."), "comment");
                    putAttachmentResponse = await _confluenceHttpClient.PostAsync(
                        $"content/{pageId}/child/attachment/{attachmentData.Results.First().Id}/data",
                        multipartContent);

                    // Result is 1 "item"
                    var content = await putAttachmentResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
                    var result = JsonConvert.DeserializeObject<Models.Confluence.Result>(content);

                    return new FileTransferResult() { Results = new Models.Confluence.Result[] { result }, Size = 1 };
                }
                else
                {
                    // Create the attachment
                    multipartContent.Add(new StringContent($"Automatic upload from TestApplication ;)."), "comment");
                    putAttachmentResponse = await _confluenceHttpClient.PostAsync(
                        $"content/{pageId}/child/attachment",
                        multipartContent);

                    // Result is a list of item.
                    var content = await putAttachmentResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
                    return JsonConvert.DeserializeObject<FileTransferResult>(content);
                }
            }

            return default(FileTransferResult);
        }

// some code in the controller

It’s a lot of code. By reading it, it should be really easy to understand. During the file upload to Atlassian Confluence, we add a comment (updated or created) and the version gets updated. That way you could show the changes over time.

What else you could do?

  • Transform the backend in order to have a provider injected
  • Create UT
  • Create IT
  • Change the token used in order to use the official Atlassian flow for plugins
  • Automate everything back-end by creating a scheduled job (not tested, but should be feasible)
    1. Selenium
    2. Image docker + chromium
    3. Execute the javascript using chrome Webdriver
    4. Send the image using a job.
  • Send a SnapShot (not necessarily from Charts) while building your backend app in a pipeline.

Conclusion

Thank you for reading and I hope you have learned something today, or at least enjoyed this article.

]]>
https://blog.honosoft.com/2019/01/11/canvas-chart-%e2%9e%a1%ef%b8%8f-snapshot-%e2%9e%a1%ef%b8%8f-manipulate-%e2%9e%a1%ef%b8%8f-send-it-to-your-confluence-page-or-elsewhere/feed/ 0
Create plugins in Dotnet Core or what we call Managed Extensibility Framework https://blog.honosoft.com/2018/11/29/create-plugins-in-dotnet-core-or-what-we-call-managed-extensibility-framework/?utm_source=rss&utm_medium=rss&utm_campaign=create-plugins-in-dotnet-core-or-what-we-call-managed-extensibility-framework https://blog.honosoft.com/2018/11/29/create-plugins-in-dotnet-core-or-what-we-call-managed-extensibility-framework/#respond Thu, 29 Nov 2018 13:27:41 +0000 https://blog.honosoft.com/?p=352 Continue Reading]]> In Java we have the Service Loader that can add plugin(s) within your application at startup. It basically means you can extend your application with a set of new features. This same service loader can also at some extent add those plugins at runtime by detecting new files within the plugin folder.

If we look what we have in Dotnet world, we have something very similar that have been existing for a long time. It’s still rare to see such system being used. It will more likely already exists without having anyone really having to maintain it. Since in Dotnet Core 2.1, the namespaces changed a bit, it makes it interesting to explore with you. I guess that if you are on this post, you are most likely a technical guy. I assume that you’ve already done some research on what it is and that you’d like an example (step by step) to really show how easy it is to build. In this article we’ll cover only partially the Manageed Extensibility Framework (MEF).

Edit: For Dotnet Core 3, there’s a new feature, so follow this link to MSDN.

What is the Managed Extensibility Framework (A.k.a.: MEF)?

Often you will hear about the acronym MEF instead of its long name “Managed Extensibility Framework”.  In the past we were also talking in parallel about the MAF which stands for Managed Add-in Framework (See details here). MAF still exists within the Dotnet Framework with Windows Client (WPF for example), however in Dotnet Core you it is not available “yet”.

In the introduction, I stated that if you’ve used Java, you know the main idea about it. For the others, the general idea is, that when, you or a third party, have finished to create your plugin you drop your JAR in the plugins folder.  In C#/Dotnet world, MEF and MAF are using the same principle, whereas instead of dropping a Jar you drop your “Assemblies”.

Let’s go back to the question, what is MEF? It’s a contracts based extensibility managed by imports and exports within your programs or libraries. You will then make an extensible application by implementing interfaces and then in your main application you will have a provider to provide the implemented contracts (interfaces) implemented. The Assemblies that you want to import in your main application require the exports. Those exports are going to be discovered through “Reflection“.

Let’s take the famous calculator as an example where you could extends the functionalities (plus, minus, division, etc.). Each of those functionalities could be a provided within different “Assemblies” loaded at the startup of your application. There would be no need of project references to those assemblies within your main application. So if you add or remove “Assemblies”, functionalities are going to be added or removed. The magic comes from the exports you would discover. For the calculator example, you could have a “Name” (Square Root) for what it is and the official “Operator” (√) for the discovered feature. You would then have an implementation of the operator receiving “Number 1, …” producing a new result (Contract within an interface shared between all the libraries, a.k.a. interfaces). The Imports are, as stated previously, most likely be in your main executable (WebApi, Console, etc.).

How does it works?

As stated above, the main program will do your discovery of shared contracts API’s. Those Exports/Imports are being assigned using attributes on your classes. Those attributes, to name only a few, will look like [import], [ImportMany], [ExportMetadata(“something”, “here”)], [Export(typeof(IYourInterface))], etc.. As you can see, the import will import only one instance while ImportMany will create an IEnumerable. Those attributes can be found within the Nuget package “System.Composition.AttributedModel“. Keep that package in your mind since it will become handy in a later stage.

If you wonder why the ExportMetadata can become something really cool. I would say that you could have some filtering that could come from there regarding your application. That could also help in order to give some capabilities about your module (Extensibility).

Practice time – Let’s build a simple App (WebApi/SPA)

Of course, we’re not going to be building the famous calculator sample. It’s all over the place already. I don’t say that the calculator is a not a great example, but my point here is not to copy what already exists. In this “exercise”, you will be scratching the surface of the Extensibility. In a real-world scenario, you could have logs that need to strips out content based on rules that are either common or specific. Let’s say, the customer numbers are not the same from customer to customer as such that each customer requires to have different custom rules mixed with general ones. That one could be done using simply a regex, but let’s say it goes to something really complex like the passport numbers. In that case, you could implement custom rules for each customer (per country) and simply drop those rules within the plugin folder. That way, you could even request the customer to build the plugin themselves and if you thrust them enough then simply allow them to update their plugin through a portal (upload, push, and either restart your service or then load at runtime the updated plugin).

The current exercice is to simply create a simple Web Application written in Dotnet Core 2.1 using a basic UI querying the API. It will then show the result in raw of what plugin have discovered.

Scaffold a simple WebApi and Projects

In previous posts, I said that I like to use my own template since it’s quite a good starter. It’s compact and fast enough for anyone with basic knowledge of Dotnet Core and VueJs. The UX is using, instead of the common Bootstrap 4, something lighter called Picnic CSS. You can download/update the template using > dotnet new -i HoNoSoFt.DotNet.Web.Spa.ProjectTemplates(Documentation here). This exercise can also be accomplished within a Console application. However, I think the most common scenario will be around the usage of a WebApplication and then extend it somehow. Let’s create the structure of the project (Command line, it’s easier):

> mkdir Sample.Mef
> cd Sample.Mef
Sample.Mef> dotnet new vuejs-picnic -o Sample.Mef.Web
Sample.Mef> dotnet new classlib -o Sample.Mef.Api
Sample.Mef> dotnet new classlib -o Sample.Mef.GiveMeOne
Sample.Mef> dotnet new classlib -o Sample.Mef.GiveMeFive
Sample.Mef> dotnet new sln
Sample.Mef> dotnet sln add .\Sample.Mef.Web\Sample.Mef.Web.csproj
Sample.Mef> dotnet sln add .\Sample.Mef.Api\Sample.Mef.Api.csproj
Sample.Mef> dotnet sln add .\Sample.Mef.GiveMeOne\Sample.Mef.GiveMeOne.csproj
Sample.Mef> dotnet sln add .\Sample.Mef.GiveMeFive\Sample.Mef.GiveMeFive.csproj
Sample.Mef> cd Sample.Mef.Web
Sample.Mef\Sample.Mef.Web> npm install
Sample.Mef\Sample.Mef.Web> dotnet run

Normally after completing the previous command, you should have the Web Project starting. The URL of the project will be displayed within the console or if you are in Visual Studio, it will open the start page by itself.

For the next part of the blog I will be using Visual Studio Community, but feel free to use VS Code.

Exercise: Structure Application Tree

In the following screen capture, some renaming have already taken place (1) and as indicated in the image, some files needs to be removed (2) or modified (3).

Not marked on the picture:

  • Remove IWeatherProvider.cs 
  • Update ServiceCollectionExtensions.cs (remove the injection of the IWeatherService).

Implement the contract in GiveMeOne & GiveMeFive

Some project will require the reference in order to be able to use IGiveNumber interface. Those relation are shown within one of the previous section (UML like)

  • Add the reference of Sample.Mef.Api project to
    • Sample.Mef.GiveMeOne
    • Sample.Mef.GiveMeFive
    • Sample.Mef.Web

The interface “IGiveNumber.cs” look like:

namespace Sample.Mef.Api
{
    public interface IGiveNumber
    {
        int GiveInt();
    }
}

This contract is going to be implemented within “GiveOne.cs” and “GiveFive.cs”

using Sample.Mef.Api;

namespace Sample.Mef.GiveMeOne
{
    public class GiveOne : IGiveNumber
    {
        public int GiveInt()
        {
            return 1;
        }
    }
}
using Sample.Mef.Api;

namespace Sample.Mef.GiveMeFive
{
    public class GiveFive : IGiveNumber
    {
        public int GiveInt()
        {
            return 5;
        }
    }
}

Now you should be able to build with no errors.

Add the Composition/Extensibility to Import/Export

You remember the nuget package from earlier? It gives the simple Import/Export and a few other basic functionalities. In our case, we will be using it simply for exporting the implemented interface. Let’s install it:

  • Add Nuget package “System.Composition.AttributedModel” to
    • Sample.Mef.GiveMeOne
    • Sample.Mef.GiveMeFive

Add the “Export” attribute

Now, let’s update our two previously defined classes (GiveOne, GiveFive) and add our exports. Those exports allow the Reflection, in a later stage, to discover the implementations. Some customization could also be done, but for that, rely on the MSDN documentation.

Within the two previous project mentioned, add the following Export attribute:

using System.Composition;
// Some code...

[Export(typeof(IGiveNumber))]
public class ...// some code...

Add the “Import” matching the previous “Export” within a provider

This part is only modify “Sample.Mef.Web” project. As we’re going to create a new provider, let’s make it properly by using Dependency Injection (DI). Within the provider, we will also be using System.Composition. This is will help doing the discovery of the plugin(s).

  1. Add the Nuget package reference to the project: “System.Composition“
  2. Create an IGiveNumberProvider + GiveNumberProvider
  3. Update the startup to inject the new provider

We will put IGiveNumberProvider and GiveNumberProvider within the same file, but it’s a sample and you can also do as you please.

using Sample.Mef.Api;
using System.Collections.Generic;
using System.Composition;
using System.Composition.Hosting;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;

namespace Sample.Mef.Web.Providers
{
    public class GiveNumberProvider : IGiveNumberProvider
    {
        public GiveNumberProvider()
        {
            Compose();
        }

        [ImportMany]
        public IEnumerable<IGiveNumber> Services { get; private set; }

        public IEnumerable<(string Id, int Value)> GetNumberFromAllFoundServices()
        {
            return Services.Select(f => new { Id = f.GetType().ToString(), Value = f.GiveInt()})
                .AsEnumerable()
                .Select(c => (c.Id, c.Value))
                .ToList();
        }

        private void Compose()
        {
            // Catalogs does not exists in Dotnet Core, so you need to manage your own.
            var assemblies = new List<Assembly>() { typeof(Program).GetTypeInfo().Assembly };
            var pluginAssemblies = Directory.GetFiles("d:\\plugins\\", "*.dll", SearchOption.TopDirectoryOnly)
                .Select(AssemblyLoadContext.Default.LoadFromAssemblyPath)
                // Ensure that the assembly contains an implementation for the given type.
                .Where(s => s.GetTypes().Where(p=> typeof(IGiveNumber).IsAssignableFrom(p)).Any());

            assemblies.AddRange(pluginAssemblies);

            var configuration = new ContainerConfiguration()
                .WithAssemblies(assemblies);

            using (var container = configuration.CreateContainer())
            {
                Services = container.GetExports<IGiveNumber>();
            }
        }
    }

    public interface IGiveNumberProvider
    {
        IEnumerable<(string Id, int Value)> GetNumberFromAllFoundServices();
    }
}

The code should be clear enough in order for you to understand what it does. However, let’s explain quickly:

  • It’s important to note that I’ve set the Assemblies for the extensibility available in the “d:\plugins\” (Please adapt)
  • We filter using reflection on the assemblies. This will enforce keeping only those that have at least one implementation from IGiveNumber
  • In case the assembly can be loaded from the container, we take the related “Export” implementing IGiveNumber and push that within the services
  • You can now access the implementations through the “Services” (Property field)
    • I use the Tuple as a return type, some of it is new in the C# 7+. In case you’ve missed that new feature, go on the following Blogs MSDN article

Add the Dependency Injection (DI)

Now that the provider is ready, let’s add the DI within Startup.cs. The usage of Singleton is privileged since we don’t want to rediscover at each HTTP requests.

// Code ...
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // code ...

            // Example with dependency injection for a data provider.
            services.AddSingleton<IGiveNumberProvider, GiveNumberProvider>();
        }
// Code ...

Alter the existing “api/values” endpoint in order to get the provider “values”.

using Microsoft.AspNetCore.Mvc;
using Sample.Mef.Web.Providers;

namespace Sample.Mef.Web.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private readonly IGiveNumberProvider _giveNumberProvider;

        public ValuesController(IGiveNumberProvider giveNumberProvider)
        {
            _giveNumberProvider = giveNumberProvider;
        }

        // GET api/values
        [HttpGet]
        public IActionResult Get()
        {
            return Ok(_giveNumberProvider.GetNumberFromAllFoundServices().ToList().Select(f=> new { f.Id, f.Value }));
        }

        // Code
}

It’s now mostly complete. I would even say that you must put your project Assemblies (DLL) in “d:\plugins” or where you’ve decided to discover them. The assemblies we’re going to look for are described next.

Time to test by moving your dll’s in the plugins folder

Since you’ve already built, or maybe not yet, your solution, it’s now the time to bring the assemblies (dll) from Sample.Mef.GiveMeOne, Sample.Mef.GiveMeFive available through the bin directory. You then move them to the “d:\plugins”, or the reconfigured folder from previous step. When you copy your bin folder, you need to copy not only the Sample.Mef.GiveMeOne, you also need to copy it’s dependencies (i.e.: Sample.Mef.Api). Since the System.Composition is included in the Web project, it is not mandatory to have a copy in related assemblies plugin folder.

The plugin folder will then contains:

  • Sample.Mef.GiveMeOne.dll
  • Sample.Mef.GiveMeFive.dll
  • Sample.Mef.Api.dll

Start your application and look at the results

Purely speaking, do a dotnet run from the Web application folder, or in Visual Studio push F5.

Go see if your API’s is answering properly before the last step

[Optional] Update your UX in order to do the call

The output is going to simply show a list from the homepage. The extensions to your project, after being loaded, will be displayed within the result of the API call.

<template>
  <div>
    <page-title title="Give the Extensions" />
    <p>He you will be finding your loaded extensions:</p>
    <ul>
      <li v-for="(data, idx) in numbers" :key="idx">{{data.id}}
          <ul>
            <li>{{data.value}}</li>
          </ul>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data () {
    return {
      numbers: []
    }
  },

  mounted() {
    this._getNumbers()
  },

  methods: {
    async _getNumbers () {
      let response = await this.$http.get(`api/values`)
      this.numbers = response.data
    }
  }
}
</script>

Simply run the application and go on the home page. In my case, while using IIS Express, it gave me https://localhost:44307

Final result

Extra Resources

Those resource were not used that much during this blog post, but I think it can help people to also understand how it works in a different context.

  • Official: https://docs.microsoft.com/en-us/dotnet/framework/mef/
  • https://dotnetthoughts.net/using-mef-in-dotnet-core/
  • https://blog.softwarepotential.com/porting-to-net-standard-2-0-part-2-porting-mef-1-0-to-mef-2-0-on-net-core/

Thank you!

By the way, all the source code is available in GitHub (personal @Nordes repository). Enjoy 😉

Featured image original is from: https://unsplash.com/photos/l090uFWoPaI
]]>
https://blog.honosoft.com/2018/11/29/create-plugins-in-dotnet-core-or-what-we-call-managed-extensibility-framework/feed/ 0
ChartJs with SonarQube API’s – Make your own reports https://blog.honosoft.com/2018/11/22/chartjs-with-sonarqube-apis-make-your-own-reports/?utm_source=rss&utm_medium=rss&utm_campaign=chartjs-with-sonarqube-apis-make-your-own-reports https://blog.honosoft.com/2018/11/22/chartjs-with-sonarqube-apis-make-your-own-reports/#respond Thu, 22 Nov 2018 14:30:28 +0000 https://blog.honosoft.com/?p=390 Continue Reading]]> Recently asked to do reports every two weeks to all the teams about their good/bad behaviors and at the same time look if the guidelines were somewhat respected, I decided to build a dashboard using ChartJs and a connection to SonarQube via a local proxy built in Dotnet Core. That report is not built in order to hit the team or anything, it’s more in order to know our current situation and what we should improve. We can also relate the data from Jira (using the api’s) in order to know if we’re improving our bug ratio.

This article will only do a quick start using the template I’ve created recently in Dotnet Core. I won’t put too much details around the code since it’s quite straight forward.

Pre-requisites

  • Dotnet core 2.1 (at the time of writing)
  • NodeJs + NPM (>10 at the time of writing)
  • Visual Studio Code OR Visual Studio Community/Professional
  • A browser (Chrome, or anything else)

Install a quick start from a Dotnet Core template

As mentioned before, you will be using a quick start I created. I use it quite often, so there’s improvement in it happening regularly. So I suggest you to look for updates once in a while. It gives a front-end that could run without backend, and a backend in dotnet core 2.1 ready to serve API’s (including swagger).

> dotnet new -i HoNoSoFt.DotNet.Web.Spa.ProjectTemplates

Once installed, let’s create our template (vuejs-picnic-table):

> mkdir Sonar.Reports
> cd Sonar.Reports
> dotnet new vuejs-picnic-table

And to be sure it works, then let’s run at least in development mode.

> npm install
> dotnet run --environment="Development"

The installation of the NPM packages are needed most likely the first time due to the nature of the project. This could be automated, but would make the project takes a lot of time before even being able to start editing any files. (Personal choice here)

The result should look like the following screen (https://localhost:5001/) OR the demo page available here.

You might get an error the time Webpack generate the files. In that case, simply hit refresh (F5)

Once the application is running, keep it running while you edit your files (next step). You will simply have to hit F5 to refresh the changes.

Clean or add route to the template

Delete all the un-necessary things or simply add a new page. We’ll go here with the latter.

  • Create the file: ./ClientApp/pages/sonar.vue
    • <template>
       <div>
         <page-title title="Sonar" />
       </div>
      </template>
      
      <script>
      export default {
        
      }
      </script>
      
  • Add the new route for the page in ./ClientApp/router/routes.js
    • Add a import in the import section: import Sonar from 'pages/sonar'
    • Add the route after about:  { name: 'Sonar', path: '/:lang?/sonar', component: Sonar, display: 'Sonar', i18n: 'route.sonar', icon: 'icon-home', meta: { order: 6 } },
  • Add the translation/I18n for the new menu in ./ClientApp/_i18n/lang/en.js
    • In the routes group under about, add: sonar: 'Sonar'
  • Save all this and hit F5 in the browser

Congratulation! You’ve created your first page in VueJs. Now it is the time to fill the data and before that, we need to create a service provider.

Access to Sonar Qube API’s from C#

Create your provider in C#, because if you’re not on the same server as your SonarQube, you might get some issue by only using the front-end. Here, we will be creating a simple proxy without any parameters. For those of you who didn’t know, the API is available at your SonarQube URL like the following: https://mySonarQube.url.com/api/.

Generate an API key for your user

  • Go in your account (top right icon)
  • Go under security
  • Generate a new token (The name does not matters, but let’s name it: Sonar.Reports)
  • Copy and paste the key somewhere you won’t loose. Otherwise you will have to re-generate the API token.

Open your project in VS Code or Visual studio

If you’re lucky enough, you will have a Properties folder, otherwise you will have to create it and add the file launchSettings.json

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false, 
    "anonymousAuthentication": true, 
    "iisExpress": {
      "applicationUrl": "http://localhost:21535",
      "sslPort": 44307
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "api/values",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "Sonar.Reports": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "api/values",
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Update your ./Startup.cs and add a new Service configuration:

public void ConfigureServices(IServiceCollection services){
  // ...
  services.AddHttpClient();
  // ...
}

Create the file ./Controllers/SonarController.cs and add the following code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace Sonar.Reports.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class SonarController : ControllerBase
    {
        private HttpClient _sonarHttpClient;

        public SonarController(IHttpClientFactory httpClientFactory){
          _sonarHttpClient = httpClientFactory.CreateClient("sonar");
          _sonarHttpClient.BaseAddress = new Uri("https://sonar.yourSonarQube.com/api/");
          var basicToken = Convert.ToBase64String(Encoding.UTF8.GetBytes("f6d6fecfdcd7836612a0fef4a3f307b7725f8387:"));
          _sonarHttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", basicToken);
        }

        /// <summary>
        /// GET Sonar data
        /// </summary>
        /// <returns>Returns an enumerable of value</returns>
        [ProducesResponseType((int)System.Net.HttpStatusCode.OK)]
        [HttpGet]
        public async Task<IActionResult> Get()
        {
            var result = await _sonarHttpClient.GetAsync("measures/search_history?component=yourComponentName&metrics=bugs%2Ccode_smells%2Cvulnerabilities%2Creliability_rating%2Csecurity_rating%2Csqale_rating&ps=1000");
            
            // Not perfect but it will do for the sample.
            return Content(await result.Content.ReadAsStringAsync());
        }
    }
}

Where:

  • www.yourSonarQube.com is your address of your Sonar Qube
  • The token you’ve created earlier replaces f6d6fecfdcd7836612a0fef4a3f307b7725f8387 (Don’t forget to keep the “:”, it is mandatory in a basic authentication, where it’s usually user:password in Base 64)
  • yourComponentName is replaced by your real component name in Sonar Qube (To get it, open sonar qube, go in your project you want to test, and then take the value from the URL. It’s always there.)

Now, restart your application (Ctrl+C and then re-type dotnet run, if you’re in console mode).

You can test the endpoint by going to http://localhost:5001/swagger/ and use the swagger UI in order to test it. If you have errors, you will see it in your console (or output in Visual Studio).

Add the ChartJS to your project

Go in your root folder where your project is located (csproj) and then type the following command in order to install Chart.Js:

> npm install chart.js --save

Now that you have installed this new dependency, you can add the Chart.Js within your VueJs application.

Edit your ./ClientApp/pages/sonar.vue and add a default Chart.Js available through all their demo on their site. Here, I will be using the line API

<template>
  <div>
    <page-title title="Sonar" />

    <canvas ref="chart" id="canvas"></canvas>
  </div>
</template>

<script>
// Import ChartJS
import Chart from 'chart.js'

// Colors
var chartColors = [
  'rgb(255, 99, 132)',
  'rgb(255, 159, 64)',
  'rgb(255, 205, 86)',
  'rgb(75, 192, 192)',
  'rgb(54, 162, 235)',
  'rgb(153, 102, 255)',
  'rgb(201, 203, 207)'
]

export default {
  data () {
    return {
      chart: null,
      chartConfig: {
        type: 'line',
        data: {
          labels: [],
          datasets: []
        },
        options: {
          responsive: true,
          title: {
            display: true,
            text: 'Enjoy your Chart.Js with Sonar'
          },
          tooltips: {
            mode: 'index',
            intersect: false,
          },
          hover: {
            mode: 'nearest',
            intersect: true
          },
          scales: {
            xAxes: [{
              display: true,
              type: 'time',
              scaleLabel: {
                display: true,
                labelString: 'Date'
              }
            }],
            yAxes: [{
              display: true,
              scaleLabel: {
                display: true,
                labelString: 'Value'
              }
            }]
          }
        }
      }
    }
  },

  mounted () {
    this.chart = new Chart(this.$refs.chart, this.chartConfig)
    this._loadSonarData()
  },

  methods: {
    _loadSonarData: async function () {
      let response = await this.$http.get(`./api/sonar`)
      var sonarData = response.data

      // We now have measures + data
      var measures = sonarData.measures
      let allLabels = []
      for (var m = 0; m < measures.length; m++) {
        let currentColor = chartColors[m % chartColors.length]
        var data = {
          label: measures[m].metric,
          backgroundColor: currentColor,
          borderColor: currentColor,
          tension: 0, // If you don't want bezier curves
          data: [],
          fill: false,
        }

        for (var h = 0; h < measures[m].history.length; h++) {
          let date = new Date(measures[m].history[h].date)
          if (allLabels.indexOf(date) < 0) {
            allLabels.push(date)
          }

          data.data.push(measures[m].history[h].value)
        }
        
        this.chartConfig.data.datasets.push(data)
      }

      this.chartConfig.data.labels = allLabels
      this.chart.update()
    }
  }
}
</script>

Results

If you’ve followed and didn’t make any big mistakes, the result should looks somewhat like the following

Next step?

Basically, yes, there’s a next step. Since you have been able to call the Sonar Qube Api’s, you now know that you can also configure all the requests to it. For example, you could say “I want data for the last 30 days” or in my case, for the last 3 sprints and then draw vertical lines (using a plugin) in order to show exactly where are the sprint and how it went (For example: rush at the end or not).

You could also add a dropdown at the top, or different menu or customization for other projects/stats you’d like to have.

Since you’re using the canvas, don’t forget you could also make a download or extract image using JavaScript. In a way, you could auto-generate a Powerpoint or word document, or other since you have all the data required to do so.

Conclusion

I hope you’ve enjoyed this post.

]]>
https://blog.honosoft.com/2018/11/22/chartjs-with-sonarqube-apis-make-your-own-reports/feed/ 0
Tutorial: Bamboo ➕ NDepend => How to integrate them together https://blog.honosoft.com/2018/11/13/tutorial-bamboo-%e2%9e%95-ndepend-how-to-integrate-them-together/?utm_source=rss&utm_medium=rss&utm_campaign=tutorial-bamboo-%25e2%259e%2595-ndepend-how-to-integrate-them-together https://blog.honosoft.com/2018/11/13/tutorial-bamboo-%e2%9e%95-ndepend-how-to-integrate-them-together/#respond Tue, 13 Nov 2018 12:44:26 +0000 https://blog.honosoft.com/?p=348 Continue Reading]]> This tutorial is intended to help you to integrate NDepend within your current builds or your new builds. In cases you missed it, NDepend is a tool in order to validate your dependencies within your project. You can look at the previous post on HoNoSoFt (French). The topic of this tutorial will be discussing about are the following:

What is Bamboo?

Quickly speaking, Bamboo is the Build server from Atlassian in it’s CI stack. It is often used with the integration of BitBucket and it also offer the possibilities to connect to any other repositories (GitHub included). Bamboo itself allow you to create build project, deployment strategy, interconnection with other Atlassian tools or even send automatically a message with the build status to your #Slack channels. If you already played with Jenkins, AppVeyor or GitLab, you should be already familiar with the daily usage of such a tool. In order to get more details on Bamboo, please follow this link, it will give a full details on Bamboo.

Pre-Requisites

  • Bamboo Server (or Agent) in Windows. In case you need to install a new instance, click here to download Bamboo.
    • It is mandatory to use or install the JDK 1.8 (Or JRE also seems to works) in a folder without any spaces. Any version greater than 8 won’t be working. This is supposed to become supported in the future.
    • If you run a test server locally in order to look how it works, it will be accessible on http://localhost:8085/
    • During the first time wizard a key will be required for activation (30 days or real license). To obtain a 30 days license, go into your Atlassian space or simply use this link in order to request the evaluation license for your account
  • NDepend Build license or Trial active on the build machine (or your current machine, if you run Bamboo locally)
  • PowerShell is not mandatory, but you will see later that it is a must in case you want to have a proper build without failure

Bamboo – Add NDepend Capability

What is a capability in Bamboo

In Bamboo, in order to be able to use any build related tools, you should normally set it for each agent in order to add a specific capabilities. In this case, we will be adding the NDepend capability to a Windows agent. That capability will be pointing to the NDepend.Console.exe coming from the NDepend package previously installed/downloaded. Don’t forget to launch it at least once in order to trigger the license. The details are available in the documentation. In my case, it is installed into [D:\Ndepend\NDepend_2018.2.1.9119]. I prefer to keep the version within the folder name. This is easy to know if my agent is using the proper version of the tool. This capability, if I change the executable folder will affect automatically all the build that was using this capability. It will not, in any cases, re-trigger an automatic build. The variable will then be re-applied starting from the next build.

It is common to keep those capabilities or executable in that screen. It regroup all your agent(s) features. More details on this feature can be found on the Atlassian website.

⚠Be aware: The capabilities displays dependencies with existing builds. That being said, if a capability variable is used within a script, you won’t see anywhere that as an active dependency. For that reason, you are required, in case of multiple agents, to specify manually what Agent should be used for your build stage.

Go to your agent configuration

  1. Click on the cog (options wheel);
  2. Select Agents;
  3. This will make you jump in the administration panel directly on the Agents build resources;
  4. Select your any of your Windows build Agent by simply clicking it.

Agent summary

  1. Validate that your agent name is valid;
  2. Go within the Capabilities tab;
  3. Click on the Add capability link button.

Adding the capability

  1. Capability type: Executable
  2. Type: Command
  3. Executable label: NDepend
  4. Path: [D:\Ndepend\NDepend_2018.2.1.9119\NDepend.Console.exe]

See the Agent specific capabilities

As you can see, after doing the previous steps, you will have your Agent specific capability added. Now, we can use that capability during our build.

Bamboo Build configuration – Capabilities (Easy)

Let’s consider you have your build already existing and that you want to add the NDepend console command. In the current tutorial bellow I will be taking a simple Dotnet Core 2.1 solution. The build will be named IdentityServer4.LdapExtension (GitHub project), where you would have your Git repository, NDepend project and your basic build already configured (checkout and build).

Add a new task to your build stage

  1. Ensure you are in the configuration of your project;
  2. Go to your build job where you usually run your tests and everything else;
  3. Within the tasks, you will find all the detail of your current job;
  4. Add a new task.

Once the popup show,

  1. Search for Command in the search box;
  2. Select Command by clicking it.

Let’s configure the command

  1. The new command task should be already displayed;
  2. The description of the task, in this case: NDepend;
  3. Select the executable you created for your agent capability: NDepend;
  4. Enter the argument to the full path of your NDPROJ  and override the indirs/outdirs parameters, see the Jenkins article for more details. Those path can be relative if you configured your project accordingly, otherwise it will be the full path. In case you don’t set those path properly, NDepend is not able to find your PDB’s files and consequently, do a proper analysis.
    • Example: ${bamboo.build.working.directory}/IdentityServer.LdapExtension.ndproj . You can add a “/silent” is if you don’t want to see all the output logs from NDepend, but I strongly not advise to do this within a build simply because you might need to debug using the logs one day. The indirs/outdirs are omitted considering you have pre-configured your project with relative path instead of the default full path.
  5. Save your configuration

Build your new configuration

I recommend to always build after doing modification. That way it’s easy to see if you caused the build to fail for a real reason (configuration) or if it’s due to code change (triggered builds).

In the case of NDepend, it’s execution returns an exit code. Be aware, the exit code will make your build fails if there’s any quality gate errors. You can read the following when executing the console application:

Notice that NDepend.Console.exe returns a non-zero exit code when at least one Quality Gate fails. This exit code can be used to eventually stop your build process in such situation.

Ref.: https://www.ndepend.com/docs/ndepend-console

The report, even on failure, will exists, however the build will be red and your artifact will probably not be even available. Since there’s no “/ErrorLevel” custom configuration for the exit code, then you will have to do differently in order to ignore those errors.

  • Option 1: Ignore the errors by adding the special tags from NDepend within your code.
    • In that case, simply add the NDepend ignore attributes where required and re-launch a build after your commit.
  • Option 2: Use a script instead of a command in order to control your flow completely and to continue even if there’s a quality gate issue
    • This is the topic of the next section

Bamboo Build configuration – PowerShell Script (Complex)

As stated previously, this is more for those who already have a project having no big issue which cause a quality gate to fail, or if you want to have the Quality Gate failed but still want to generate the reports.

Don’t get me wrong, the Capability created was not for nothing, however, the only drawback in this case is that the Capability will not be detecting our build dependency to NDepend.

In this case, instead of creating a command, we will be creating a script (or add to current Dotnet Core build script)

  1. Go in your Build configuration, default stage (or specific one) and click on Add task;
  2. Select the Script type;
  3. Enter the description and select Windows PowerShell as the Interpreter;
  4. In this case, we will be using the Inline type instead of a PS1 shell script. This option will create automatically a shell script behind the scene. After, simply re-write the code-snippet bellow the image.

The PowerShell is using the existing NDproj (XML file) and then replace the InDirs by the current build path in the command line. This is for your NDproj file that you wouldn’t have any relative path. Once this is completed, it will create the final command and execute it (Invoke-Expression). Here’s the PowerShell script used in the above picture.

# NDProj should normally be a variable in the build, the same goes for other parts. That way
# you can use a build script instead of inline, and that build script can be part of your build
# repository.
$ndproj = "IdentityServer.LdapExtension.ndproj"

# Load the NDepend XML project file.
$ndependXml = [xml](Get-Content .\${ndproj})

# If you have any spaces withn your project, you might encounter issue
# be aware of that. Here the string is a "vanilla" path.
$inDirs = (( `
    $ndependXml.SelectNodes("//Dir") |
    where {$_."#Text" -like "*IdentityServer4.LdapExtension*"} | 
    select -ExpandProperty "#Text" )`
    -replace '^.*(\\IdentityServer4\.LdapExtension\\)',"${bamboo.build.working.directory}\")`
    -join " "

## Launch NDepend command
$expr = "${bamboo.capability.system.builder.command.NDepend} " +`
   "${bamboo.build.working.directory}\${ndproj} " +`
   "/InDirs ${inDirs} " +`
   "/OutDir ${bamboo.build.working.directory}\NDependOut"

Invoke-Expression $expr

# Simple output for the execution result.
write-host "The exit code was: ${LASTEXITCODE}"

Generate the NDepend Artifact

Now, you should be able to build and have the NDepend output folder. In this demo, we have NDependOut as the folder. Todo so, we will have to add the artifact configuration. Please, go back in the Build configuration and your default stage where you have the NDepend script. Once this is complete, select the Artifacts tab and click on Create artifact button.

The fields to input are:

  • Name: NDepend
  • Location: NDependOut
  • Copy pattern: **/*

Once this is completed, you should be able to re-build your project and see if your build artifact is there. Note that if you want to have comparisons between your builds, you could re-use the shared artifact. This is a bit more complex, but it’s possible and it won’t be explored in this tutorial. Let’s say that you could simply avoid cleaning up the build folder every new build. I don’t suggest to do that, but it’s something that could work out.

For example, we have the following successful build:

  1. Green build = success
  2. Go in your artifacts of your build
  3. Click on the artifact name that interest you (NDepend in this case)

Once the artifact will open, it will show the content of the ZIP file. Simply click on the file NDependReport.html. This will then open a link that looks like: http://localhost:8085/artifact/I4E-ID/shared/build-19/NDepend/NDependReport.html#Main

Your page should look similar to this (It depends actually on your project):

Conclusion

Thank you for reading, and I hope it helped you configure your build! If you wish to also integrate with SonarQube, you also can. You simply have to do what is proposed in SonarQube integration with NDepend article and in your Bamboo script, where you build your Dotnet project, you add the NDepend Sonar Runner. The result can then be seen after your build directly within SonarQube. Don’t forget that you might want to install some Marketplace plugin in Bamboo for Sonar in order to send the data. In case you don’t want to use a plugin, don’t worry, it is also possible.

]]>
https://blog.honosoft.com/2018/11/13/tutorial-bamboo-%e2%9e%95-ndepend-how-to-integrate-them-together/feed/ 0
Ldap + Identity Server Series ー Part I ー OpenLdap on docker container https://blog.honosoft.com/2018/06/18/ldap-identity-server-series-%e3%83%bc-part-i-%e3%83%bc-openldap-on-docker-container/?utm_source=rss&utm_medium=rss&utm_campaign=ldap-identity-server-series-%25e3%2583%25bc-part-i-%25e3%2583%25bc-openldap-on-docker-container https://blog.honosoft.com/2018/06/18/ldap-identity-server-series-%e3%83%bc-part-i-%e3%83%bc-openldap-on-docker-container/#comments Mon, 18 Jun 2018 12:32:09 +0000 https://blog.honosoft.com/?p=68 Continue Reading]]> Objectives

This is a multi-part tutorial playground in order to build, for dev or for some of your needs, an integration of:

  • [Part I*] OpenLdap (Or active directory) using Contoso Users
  • [Part II] IdentityServer 4 with OpenLdap (Built in Dotnet Core 2.x)
    • Option of using Redis to persist user cache (tokens)
  • [Part III] Docker to run the previous two options all together (If not already explained in previous 2)
  • [Part IV] A small back-end with protected Api’s
    • One in Dotnet Core 2 using the WebApi format
    • One in NodeJS
  • [Part V] A small front-end to call the Api’s
    • One in Dotnet Core 2 using the WebApp MVC format
    • One in NodeJS

Requirements for this part

  • Docker (Windows, Linux or Mac)

Let’s begin [Part I]

In this part, if you are using Active Directory, it will be easy to already find a VM having the Contoso users already within Active Directory. However, if you are using OpenLdap, you won’t find anything. For development purpose it’s nice to have an already populated Ldap database. For some reason, I will stay with the default schema from OpenLdap. The schema differ from Active Directory.

In this part we will cover:

  • How to create an empty OpenLdap Server
  • How to start the Server
  • How to convert a csv file to ldif format
  • How to import within OpenLdap Server
  • How to see the data (tool)

How to create an empty OpenLdap Server

You have the choice here of reading all the documentation to install step-by-step or speed up by using a Docker image. For a production environment I would strongly suggest to read all the documentation regarding OpenLdap. It will helps to avoid security holes and also to build properly your Ldap tree.

Let’s pull the best image I’ve found so far for OpenLdap (osixia/openldap)

$ docker pull osixia/openldap:latest

The download should be pretty quick since the image size is not so big. At the time of writing this, the image size on the disk was about `222 MB`. The Github site from that image help you to configure it and also indicate how to have persistance and so much more. Let’s do a quick `Contoso` configuration:

Create an OpenLdap environment file

This file will basically set the Base DC and Admin/Config/Read-Only & {user} + {password}.

  • File: ./ldap.env
LDAP_ORGANISATION=Contoso Ltd.
LDAP_DOMAIN=contoso.com
LDAP_ADMIN_PASSWORD=P@ss1W0Rd!
LDAP_CONFIG_PASSWORD=P@ss1W0Rd!

LDAP_READONLY_USER=true
LDAP_READONLY_USER_USERNAME=ldap-ro
LDAP_READONLY_USER_PASSWORD=P@ss1W0Rd!
  • Create a folder to put shared files within Docker and OpenLdap
    • ./ldap/

Create a docker compose file

The docker-compose  file will be useful to start/restart or reconfigure the image. I prefer the docker-compose version to the all-in-one command line ( docker run ... ). It is still up to you! 😉

File ./docker-compose.yml

ldap:
   container_name: contosoOpenLdap
   image: osixia/openldap:latest
   ports: 
     - "389:389"
     - "636:636"
   env_file:
     - ./ldap.env
   volumes:
     - ./ldap:/data/ldif

How to start the Server

Within the directory where you have the docker-compose.yml file, you should be able to use docker-compose executable (if in windows) or if you are on Linux you might have to also install that package.

$ docker-compose start
  • This will be starting the server. If you prefer to see all the log within the screen, you can instead use docker-compose up command.

How to convert a csv file to ldif format

First of, what is a ldif file? Easy reading on Wikipedia. Basically, it is a Ldap Data Interchange Format. It gives the possibility to add, remove, modify (by properties if we want) or even delete using a specific text format. The format in itself is not complex, the issue you will encounter is that the attributes might not be defined (OpenLdap VS Active Directory). By example the sAMAccount exists in Active Directory and in OpenLdap it might be the uid attribute, depending on your organization.

I have created a simple project named csv2ldif (far from perfect) that can quickly convert a simple CSV format into a ldif format with a default password of P@ss1W0Rd! (Yes again, let’s keep it simple). If you want to change the default password, please do, it’s not so much work. You can also contribute to the repository. It will generate basically 2 files. The first one will be about creating the user groups. The second one is about creating the user and then assigning them to the group. In that project you can also find the CSV file regarding the Contoso Users (with also the pre-generated demo in the example folder).

Regarding the LDIF generation, you can find good information on the LdapWiki.

Small peek (Raw csv)

dn,objectClass,uid,homeDirectory,group,givenName,sn,displayName,cn,mail,manager,telephoneNumber,title
cn=Dan Jump,inetOrgPerson;person;organizationalPerson;posixAccount;top,danj,/home/danj,Executive,Dan,Jump,Dan Jump,Dan Jump,danj@contoso.com,,(425) 555-0179,CEO
cn=Adam Barr,inetOrgPerson;person;organizationalPerson;posixAccount;top,adamb,/home/adamb,Operations,Adam,Barr,Adam Barr,Adam Barr,adamb@contoso.com,cn=Dan Jump,(206) 555-5472,General Manager of Professional Services

Note that the Base OU for the groups and users are not generated. Usually you already have this in your tree. In our case we have something empty.

Create the groups and users OU using a LDIF file

dn: ou=users, dc=contoso,dc=com
ou: users
description: All people in organisation
objectclass: organizationalunit

dn: ou=groups, dc=contoso,dc=com
ou: groups
description: All Groups in organisation
objectclass: organizationalunit

Small peek (Generated groups LDIF)

dn: cn=Executive,ou=groups,dc=contoso,dc=com
objectClass: groupOfUniqueNames
cn: Executive
uniqueMember: cn=Executive,ou=groups,dc=contoso,dc=com
description: Executive

dn: cn=Operations,ou=groups,dc=contoso,dc=com
objectClass: groupOfUniqueNames
cn: Operations
uniqueMember: cn=Operations,ou=groups,dc=contoso,dc=com
description: Operations

Small peek (Generated users LDIF)

dn: cn=Dan Jump,ou=users,dc=contoso,dc=com
objectClass: inetOrgPerson
objectClass: person
objectClass: organizationalPerson
objectClass: posixAccount
objectClass: top
uid: danj
uidNumber: 70000
gidNumber: 70001
homeDirectory: /home/danj
givenName: Dan
sn: Jump
displayName: Dan Jump
cn: Dan Jump
mail: danj@contoso.com
telephoneNumber: (425) 555-0179
title: CEO
userPassword: {SHA}RKkNn7+KoG94IN3x/B2jnm/4DS0=

dn: cn=Executive,ou=groups,dc=contoso,dc=com
changetype: modify
add: uniqueMember
uniqueMember: cn=Dan Jump,ou=users,dc=contoso,dc=com

dn: cn=Adam Barr,ou=users,dc=contoso,dc=com
objectClass: inetOrgPerson
objectClass: person
objectClass: organizationalPerson
objectClass: posixAccount
objectClass: top
uid: adamb
uidNumber: 70002
gidNumber: 70003
homeDirectory: /home/adamb
givenName: Adam
sn: Barr
displayName: Adam Barr
cn: Adam Barr
mail: adamb@contoso.com
manager: cn=Dan Jump,ou=users,dc=contoso,dc=com
telephoneNumber: (206) 555-5472
title: General Manager of Professional Services
userPassword: {SHA}RKkNn7+KoG94IN3x/B2jnm/4DS0=

dn: cn=Operations,ou=groups,dc=contoso,dc=com
changetype: modify
add: uniqueMember
uniqueMember: cn=Adam Barr,ou=users,dc=contoso,dc=com

If you want the entire data don’t forget to just go on https://github.com/Nordes/Csv2Ldif/tree/master/example. There’s the 00, 01, 02 sample file where it have the entire Contoso setup.

How to import within OpenLdap Server

Right now, we have an empty OpenLdap Server. What we want is a server with some users and groups in order for us to play with them within an application or during our Devs.

Download the following files (if not already) and save them within the ./ldap/ folder you’ve created earlier:

  1. 00-startup.ldif
    • Create the users and groups OU (Organisation Unit) (ou=…,dc=contoso,dc=com)
  2. 01-output-groups.ldif
    • Create all the  groups CN within the  groups OU (cn=…,ou=groups,dc=contoso,dc=com)
  3. [02-output-users.ldif](https://github.com/Nordes/Csv2Ldif/blob/master/example/02-output-users.ldif)
    • Create all the users CN within the users OU (cn=…,ou=users,dc=contoso,dc=com)
    • Attach all the users to their pre-defined group. In our case it’s a 1-1 match.

Import

It’s now time to launch the command line tool (if possible where you have your ldif file).

Our action parameter will be using the following:

  • -f: Import the file launching the ldapmodify command within the container
  • -d: Use the administrator role to import
  • -w: The administrator password (if not set, but with the -w option, it will request the password). In case the password is not set, it will fail.
  • -c: Continue on failure (we never know, but at least it will give the logs)
  • …: The other one are trivial.

The output result when launching the command are describing what’s happening. By example

...
adding new entry "cn=Josh Edwards,ou=users,dc=contoso,dc=com"
modifying entry "cn=Sales,ou=groups,dc=contoso,dc=com"
...

If you are too lazy to type the commands, you can copy the batch file located at the same emplacement as the 3 previous downloads

Organisation Units

PowerShell uses the back tick &#96; for multiline, however on linux it will be \ character.

$ docker exec contosoOpenLdap ldapmodify \
-a -x -h localhost -p 389 \
-D "cn=admin,dc=contoso,dc=com" \
-f /data/ldif/00-startup.ldif \
-w P@ss1W0Rd! \
-c

Groups

$ docker exec contosoOpenLdap ldapmodify \
-a -x -h localhost -p 389 \
-D "cn=admin,dc=contoso,dc=com" \
-f /data/ldif/01-output-groups.ldif \
-w P@ss1W0Rd! \
-c

Users and membership

$ docker exec contosoOpenLdap ldapmodify \
-a -x -h localhost -p 389 \
-D "cn=admin,dc=contoso,dc=com" \
-f /data/ldif/02-output-users.ldif \
-w P@ss1W0Rd! \
-c

How to see the data (tool)

You have some choice here, but since I am not a big fan of doing the query all by hand and do the request through the OpenLdap Docker instance, I prefer to use LdapAdmin. It gives a UI which is easy to play with.

When creating a connection you will need to set the following:

What Value Desc.
Connection Name OpenLdap Contoso Test Name of the connection (can be what you want)
Host localhost Since we’re running thing locally, localhost is good
port 389 Default port (unsecured). The secured is supposed to be also available.
base dc=contoso,dc=com Base DC when opening the connection
Auth type Simple Authentication Option under the base
Username cn=admin,dc=contoso,dc=com User to connect with (let’s use Admin, but you can also use the ldap-ro or other user)
Password P@ss1W0Rd! Admin password in this case

Look After Import

ldap-Imported

ldap-Imported

 

The end! See you soon for the next part.

]]>
https://blog.honosoft.com/2018/06/18/ldap-identity-server-series-%e3%83%bc-part-i-%e3%83%bc-openldap-on-docker-container/feed/ 13