├── .gitignore ├── LICENSE ├── README.md ├── basic-securepwd └── run.ps1 ├── basic ├── README.md └── run.ps1 ├── basic_userpwd ├── README.md ├── run.inputPwd.ps1 └── run.ps1 ├── basic_winauth ├── README.md ├── my │ └── README.md └── run.ps1 ├── git-sync ├── .gitignore ├── README.md ├── my │ ├── AdditionalSetup.ps1 │ ├── HelperFunctions.ps1 │ ├── MainLoop.ps1 │ ├── SetupVariables.ps1 │ ├── gitsync │ │ ├── DeployTSQL.ps1 │ │ ├── SCM.sp_InsertToObjLog.sql │ │ ├── SCM.sp_UpsertToObjMetadata.sql │ │ ├── SCM.t_ObjLog.sql │ │ ├── SCM.t_ObjectMetadata.sql │ │ └── SCM.tr_ObjectTrigger.sql │ └── ps2exe.ps1 └── run.ps1 ├── gmsa ├── CredentialSpec.psm1 ├── New-gMSA.ps1 ├── README.md └── my │ ├── AdditionalSetup.ps1 │ ├── HelperFunctions.ps1 │ ├── MyCustomScripts │ ├── InstallModules.ps1 │ └── SetupMyUsers.ps1 │ ├── SetupConfiguration.ps1 │ ├── SetupVariables.ps1 │ └── ps2exe.ps1 ├── helpers ├── Get-NavVersion.ps1 ├── Get-NavVersionDir.ps1 ├── Get-NavVersionMajor.ps1 ├── Get-Pwd.ps1 ├── Get-PwdSecured.ps1 ├── Init-Environment.ps1 └── Test-NavImage.ps1 ├── local_cside ├── README.md ├── my │ ├── AdditionalSetup.ps1 │ ├── HelperFunctions.ps1 │ └── ps2exe.ps1 └── run.ps1 ├── media ├── basic_containerStarted.jpg ├── basic_userpwd_containerList.jpg ├── basic_userpwd_containerStarted_01.jpg ├── basic_userpwd_containerStarted_02.jpg ├── basic_userpwd_dockerInspect_01.jpg ├── basic_winauth_clickOnceInstallation.jpg ├── basic_winauth_clickOnce_RTC.jpg ├── basic_winauth_containerStarted.jpg ├── basic_winauth_securePwd_inspect.jpg ├── basic_winauth_vsCode_changeLocale.jpg ├── basic_winauth_vsCode_installExtensionCmd.jpg ├── basic_winauth_vsCode_launchConfigAndDownloadSymbols.jpg ├── git-sync_demo_01.gif ├── local_cside_containerStarted.jpg ├── local_cside_myFolderContent.jpg ├── local_cside_myTableCSIDE.jpg ├── local_cside_myTableExtension.jpg ├── local_cside_rtcFolderContent.jpg ├── local_cside_runningCside.jpg ├── local_cside_runningRtc.jpg ├── share_mount_addins_listFolderContent.jpg ├── share_mount_addins_viewFolder.jpg ├── swarm_winauth_containersList.jpg ├── swarm_winauth_createSecret.jpg ├── swarm_winauth_dockerInfo.jpg ├── swarm_winauth_mulitreplicasListContainers.jpg ├── swarm_winauth_mulitreplicasLogs.jpg ├── swarm_winauth_mulitreplicasPingA.jpg ├── swarm_winauth_mulitreplicasPingB.jpg ├── swarm_winauth_scaleReplicas.jpg ├── swarm_winauth_servicesListAndLog.jpg └── swarm_winauth_verifyPwdFromTheContainer.jpg ├── share_mount_addins ├── Add-ins │ └── README.md ├── README.md └── run.ps1 └── swarm_winauth ├── README.md ├── my └── SetupVariables.ps1 └── run.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | _license.flf 2 | *.key 3 | RoleTailored Client 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jakub Vaňák 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Examples and use-cases for MS Dynamics NAV on Docker 2 | 3 | ## !!! IMPORTANT !!! 4 | 5 | At this moment, all examples use Docker images for MS Dynamics NAV provided by Microsoft. 6 | 7 | ~~Microsoft at the moment doesn\`t provide the images in the public repositories (e.g. [Docker Hub](https://hub.docker.com/)). Instead, there is a private repository for testing purposes only. Currently, they have been opened to give the access to the private/testing repository to *anyone* interested and willing to do some tests and provide a feedback. This can change at any moment I suppose.~~ 8 | 9 | Microsoft has recently started with publishing of the images into the official [Docker Hub](https://hub.docker.com/r/microsoft/dynamics-nav/). Currently anyone can start using NAV on Docker. Please, read all the information in the repository to understand which version will be published in the repo, how to localize them and for what purposes you are allowed to use them. Microsoft, many thanks!!! 10 | 11 | Also, you can visit Microsoft GitHub repository [nav-docker](https://github.com/Microsoft/nav-docker) with the source code they use to build the images. There, you can also register any issue that will appear during the testing. 12 | 13 | It is pretty possible that some examples could fail because of the breaking changes in the sources images. Please, in this case I will appreciate your feedback (create an issue). 14 | 15 | ## PREREQUISITES AND GENERAL SETUP 16 | - [Docker](https://www.docker.com/) has to be installed and properly configured on your **Win10** / **WinServer2016** (or higher) machine. Some examples will need some extra setups but those will be described for each example explicitly. 17 | - If you want to install **Docker EE** on your Windows Server you can use [InstallDockerEE.ps1](https://gist.github.com/Koubek/1831c2aba7f558de4b1461476105ba85) script that will install **Containers** Windows Feature and then **Docker EE**. The script can be used also to upgrade your current version. It detects an existing (installed) **Docker EE** version present on your host and let you compare your version with the newest one available to download and install. If you confirm the new version will be downloaded and installed. This is very useful in case you are waiting for a specific Docker release and you want to check if this release has been already pushed into the repo or not. 18 | - By default, we will be using **NAT** network which is the default one configured during the Docker installation process. You can find more details about [Docker networking here](https://docs.microsoft.com/virtualization/windowscontainers/manage-containers/container-networking). 19 | - All examples specify NAV docker image using `${NAV_DOCKER_IMAGE}` variable. This gives us some sort of flexibility in case Microsoft migrate the repository or change the name of the images, tags etc. 20 | - So the first step, before you run any script including `docker run` command, is setting the variable. For example: 21 | ```powershell 22 | # Private Microsoft Repository for NAV previews and for internal purposes. 23 | $NAV_DOCKER_IMAGE = 'navdocker.azurecr.io/dynamics-nav:devpreview' 24 | 25 | # Official images available on Docker Hub (I will use the last W1 version): 26 | $NAV_DOCKER_IMAGE = 'microsoft/dynamics-nav' 27 | ``` 28 | 29 | ## EXAMPLES 30 | 31 | - [basic](basic) - This is the most elemental example. I would recommend running exactly this one at the very first moment to validate that everything is working fine. You specify the minimum of the parameters. 32 | 33 | - [basic with user+pwd defined](basic_userpwd) - Similar to the previous one but you specify **user name**, **user pwd**, **container hostname**, **container name**. There are also described some security concerns (security of the password you use). The example includes two variants. 34 | 35 | - [winauth (shared) + VS Code](basic_winauth) - This example demonstrates *shared* Windows authentication. We will also see new security approach that will protect your password. This security approach is applicable to any authentication mechanism (so the WinAuth is not the only one) that requires providing a password in an explicite way. 36 | 37 | We will created and published **ClickOnce** package. And finally, we will try to connect from **VS Code** to the container`s **dev services**. 38 | 39 | - [winauth on Docker Swarm + Secrets](swarm_winauth) - One of the advanced scenarios. We will increase the security of your credentials using Docker Swarm\`s Secrets. We will also talk about the **scaling** capabilities of the *Docker Swarm*. You will need to promote your docker host on the [Docker Swarm](https://docs.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/swarm-mode) node. But don\`t worry, this is actually quite easy to do. 40 | 41 | - [share data using mounts](share_mount_addins) - In case you need to share (for example) **add-ins** between your *Docker host* and containers *Docker Volumes* would be probably the easiest way for you. 42 | 43 | - [locally copied C/SIDE](local_cside) - An example that will demonstrate how to copy *client folder* down to your docker host to be able to access **C/SIDE** without installing it. You *don\`t need* to run **gMSA**. Actually, I use the *WinAuth hack* (mentioned before) in the example. 44 | 45 | - [gMSA](gmsa) - This is one of the most complex examples, it will be updated when I have some time. I am starting with the scripts, later I'll improve them and I'll add the documentation. 46 | -------------------------------------------------------------------------------- /basic-securepwd/run.ps1: -------------------------------------------------------------------------------- 1 | $hostname = "navex-basic-securepwd" 2 | 3 | # Create AES key to encrypt the password: 4 | $KeyFile = ".\my\myAES.key" 5 | $Key = New-Object Byte[] 16 # You can use 16, 24, or 32 for AES 6 | [Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($Key) 7 | $Key | out-file $KeyFile 8 | 9 | $passsec = Read-Host 'Input the user`s password' -AsSecureString 10 | $passsec = ConvertFrom-SecureString $passsec -Key $Key 11 | 12 | docker run ` 13 | --rm ` 14 | -m 3G ` 15 | --name $hostname ` 16 | --hostname $hostname ` 17 | -v $PSScriptRoot\my:c:\run\my ` 18 | -e Accept_eula=Y ` 19 | -e Auth=Windows ` 20 | -e username=$env:USERNAME ` 21 | -e securePassword=$passsec ` 22 | -e passwordKeyFile='c:\run\my\myAES.key' ` 23 | -e clickonce=Y ` 24 | ${NAV_DOCKER_IMAGE} -------------------------------------------------------------------------------- /basic/README.md: -------------------------------------------------------------------------------- 1 | # Examples and use-cases for MS Dynamics NAV on Docker 2 | 3 | ## BASIC EXAMPLE 4 | 5 | This is the most elemental example. I would recommend to run exactly this one at the very first moment to validate that everything is working fine. You have to specify very limited amount of the parameters. 6 | 7 | I will explain some of them: 8 | - `--rm` - This is the Docker parameter that will remove automatically the container in case of the failure or in case the container internal lifetime expires. The second case is not expected in case of **nav-docker** containers as the provided images contain a loop. The loop is being executed after the internal services has been configured and executed. The loop is infinite. 9 | 10 | - `-m 3G` - Sets the memory limits. Actually, this parameter shouldn\`t be required but we so same problems related with memory and using this parameters we *solved* (of course, this is not the best solution) them. Maybe you can try running the containers without it and give some feedback. 11 | 12 | - `-e "Accept_eula=Y"` - Required [environment variable](https://docs.docker.com/engine/reference/run/#env-environment-variables). Practically all `-e` parameters are **nav-docker** related and will affect the behavior of the container. The meaning of `Accept_eula` should be pretty clear - **you accept EULA**. Without accepting EULA the container won\`t start correctly (will fail). 13 | 14 | If you fire the script your docker environment will probably need to download the required layers. This will take some time depending on your internet connection, your machine (CPU and HDD performance). If you for example have [microsoft/windowsservercore](https://hub.docker.com/r/microsoft/windowsservercore/) layer already present the required time will be reduced drastically. 15 | 16 | After all layers are downloaded the container will be initialized and started. After the container starts up the internal scripts (images scripts) will configure everything needed and all required services will be started. 17 | 18 | At the end you should be able to see something like this: 19 | ![](../media/basic_containerStarted.jpg) 20 | 21 | The container provides generated information like: 22 | - NAV user name, NAV user PWD (in this example **NavUserPassword** authentication will be used. 23 | - Container name (hostname) and its IP. 24 | - Web client link. 25 | - In my case (image with **NAV dev preview**) also link to access from Visual Studio Code. 26 | - Dynamics NAV instance name which is **NAV** by default. 27 | - **VSIX** extension/plugin for Visual Studio Code. 28 | - Certificate (public key) as a self signed certificate has been created to encrypt the communication with NAV services. 29 | 30 | ### You should write down the data to be able use them later. -------------------------------------------------------------------------------- /basic/run.ps1: -------------------------------------------------------------------------------- 1 | docker run ` 2 | --rm ` 3 | -m 3G ` 4 | -e "Accept_eula=Y" ` 5 | ${NAV_DOCKER_IMAGE} -------------------------------------------------------------------------------- /basic_userpwd/README.md: -------------------------------------------------------------------------------- 1 | # Examples and use-cases for MS Dynamics NAV on Docker 2 | 3 | ## BASIC EXAMPLE + USER & PASSWORD 4 | 5 | This is a bit more advanced example but still very simple one. You specify **user name**, **user password** (these won\`t be generated by the container itself) and something more but the amount of the parameters is still very limited. 6 | 7 | Let\`s review specific parameters we use in this example (by default we will be talking about `run.ps1`): 8 | - `--name=navex-basic-userpwd_container` - This parameter sets the name of the container you about to create. Keep in mind the container names must be unique on one docker host (you can\`t create two containers with the same name; you need to kill the first one using `docker rm [container_name]` / `docker rm -f [container_name]` and then you can create the second one). 9 | 10 | **Note:** 11 | `run.inputPwd.ps1` defines `$hostname` and use it later to set `--name` parameter, as well as `--hostname` parameter. This means **container hostname** = **container name**. 12 | 13 | - `--hostname=navex-basic-userpwd` - Hostname of the container. 14 | 15 | - `-e "username=NavUser1"` - Required in this example. Name of the NAV user specified by *you*. 16 | 17 | - `-e "password=NavUser1Password"` - Required in this example. Again, *you* specify the password for the NAV user. 18 | 19 | **Note:** 20 | The password will be passed as a plain text. Anyone with access to the Docker API can reveal it using `docker inspect [container_name]` at any moment (even if the container has been stopped but not removed). 21 | 22 | You can see there are two examples: 23 | - `run.ps1` - The default one with everything (pwd) included in the file. 24 | - `run.inputPwd.ps1` - We can consider this variant a little bit better as the password isn\`t included in the file and you need to present it when running the script. 25 | 26 | `docker inspect [container_name]` still reveal the password in both cases!!! 27 | 28 | - Output of the `run.ps1` 29 | 30 | ![](../media/basic_userpwd_containerStarted_01.jpg) 31 | 32 | You can see **hostname** is that one we defined in the script using `--hostname` parameter. 33 | 34 | - Output of the `run.inputPwd.ps1` 35 | 36 | ![](../media/basic_userpwd_containerStarted_02.jpg) 37 | 38 | ~~You can see the password is being displayed on the screen so the purpose of the input dialog is very limited. The password is not being stored on the docker host but still, you can see it in the log.~~ This was reported and fixed: [Issue #7](https://github.com/Microsoft/nav-docker/issues/7). 39 | 40 | - `docker ps` displays both containers. Both of them are running and can coexist because the container names are different. 41 | 42 | ![](../media/basic_userpwd_containerList.jpg) 43 | 44 | # !!! How can be revealed the password value !!! 45 | 46 | - The output of the container: 47 | 48 | - Of course, you can run any container in the detached mode but still, you can use `docker logs [container_name]`. This command will output the log so you can see the same data you can see in the *interactive* mode. 49 | 50 | - You can run `docker inspect [container_name]` which displays a *JSON* string with a lot of metadata describing the container. All input parameters are included!!! 51 | 52 | - The following displays only **env** variables (this is where the pwd is being stored): `docker inspect --format '{{ index .Config.Env }}' navex-basic-userpwd` 53 | 54 | ![](../media/basic_userpwd_dockerInspect_01.jpg) 55 | -------------------------------------------------------------------------------- /basic_userpwd/run.inputPwd.ps1: -------------------------------------------------------------------------------- 1 | $hostname = "navex-basic-userpwd" 2 | 3 | $passsec = Read-Host 'Input the user`s password' -AsSecureString 4 | $passplain = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($passsec)) 5 | 6 | docker run ` 7 | --rm ` 8 | -m 3G ` 9 | --name $hostname ` 10 | --hostname $hostname ` 11 | -e "Accept_eula=Y" ` 12 | -e "username=NavUser1" ` 13 | -e "password=$passplain" ` 14 | ${NAV_DOCKER_IMAGE} 15 | 16 | $passplain = $null -------------------------------------------------------------------------------- /basic_userpwd/run.ps1: -------------------------------------------------------------------------------- 1 | docker run ` 2 | --rm ` 3 | -m 3G ` 4 | --name "navex-basic-userpwd_container" ` 5 | --hostname "navex-basic-userpwd" ` 6 | -e "Accept_eula=Y" ` 7 | -e "username=NavUser1" ` 8 | -e "password=NavUser1Password" ` 9 | ${NAV_DOCKER_IMAGE} -------------------------------------------------------------------------------- /basic_winauth/README.md: -------------------------------------------------------------------------------- 1 | # Examples and use-cases for MS Dynamics NAV on Docker 2 | 3 | ## SHARED WINDOWS AUTHENTICATION + CLICKONCE + VISUAL STUDIO CODE 4 | 5 | In this case, we will describe a simple solution from the Docker perspective. Our command `docker run` will be pretty straightforward and if you passed through the previous examples you will have no problem to understand and achieve same results as I do. 6 | 7 | Let\`s say a word about the **Windows authentication** for **Windows Containers**. Containers **by default** don\`t provide mechanisms that will enable real Windows authentication for domain users. This means you can\`t specify domain accounts for the services (for example) running inside the containers. And in a very similar way, you can\`t authenticate yourself using your domain account against the services and solutions running inside the containers. 8 | 9 | As I have already mentioned - this behavior comes by default. But you have two options how to solve WinAuth problem: 10 | 11 | - You can provide your credentials explicitly (pass your windows credentials into the container) and benefit from the fact that Windows will authenticate you correctly even in the case you are on the domain (your Docker host) and the container has its own workgroup inside. 12 | You can see that the solution we have just mentioned improves somehow our possibilities, it is the easy-to-achieve solution. 13 | 14 | ~~On the other hand, we still need to provide our credentials and those can be revealed using the techniques mentioned in the previous example.~~ 15 | 16 | Here we need to mention a really [nice contribution](https://github.com/Microsoft/nav-docker/pull/76) from [Michael Megel](https://www.linkedin.com/in/michaelmegel/). This is a real game changer that improves the security of your password. He (Michael) uses secure password string in combination with a cryptographic key. The encrypted password is being sent via an **env** parameter into the container and the **key** is being sent into the container as a **file**. In the container, he uses the **key** to decrypt the **encrypted password** and delete the key file. If anyone fires `docker inspect [container_name]` he won\`t see anymore your password in a plain text. Instead, the encrypted password will appear. Really nice solution and many thanks to [Michael Megel](https://www.linkedin.com/in/michaelmegel/)!!! 17 | 18 | I need to say that I was a bit skeptic at the very first moment because you could pass your password directly via the file (as he does with the **key**) and delete it once received by the container (again, the **key** is being deleted when retrieved inside the container). But now I can see there is one advantage - you split your security information into two separate pieces. Each of them is useless without the pair information. It is still possible to hack a password being passed in this way but it would be more complicated to hack it. Michael, sorry that I was so blind at the very first moments!!! 19 | 20 | Here, in the example, the key uses 16 randomly generated bytes and is being sored here: `$KeyFile = ".\my\myAES.key"`. This file will be mapped into the container once the container has been initialized. 21 | 22 | - You can use [gMSA](https://technet.microsoft.com/en-us/library/jj128431(v=ws.11).aspx) and solve the integration with the domain properly. Unfortunately, this is not so easy to achieve. There are few prerequisites you must fulfill. One of them and this one is the key one, goes against your **Domain Controller**. **"The Active Directory schema in the gMSA domain’s forest needs to be updated to Windows Server 2012 to create a gMSA."** I can see this requirement can be a real problem for some partners. An even worse situation can be seen in the case of the partners\` customers. There are many companies with the domain\`s forest on the 2003 level. As the objective of the example is the previous (the less secure one) solution we won\`t discuss any other details related to **gMSA** right now. There will be one or more examples focused on the **gMSA** solution. 23 | 24 | 25 | ### Specific `docker run` parameters in the example are: 26 | 27 | - `-e Auth=Windows` - Required in this example. Alternatively, you can also use `-e WindowsAuth=Y` to achieve the same - setting the container (NAV services) to work in Windows Authentication mode. 28 | 29 | - `-e username=$env:USERNAME` - Required in this example. `$env:USERNAME` will automatically set your Windows user account (**without** the domain name part, this is very important). 30 | 31 | - `-e securePassword=$passsec` - Required in this example. Your encrypted password for the provided user account. The password (before it is being encrypted) must match with your Windows account password!!! You can see we use a different approach that was demonstrated in the [previous example](../basic_userpwd) (in the second variant). This newer alternative is much better in terms of security (you split the password into two pieces and each of them is being transferred in a different way; one of them will be deleted once the original password will be restored in the container and the second one itself will remain useless to anyone without the deleted part). 32 | 33 | - `-v $PSScriptRoot\my:c:\run\my` - Required in this example. The shared folder will be used to pass the **cryptographic key** into the container. 34 | 35 | - `-e passwordKeyFile='c:\run\my\myAES.key'` - Required in this example. The path and the name of the **cryptographic key** that will be used to encrypt the password inside the container. This file will be automatically deleted (you can eventually suppress the deletion using `-e RemovePasswordKeyFile=Y`). 36 | 37 | - `-e clickonce=Y` - The container will create and publish **ClickOnce** package. This gives us the possibility to access NAV using Windows Client (aka RTC) and access natively using our Windows credentials. 38 | 39 | --- 40 | 41 | ## The output of the `run.ps1` script: 42 | 43 | ![](../media/basic_winauth_containerStarted.jpg) 44 | 45 | We can see that there is no information about the user (user name and user password are not present). 46 | 47 | If we run `docker inspect navex-basic-securepwd` we will see that the password is still there but it is visible in the encrypted form and without the key, anybody won\`t be able to reveal it. I repeat that the key is being deleted automatically once the password was decrypted in the container (practically within a few seconds after the container was started). 48 | 49 | ![](../media/basic_winauth_securePwd_inspect.jpg) 50 | 51 | ## CLICKONCE 52 | 53 | You can see there is a new link to download "ClickOnce Manifest". Use the link, open the web page and download the manifest clicking on **Install now**. 54 | 55 | ![](../media/basic_winauth_clickOnceInstallation.jpg) 56 | 57 | 58 | Run the manifest. After that, you should be able to see running RTC (no password will be required). 59 | 60 | ![](../media/basic_winauth_clickOnce_RTC.jpg) 61 | 62 | **Note:** 63 | If you are using Google Chrome (I usually do) and see some security errors when downloading and/or running the manifest, I would recommend for example **Internet Explorer** or **Edge**. Both of them work fine for my (I don\`t use *recommended settings* in IE). 64 | 65 | 66 | ## VISUAL STUDIO CODE - INSTALL THE EXTENSION / PLUGIN: 67 | 68 | I suppose you have [Visual Studio Code](https://code.visualstudio.com/) already installed and have some experience with it. 69 | 70 | Probably you have already mentioned there is one link in the *container output table* with the extension **vsix**. This is the extension you need to add into your *VS Code*. 71 | 72 | Before you install the extension the **vsix** file should be downloaded. Once you have downloaded your file, go to *VS Code*, run **Command Palette** (*Ctrl + Shift + P*) and run `Extensions: Install from VSIX...`: 73 | 74 | ![](../media/basic_winauth_vsCode_installExtensionCmd.jpg) 75 | 76 | Then you select the **vsix** file you have previously downloaded and you ready to start with development :) 77 | 78 | **Note:** 79 | You can eventually download the **vsix** file directly from *VS Code* in the same dialog you specify the **vsix** file. Just put the link instead of any file and the file will be downloaded and installed. It is probably the easiest way but maybe a bit less transparent than the previous one. 80 | 81 | 82 | ## VISUAL STUDIO CODE - SETUP THE PROJECT: 83 | 84 | You can use any of the sample projects you can find online (for example). You can create an empty project and this is exactly I am going to describe here. 85 | 86 | - Run *Ctrl + Shift + P* and run `AL: Go!` command. This will create a new project with all required files. 87 | 88 | - Change `locale` property in `app.json` file. Switch from **US** to **W1**: 89 | 90 | ![](../media/basic_winauth_vsCode_changeLocale.jpg) 91 | 92 | - Configure `launch.json` file to point to your instance running inside the container (use **Dev. Server** property`s value from the *container output table*): 93 | 94 | ![](../media/basic_winauth_vsCode_launchConfigAndDownloadSymbols.jpg) 95 | 96 | Now you just click on **Download Symbols**, wait a while until the download is finished. And now you can start with your development, you have everything ready to go!!! -------------------------------------------------------------------------------- /basic_winauth/my/README.md: -------------------------------------------------------------------------------- 1 | Here you can override the standard scripts. 2 | 3 | This folder will be used in this example to pass the cryptographic key into the container. -------------------------------------------------------------------------------- /basic_winauth/run.ps1: -------------------------------------------------------------------------------- 1 | $hostname = "navex-basic-securepwd" 2 | 3 | # Create AES key to encrypt the password: 4 | $KeyFile = ".\my\myAES.key" 5 | $Key = New-Object Byte[] 16 # You can use 16, 24, or 32 for AES 6 | [Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($Key) 7 | $Key | out-file $KeyFile 8 | 9 | $passsec = Read-Host 'Input the user`s password' -AsSecureString 10 | $passsec = ConvertFrom-SecureString $passsec -Key $Key 11 | 12 | docker run ` 13 | -m 2G ` 14 | --name $hostname ` 15 | --hostname $hostname ` 16 | -v $PSScriptRoot\my:c:\run\my ` 17 | -e Accept_eula=Y ` 18 | -e Auth=Windows ` 19 | -e username=$env:USERNAME ` 20 | -e securePassword=$passsec ` 21 | -e passwordKeyFile='c:\run\my\myAES.key' ` 22 | -e clickonce=Y ` 23 | ${NAV_DOCKER_IMAGE} -------------------------------------------------------------------------------- /git-sync/.gitignore: -------------------------------------------------------------------------------- 1 | repo/*.TXT -------------------------------------------------------------------------------- /git-sync/README.md: -------------------------------------------------------------------------------- 1 | # Examples and use-cases for MS Dynamics NAV on Docker 2 | 3 | ## SOURCE CONTROL MANAGEMENT (SCM) INTEGRATION (EXPERIMENTAL, WORK-IN-PROGRESS) 4 | 5 | 6 | **Note:** 7 | This example is using some prerequisites like: *license*, *repo folder*. 8 | 9 | 10 | ![](../media/git-sync_demo_01.gif) 11 | 12 | This is an experimental example that will allow you to synchronize object changes to a specified folder (for example Git workspace). 13 | 14 | In this example you have to create (if is not present) a folder named `repo` in the same folder the script `run.ps1` is being placed. This will be your repository folder. 15 | 16 | When the container is being started it will register some T-SQL scripts that will be used to detect object changes and sync only necessary objects. This approach has some limitations (eg: renaming of the fields are not being detected) so you have another option how to sync fully. You need to remove all files from the `repo` folder. 17 | 18 | The container detects that the folder has no files inside and will run the full export. This actually happens during the container startup phase as well (and again, only if the folder is empty). 19 | 20 | I am going to add a simple API into the container to be able run commands inside the container via this HTTP API. This will allow you to run commands like `git2nav`, `full-nav2git` etc. explicitely. 21 | 22 | And again, this example will create `RoleTailored Client` folder under `my` folder so you can start working and watch how the changes are being populated (with a tiny delay) in your source control management software (eg: Visual Studio Code). 23 | 24 | --- 25 | 26 | ### Any ideas, suggestions or contributions are welcome!!! :) -------------------------------------------------------------------------------- /git-sync/my/AdditionalSetup.ps1: -------------------------------------------------------------------------------- 1 | # Invoke default behavior 2 | . (Join-Path $runPath $MyInvocation.MyCommand.Name) 3 | 4 | if (!$restartingInstance) { 5 | Install-Chocolatey 6 | Install-Git 7 | 8 | Register-NavChangeTracker 9 | } 10 | 11 | Export-ClientFolder -------------------------------------------------------------------------------- /git-sync/my/HelperFunctions.ps1: -------------------------------------------------------------------------------- 1 | # Invoke default behavior 2 | . (Join-Path $runPath $MyInvocation.MyCommand.Name) 3 | 4 | function Export-ClientFolder 5 | { 6 | [CmdletBinding()] 7 | param( 8 | [string]$Path 9 | ) 10 | 11 | if ([System.String]::IsNullOrEmpty($Path)) { 12 | $Path = $myPath; 13 | } 14 | 15 | if (!(Test-Path "$Path\RoleTailored Client" -PathType Container)) { 16 | Write-Host "Copy RoleTailoted Client files" 17 | Copy-Item -path $roleTailoredClientFolder -destination $Path -force -Recurse -ErrorAction Ignore 18 | 19 | $sqlServerName = if ($databaseServer -eq "localhost") { $hostname } else { $databaseServer } 20 | if (!([System.String]::IsNullOrEmpty($databaseInstance))) { 21 | $sqlServerName = "$sqlServerName\$databaseInstance" 22 | } 23 | 24 | $ntAuth = if ($auth -eq "Windows") { $true } else { $false } 25 | 26 | $ClientUserSettingsFileName = "$runPath\ClientUserSettings.config" 27 | [xml]$ClientUserSettings = Get-Content $clientUserSettingsFileName 28 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='Server']").value = "$hostname" 29 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='ServerInstance']").value="NAV" 30 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='ServicesCertificateValidationEnabled']").value="false" 31 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='ClientServicesPort']").value="$publicWinClientPort" 32 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='ACSUri']").value = "" 33 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='DnsIdentity']").value = "$dnsIdentity" 34 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='ClientServicesCredentialType']").value = "$Auth" 35 | $clientUserSettings.Save("$Path\RoleTailored Client\ClientUserSettings.config") 36 | 37 | New-FinSqlExeRunner -FileFullPath "$Path\RoleTailored Client\_finsql-on-docker.exe" ` 38 | -SqlServerName $sqlServerName ` 39 | -DbName "$databaseName" ` 40 | -NtAuth $ntAuth ` 41 | -Id "docker_$hostname" ` 42 | -GenerateSymbolRef ($enableSymbolLoadingAtServerStartup -eq $true) 43 | } 44 | } 45 | 46 | function New-FinSqlExeRunner 47 | { 48 | [CmdletBinding()] 49 | param( 50 | [Parameter(Mandatory=$True)] 51 | [string]$FileFullPath, 52 | [Parameter(Mandatory=$True)] 53 | [string]$SqlServerName, 54 | [Parameter(Mandatory=$True)] 55 | [string]$DbName, 56 | [Parameter(Mandatory=$True)] 57 | [bool]$NtAuth, 58 | [Parameter(Mandatory=$True)] 59 | [string]$Id, 60 | [Parameter()] 61 | [bool]$GenerateSymbolRef=$false 62 | ) 63 | 64 | $useNtAuth = If ($NtAuth) { 1 } Else { 0 } 65 | $fileName = Split-Path $FileFullPath -Leaf 66 | $buildFolder = Join-Path $PSScriptRoot '_buildfinsqlrunner' 67 | $iconFile = 'finsqlicon.ico' 68 | 69 | New-Item -ItemType Directory -Path $buildFolder -Force | Out-Null 70 | 71 | $generateSymbolRefStr = "" 72 | if ($GenerateSymbolRef -and (IsEnableSymbolLoadingSupported)) { 73 | $generateSymbolRefStr = ', generatesymbolreference=yes ' 74 | } 75 | 76 | # Extract and prepare icon file 77 | $icon = [System.IO.FileStream]::new("$buildFolder\$iconFile", [System.IO.FileMode]::OpenOrCreate) 78 | (Get-CsideIcon).Save($icon) 79 | $icon.Close() 80 | Copy-Item "$buildFolder\$iconFile" "c:\$iconFile" 81 | 82 | Set-Content "$buildFolder\$fileName.ps1" "Start-Process '.\finsql.exe' -ArgumentList ""servername=$SqlServerName, database=$DbName, ntauthentication=$useNtAuth, id=$Id $generateSymbolRefStr""" -Force 83 | & (Join-Path $PSScriptRoot 'ps2exe.ps1') -inputFile "$buildFolder\$fileName.ps1" -outputFile "$buildFolder\$fileName" -iconFile "$iconFile" -noconsole -runtime40 -wait -end *>$null 84 | 85 | Copy-Item "$buildFolder\$fileName" $FileFullPath -Force | Out-Null 86 | 87 | # Cleanup 88 | Remove-Item $buildFolder -Recurse -Force | Out-Null 89 | Remove-Item "c:\$iconFile" -Force | Out-Null 90 | } 91 | 92 | function IsEnableSymbolLoadingSupported 93 | { 94 | [CmdletBinding()] 95 | param( 96 | ) 97 | 98 | return ($(Get-NavVersion) -ge [System.Version]'11.0.19097.0') 99 | } 100 | 101 | function Get-NavVersion 102 | { 103 | [CmdletBinding()] 104 | param( 105 | ) 106 | 107 | $finSql = Get-ChildItem $roleTailoredClientFolder 'finsql.exe' 108 | 109 | return $finSql.VersionInfo.ProductVersionRaw 110 | } 111 | 112 | function Get-CsideIcon { 113 | [CmdletBinding()] 114 | param( 115 | ) 116 | 117 | Add-Type -AssemblyName System.Drawing 118 | return ([Drawing.Icon]::ExtractAssociatedIcon((Get-ChildItem $roleTailoredClientFolder 'finsql.exe').FullName)) 119 | } 120 | 121 | function Get-SqlServerAndInstance { 122 | [CmdletBinding()] 123 | param( 124 | ) 125 | 126 | $sqlServerInstance = $databaseServer 127 | if ($databaseInstance) { 128 | $sqlServerInstance += "\$databaseInstance" 129 | } 130 | 131 | return $sqlServerInstance 132 | } 133 | 134 | function Register-NavChangeTracker { 135 | [CmdletBinding()] 136 | param( 137 | ) 138 | 139 | Write-Host "Registering NAV changes tracker (T-SQL)" 140 | . (Join-Path $PSScriptRoot 'gitsync\DeployTSQL.ps1') -SqlServerInstance (Get-SqlServerAndInstance) -Database $databaseName 141 | } 142 | 143 | function Start-NavChangeTrackerExport { 144 | [CmdletBinding()] 145 | param( 146 | [Boolean]$CompleteSync = $false 147 | ) 148 | 149 | try { 150 | if (!$CompleteSync) { 151 | Export-Nav2Scm -RepoPath $objRepoPath -SqlServerInstance (Get-SqlServerAndInstance) -Database $databaseName 152 | } else { 153 | Export-Nav2ScmAll -RepoPath $objRepoPath -SqlServerInstance (Get-SqlServerAndInstance) -Database $databaseName 154 | } 155 | } 156 | catch { 157 | Resolve-ScmOperationException -ScmException $_ 158 | } 159 | } 160 | 161 | function Install-Chocolatey { 162 | [CmdletBinding()] 163 | param( 164 | ) 165 | 166 | Write-Host "Installing Chocolatey" 167 | $env:chocolateyUseWindowsCompression = $false 168 | Invoke-Expression ((New-Object Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) *>$null 169 | choco feature enable -n allowGlobalConfirmation *>$null 170 | } 171 | 172 | function Install-Git { 173 | [CmdletBinding()] 174 | param( 175 | ) 176 | 177 | Write-Host "Installing Git" 178 | choco install git *>$null 179 | } 180 | 181 | function Register-FileSystemWatcher { 182 | [CmdletBinding()] 183 | param( 184 | [Parameter(Mandatory = $true)] 185 | [String]$FolderToWatch, 186 | [Parameter()] 187 | [String]$Filter = '*.*', 188 | [Parameter()] 189 | [Boolean]$IncludeSubfolders = $false, 190 | [Parameter()] 191 | [Boolean]$TrackCreate = $false, 192 | [Parameter()] 193 | [scriptblock]$OnCreateCode, 194 | [Parameter()] 195 | [Boolean]$TrackModify = $false, 196 | [Parameter()] 197 | [scriptblock]$OnModifyCode, 198 | [Parameter()] 199 | [Boolean]$TrackDelete = $false, 200 | [Parameter()] 201 | [scriptblock]$OnDeleteyCode 202 | ) 203 | 204 | $fswEvents = New-Object System.Collections.ArrayList 205 | 206 | $fsw = New-Object IO.FileSystemWatcher $FolderToWatch, $Filter -Property @{IncludeSubdirectories = $IncludeSubfolders;NotifyFilter = [IO.NotifyFilters]'FileName, LastWrite'} 207 | 208 | if (($TrackCreate -eq $true) -and ($OnCreateCode)) { 209 | $createEventIdentifier = [guid]::NewGuid() 210 | Register-ObjectEvent $fsw Created -SourceIdentifier $createEventIdentifier -Action $OnCreateCode 211 | $fswEvents.Add($createEventIdentifier) 212 | } 213 | 214 | if (($TrackModify -eq $true) -and ($OnModifyCode)) { 215 | $modifyEventIdentifier = [guid]::NewGuid() 216 | Register-ObjectEvent $fsw Changed -SourceIdentifier $modifyEventIdentifier -Action $OnModifyCode 217 | $fswEvents.Add($modifyEventIdentifier) 218 | } 219 | 220 | if (($TrackDelete -eq $true) -and ($OnDeleteyCode)) { 221 | $deleteEventIdentifier = [guid]::NewGuid() 222 | Register-ObjectEvent $fsw Changed -SourceIdentifier $deleteEventIdentifier -Action $OnDeleteyCode 223 | $fswEvents.Add($deleteEventIdentifier) 224 | } 225 | } 226 | 227 | function Get-ObjectTypeFilePrefix { 228 | [CmdletBinding()] 229 | param( 230 | [Parameter(Mandatory = $true)] 231 | [Int]$ObjType 232 | ) 233 | 234 | # TableData,Table,,Report,,Codeunit,XMLport,MenuSuite,Page,Query,System,FieldNumber 235 | switch ($ObjType) { 236 | 1 { $objFilePrefix = 'TAB' } 237 | # 2 { $objFilePrefix = 'FOR' } 238 | 3 { $objFilePrefix = 'REP' } 239 | 5 { $objFilePrefix = 'COD' } 240 | 6 { $objFilePrefix = 'XML' } 241 | 7 { $objFilePrefix = 'MEN' } 242 | 8 { $objFilePrefix = 'PAG' } 243 | 9 { $objFilePrefix = 'QUE' } 244 | } 245 | 246 | return $objFilePrefix 247 | } 248 | 249 | function Get-ObjectTypeIdFromFilename { 250 | [CmdletBinding()] 251 | param( 252 | [Parameter(Mandatory = $true)] 253 | [String]$Filename 254 | ) 255 | 256 | $prefix = $Filename.Substring(0, 3) 257 | 258 | # TableData,Table,,Report,,Codeunit,XMLport,MenuSuite,Page,Query,System,FieldNumber 259 | switch ($prefix) { 260 | 'TAB' { $objType = 1 } 261 | # 'FOR' { $objType = 2 } 262 | 'REP' { $objType = 3 } 263 | 'COD' { $objType = 5 } 264 | 'XML' { $objType = 6 } 265 | 'MEN' { $objType = 7 } 266 | 'PAG' { $objType = 8 } 267 | 'QUE' { $objType = 9 } 268 | } 269 | 270 | [Int]$objId = $Filename.Substring(3) 271 | 272 | return { $objType, $objId } 273 | } 274 | 275 | function Import-NavModelToolsModule { 276 | [CmdletBinding()] 277 | param( 278 | ) 279 | 280 | if ($Global:NavModelToolsModuleImported -eq $true) { 281 | return 282 | } 283 | 284 | $module = Get-ChildItem 'C:\Program Files (x86)\Microsoft Dynamics NAV\*\RoleTailored Client\' -Filter 'NavModelTools.ps1' -Recurse 285 | Import-Module $module -DisableNameChecking -Global *>$null 286 | 287 | $Global:NavModelToolsModuleImported = $true 288 | } 289 | 290 | function Resolve-ScmOperationException { 291 | [CmdletBinding()] 292 | param( 293 | [Parameter(Mandatory = $true)] 294 | $ScmException 295 | ) 296 | 297 | Write-Warning "SCM EXCEPTION" 298 | if ($ScmException) { 299 | Write-Warning "ERROR MSG: $($ScmException.Exception.Message)" 300 | Write-Warning "ERROR TRACE: $($ScmException.ScriptStackTrace)" 301 | } 302 | } 303 | 304 | function Resolve-FullNav2ScmExportRecommended { 305 | [CmdletBinding()] 306 | param( 307 | [Parameter(Mandatory)] 308 | [String]$RepoPath 309 | ) 310 | 311 | if (!(Test-Path $RepoPath)) 312 | { 313 | return $true 314 | } 315 | 316 | $objCountInFolder = Get-ChildItem $RepoPath -Filter '*.TXT' | Measure-Object 317 | if ($objCountInFolder.Count -eq 0) { 318 | return $true 319 | } 320 | 321 | return $false 322 | } 323 | 324 | function Export-Nav2ScmAll { 325 | [CmdletBinding()] 326 | param( 327 | [Parameter(Mandatory)] 328 | [String]$RepoPath, 329 | [String]$SqlServerInstance = "LOCALHOST\SQLEXPRESS", 330 | [String]$Database = "CRONUS" 331 | ) 332 | 333 | try { 334 | 335 | Write-Host "Running NAV object full sync (export). This process may take several minutes..." 336 | 337 | if (!(Test-Path $RepoPath)) 338 | { 339 | git init $RepoPath *>$null 340 | New-Item -Path $RepoPath -ItemType Directory -Force | Out-Null 341 | } 342 | 343 | $localWorkPath = Join-Path $repoPath 'TEMPEXP' 344 | if (!(Test-Path $localWorkPath)) { 345 | New-Item $localWorkPath -Type Directory -Force | Out-Null 346 | '*' | Set-Content (Join-Path $localWorkPath '.gitignore') 347 | } 348 | 349 | $expFile = Join-Path $localWorkPath 'NAV_ScmExport.txt' 350 | 351 | Import-NavModelToolsModule 352 | Export-NAVApplicationObject -DatabaseName $database -DatabaseServer $SqlServerInstance -Path $expFile -Force | Out-Null 353 | if (Test-Path($expFile)) 354 | { 355 | if ((Get-Item $expFile).length -gt 0kb) 356 | { 357 | Remove-Item -Path (Join-Path $repoPath '*.TXT') -Force 358 | Split-NAVApplicationObjectFile -Source $expFile -Destination $repoPath -Force 359 | } 360 | Remove-Item $expFile -Force 361 | } 362 | Write-Host "NAV object full export has been finished." 363 | } 364 | catch { 365 | Resolve-ScmOperationException -ScmException $_ 366 | } 367 | } 368 | 369 | function Export-Nav2Scm { 370 | [CmdletBinding()] 371 | param( 372 | [Parameter(Mandatory)] 373 | [String]$RepoPath, 374 | [String]$SqlServerInstance = "LOCALHOST\SQLEXPRESS", 375 | [String]$Database = "CRONUS" 376 | ) 377 | 378 | if (Resolve-FullNav2ScmExportRecommended -RepoPath $RepoPath) 379 | { 380 | Export-Nav2ScmAll -RepoPath $RepoPath -SqlServerInstance $SqlServerInstance -Database $Database 381 | return 382 | } 383 | 384 | try { 385 | 386 | $pendingObjects = Invoke-Sqlcmd -Query "SELECT * FROM [dbo].[SCM.ObjectLog]" -ServerInstance "$SqlServerInstance" -Database $database 387 | $pendingObjCount = $pendingObjects | Measure-Object 388 | 389 | if ($pendingObjCount.Count -eq 0) { 390 | return 391 | } 392 | 393 | $localWorkPath = Join-Path $RepoPath 'TEMPEXP' 394 | if (!(Test-Path $localWorkPath)) { 395 | New-Item $localWorkPath -Type Directory -Force | Out-Null 396 | '*' | Set-Content (Join-Path $localWorkPath '.gitignore') 397 | } 398 | 399 | $expFile = Join-Path $localWorkPath 'NAV_ScmExport.txt' 400 | 401 | Import-NavModelToolsModule 402 | 403 | foreach ($obj in $pendingObjects) { 404 | 405 | $objType = $obj.Item("Object Type") 406 | $objId = $obj.Item("Object ID") 407 | $objMod = $obj.Item("Action GUID") 408 | $objAction = $obj.Item("Last Action") 409 | 410 | if ($objAction -eq 3) { 411 | # DELETE action - we need to remove an existing file. 412 | $objFilePrefix = Get-ObjectTypeFilePrefix -ObjType $objType 413 | $fileToRemove = Join-Path $repoPath "$objFilePrefix$objId.TXT" 414 | if (Test-Path $fileToRemove) { 415 | Remove-Item $fileToRemove -Force 416 | } 417 | } 418 | 419 | Export-NAVApplicationObject -DatabaseName $database -DatabaseServer $SqlServerInstance -Path $expFile -Filter "Type=$objType;ID=$objId" -Force | Out-Null 420 | 421 | Invoke-Sqlcmd -Query "DELETE FROM [dbo].[SCM.ObjectLog] WHERE ([Object Type] = $objType) AND ([Object ID] = $objId) AND ([Action GUID] = '$objMod')" ` 422 | -ServerInstance "$SqlServerInstance" -Database $database 423 | 424 | if (Test-Path($expFile)) 425 | { 426 | if ((Get-Item $expFile).length -gt 0kb) 427 | { 428 | Split-NAVApplicationObjectFile -Source $expFile -Destination $repoPath -Force 429 | } 430 | Remove-Item $expFile -Force 431 | } 432 | } 433 | } 434 | catch { 435 | Resolve-ScmOperationException -ScmException $_ 436 | } 437 | } -------------------------------------------------------------------------------- /git-sync/my/MainLoop.ps1: -------------------------------------------------------------------------------- 1 | $lastCheck = (Get-Date).AddSeconds(-2) 2 | while ($true) 3 | { 4 | $thisCheck = Get-Date 5 | Get-EventLog -LogName Application -After $lastCheck -ErrorAction Ignore | Where-Object { ($_.Source -like '*Dynamics*' -or $_.Source -eq $SqlServiceName) -and $_.EntryType -eq "Error" -and $_.EntryType -ne "0" } | Select-Object TimeGenerated, EntryType, Message | format-list 6 | $lastCheck = $thisCheck 7 | Start-Sleep -Seconds 2 8 | 9 | Start-NavChangeTrackerExport 10 | } -------------------------------------------------------------------------------- /git-sync/my/SetupVariables.ps1: -------------------------------------------------------------------------------- 1 | # Invoke default behavior 2 | . (Join-Path $runPath $MyInvocation.MyCommand.Name) 3 | 4 | 5 | $objRepoPath = "$env:objRepoPath" -------------------------------------------------------------------------------- /git-sync/my/gitsync/DeployTSQL.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param( 3 | [String]$SqlServerInstance = "LOCALHOST\SQLEXPRESS", 4 | [String]$Database = "CRONUS" 5 | ) 6 | 7 | if ($auth -eq "Windows") { 8 | Invoke-Sqlcmd -InputFile (Join-Path $PSScriptRoot "SCM.t_ObjLog.sql") -ServerInstance "$SqlServerInstance" -Database $Database 9 | Invoke-Sqlcmd -InputFile (Join-Path $PSScriptRoot "SCM.sp_InsertToObjLog.sql") -ServerInstance "$SqlServerInstance" -Database $Database 10 | Invoke-Sqlcmd -InputFile (Join-Path $PSScriptRoot "SCM.tr_ObjectTrigger.sql") -ServerInstance "$SqlServerInstance" -Database $Database 11 | } else { 12 | # NOT TESTED YET !!! 13 | $pwd = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword)) 14 | Invoke-Sqlcmd -InputFile (Join-Path $PSScriptRoot "SCM.t_ObjLog.sql") -ServerInstance "$SqlServerInstance" -Database $Database -Username "sa" -Password $pwd 15 | Invoke-Sqlcmd -InputFile (Join-Path $PSScriptRoot "SCM.sp_InsertToObjLog.sql") -ServerInstance "$SqlServerInstance" -Database $Database -Username "sa" -Password $pwd 16 | Invoke-Sqlcmd -InputFile (Join-Path $PSScriptRoot "SCM.tr_ObjectTrigger.sql") -ServerInstance "$SqlServerInstance" -Database $Database -Username "sa" -Password $pwd 17 | $pwd = $null 18 | } -------------------------------------------------------------------------------- /git-sync/my/gitsync/SCM.sp_InsertToObjLog.sql: -------------------------------------------------------------------------------- 1 | SET ANSI_NULLS ON 2 | GO 3 | SET QUOTED_IDENTIFIER ON 4 | GO 5 | 6 | -- At first delete existing procedure 7 | IF EXISTS (SELECT * FROM sysobjects WHERE name = 'SCM.InsertToObjLog' AND type = 'P') 8 | BEGIN 9 | DROP PROCEDURE [dbo].[SCM.InsertToObjLog]; 10 | END; 11 | GO 12 | 13 | -- Create it again 14 | CREATE PROCEDURE [dbo].[SCM.InsertToObjLog] 15 | @ObjType AS INT, 16 | @ObjId AS INT, 17 | @ObjName AS VARCHAR(30), 18 | @ObjVList AS VARCHAR(80), 19 | @ObjMod AS TINYINT, 20 | @ObjCompiled AS TINYINT, 21 | @ObjDate AS DATETIME, 22 | @ObjTime AS DATETIME, 23 | @CurrOperDT AS DATETIME, 24 | @BlobContent AS VARBINARY(MAX), 25 | /* 26 | @TriggerAction = 1 => INSERT 27 | @TriggerAction = 2 => UPDATE 28 | @TriggerAction = 3 => DELETE 29 | */ 30 | @TriggerAction AS TINYINT 31 | AS BEGIN 32 | 33 | BEGIN TRY 34 | 35 | DECLARE 36 | @Count AS INT, 37 | @LockingActive AS BIT = 0, 38 | @ErrorMessage NVARCHAR(4000), 39 | @ErrorSeverity INT, 40 | @ErrorState INT; 41 | 42 | -- Exists object log register (record in the [SCM.ObjectLog] table)? 43 | SELECT @Count = COUNT(*) FROM [dbo].[SCM.ObjectLog] AS UOL 44 | WHERE (UOL.[Object ID] = @ObjId) AND (UOL.[Object Type] = @ObjType); 45 | 46 | IF (@Count > 0) BEGIN 47 | 48 | UPDATE [dbo].[SCM.ObjectLog] SET 49 | [Last Block DateTime] = @CurrOperDT, 50 | [Object Last Name] = @ObjName, 51 | [Object Last Version List] = @ObjVList, 52 | [Object Last Object Date] = @ObjDate, 53 | [Object Last Object Time] = @ObjTime, 54 | [Last Action] = @TriggerAction, 55 | [Action GUID] = NEWID() 56 | WHERE 57 | ([Object ID] = @ObjId) AND ([Object Type] = @ObjType); 58 | 59 | END ELSE BEGIN 60 | 61 | INSERT INTO [dbo].[SCM.ObjectLog] ( 62 | [Object Type], 63 | [Object ID], 64 | [Initial Block DateTime], 65 | [Last Block DateTime], 66 | [Object Last Name], 67 | [Object Last Version List], 68 | [Object Last Object Date], 69 | [Object Last Object Time], 70 | [Last Action], 71 | [Action GUID]) 72 | VALUES ( 73 | @ObjType, 74 | @ObjId, 75 | @CurrOperDT, 76 | @CurrOperDT, 77 | @ObjName, 78 | @ObjVList, 79 | @ObjDate, 80 | @ObjTime, 81 | @TriggerAction, 82 | NEWID()); 83 | END; 84 | 85 | /* 86 | INSERT INTO [dbo].[SCM.ObjectLogDetail] ( 87 | [Object Type], 88 | [Object ID], 89 | [Modification DateTime], 90 | [Object BLOB Content], 91 | [Operation Type], 92 | [Object Name], 93 | [Object Version List], 94 | [Object Modified], 95 | [Object Compiled], 96 | [Object Date], 97 | [Object Time]) 98 | VALUES ( 99 | @ObjType, 100 | @ObjId, 101 | @CurrOperDT, 102 | @BlobContent, 103 | @TriggerAction, 104 | @ObjName, 105 | @ObjVList, 106 | @ObjMod, 107 | @ObjCompiled, 108 | @ObjDate, 109 | @ObjTime); 110 | */ 111 | 112 | END TRY 113 | BEGIN CATCH 114 | SELECT 115 | @ErrorMessage = ERROR_MESSAGE(), 116 | @ErrorSeverity = ERROR_SEVERITY(), 117 | @ErrorState = ERROR_STATE(); 118 | 119 | SET @ErrorMessage = 120 | CHAR(13) + CHAR(13) + 121 | '===============================================' + 122 | CHAR(13) + 123 | @ErrorMessage + 124 | CHAR(13) + 125 | '===============================================' + 126 | CHAR(13); 127 | 128 | RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState); 129 | END CATCH; 130 | END; 131 | 132 | GO -------------------------------------------------------------------------------- /git-sync/my/gitsync/SCM.sp_UpsertToObjMetadata.sql: -------------------------------------------------------------------------------- 1 | SET ANSI_NULLS ON 2 | GO 3 | SET QUOTED_IDENTIFIER ON 4 | GO 5 | 6 | -- At first delete existing procedure 7 | IF EXISTS (SELECT * FROM sysobjects WHERE name = 'SCM.UpsertToObjMetadata' AND type = 'P') 8 | BEGIN 9 | DROP PROCEDURE [dbo].[SCM.UpsertToObjMetadata]; 10 | END; 11 | GO 12 | 13 | -- Create it again 14 | CREATE PROCEDURE [dbo].[SCM.UpsertToObjMetadata] 15 | @ObjType AS INT, 16 | @ObjId AS INT, 17 | @Filename AS NVARCHAR(50), 18 | @TxtFileHash AS NVARCHAR(32) 19 | AS BEGIN 20 | 21 | BEGIN TRY 22 | 23 | DECLARE 24 | @Count AS INT, 25 | @LockingActive AS BIT = 0, 26 | @ErrorMessage NVARCHAR(4000), 27 | @ErrorSeverity INT, 28 | @ErrorState INT; 29 | 30 | -- Exists object log register (record in the [SCM.ObjectMetadata] table)? 31 | SELECT @Count = COUNT(*) FROM [dbo].[SCM.ObjectMetadata] AS OM 32 | WHERE (OM.[Object ID] = @ObjId) AND (OM.[Object Type] = @ObjType); 33 | 34 | IF (@Count > 0) BEGIN 35 | 36 | UPDATE [dbo].[SCM.ObjectLog] SET 37 | [Last Block DateTime] = @CurrOperDT, 38 | [Object Last Name] = @ObjName, 39 | [Object Last Version List] = @ObjVList, 40 | [Object Last Object Date] = @ObjDate, 41 | [Object Last Object Time] = @ObjTime, 42 | [Last Action] = @TriggerAction, 43 | [Transaction GUID] = NEWID() 44 | WHERE 45 | ([Object ID] = @ObjId) AND ([Object Type] = @ObjType); 46 | 47 | END ELSE BEGIN 48 | 49 | INSERT INTO [dbo].[SCM.ObjectMetadata] ( 50 | [Object Type], 51 | [Object ID], 52 | [Initial Block DateTime], 53 | [Last Block DateTime], 54 | [Object Last Name], 55 | [Object Last Version List], 56 | [Object Last Object Date], 57 | [Object Last Object Time], 58 | [Last Action], 59 | [Action GUID]) 60 | VALUES ( 61 | @ObjType, 62 | @ObjId, 63 | @CurrOperDT, 64 | @CurrOperDT, 65 | @ObjName, 66 | @ObjVList, 67 | @ObjDate, 68 | @ObjTime, 69 | @TriggerAction, 70 | NEWID()); 71 | END; 72 | 73 | 74 | /* 75 | INSERT INTO [dbo].[SCM.ObjectLogDetail] ( 76 | [Object Type], 77 | [Object ID], 78 | [Modification DateTime], 79 | [Object BLOB Content], 80 | [Operation Type], 81 | [Object Name], 82 | [Object Version List], 83 | [Object Modified], 84 | [Object Compiled], 85 | [Object Date], 86 | [Object Time]) 87 | VALUES ( 88 | @ObjType, 89 | @ObjId, 90 | @CurrOperDT, 91 | @BlobContent, 92 | @TriggerAction, 93 | @ObjName, 94 | @ObjVList, 95 | @ObjMod, 96 | @ObjCompiled, 97 | @ObjDate, 98 | @ObjTime); 99 | */ 100 | 101 | END TRY 102 | BEGIN CATCH 103 | SELECT 104 | @ErrorMessage = ERROR_MESSAGE(), 105 | @ErrorSeverity = ERROR_SEVERITY(), 106 | @ErrorState = ERROR_STATE(); 107 | 108 | SET @ErrorMessage = 109 | CHAR(13) + CHAR(13) + 110 | '===============================================' + 111 | CHAR(13) + 112 | @ErrorMessage + 113 | CHAR(13) + 114 | '===============================================' + 115 | CHAR(13); 116 | 117 | RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState); 118 | END CATCH; 119 | END; 120 | 121 | GO -------------------------------------------------------------------------------- /git-sync/my/gitsync/SCM.t_ObjLog.sql: -------------------------------------------------------------------------------- 1 | SET ANSI_NULLS ON 2 | GO 3 | 4 | SET QUOTED_IDENTIFIER ON 5 | GO 6 | 7 | SET ANSI_PADDING ON 8 | GO 9 | 10 | IF EXISTS (SELECT name FROM sysobjects WHERE name = 'SCM.ObjectLog' AND type = 'U') 11 | BEGIN 12 | DROP TABLE [dbo].[SCM.ObjectLog] 13 | END 14 | GO 15 | 16 | CREATE TABLE [dbo].[SCM.ObjectLog]( 17 | [timestamp] [timestamp] NOT NULL, 18 | [Object Type] [int] NOT NULL, 19 | [Object ID] [int] NOT NULL, 20 | [Initial Block DateTime] [datetime] NOT NULL, 21 | [Last Block DateTime] [datetime] NOT NULL, 22 | [Object Last Name] [varchar](30) NOT NULL, 23 | [Object Last Version List] [varchar](80) NOT NULL, 24 | [Object Last Object Date] [datetime] NOT NULL, 25 | [Object Last Object Time] [datetime] NOT NULL, 26 | [Last Action] [int] NOT NULL, 27 | [Action GUID] [varchar](60) NOT NULL, 28 | --[Delete Permited] [tinyint] NOT NULL, 29 | CONSTRAINT [SCM.ObjectLog$0] PRIMARY KEY CLUSTERED 30 | ( 31 | [Object Type] ASC, 32 | [Object ID] ASC 33 | )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 34 | ) ON [PRIMARY] 35 | 36 | GO 37 | 38 | SET ANSI_PADDING OFF 39 | GO 40 | 41 | 42 | -------------------------------------------------------------------------------- /git-sync/my/gitsync/SCM.t_ObjectMetadata.sql: -------------------------------------------------------------------------------- 1 | SET ANSI_NULLS ON 2 | GO 3 | 4 | SET QUOTED_IDENTIFIER ON 5 | GO 6 | 7 | IF EXISTS (SELECT name FROM sysobjects WHERE name = 'SCM.ObjectMetadata' AND type = 'U') 8 | BEGIN 9 | DROP TABLE [dbo].[SCM.ObjectMetadata] 10 | END 11 | GO 12 | 13 | CREATE TABLE [dbo].[SCM.ObjectMetadata]( 14 | [timestamp] [timestamp] NOT NULL, 15 | [Object Type] [int] NOT NULL, 16 | [Object ID] [int] NOT NULL, 17 | [Metadata] [image] NULL, 18 | [User Code] [image] NULL, 19 | [User AL Code] [image] NULL, 20 | [Metadata Version] [int] NOT NULL, 21 | [Hash] [nvarchar](32) NOT NULL, 22 | [Object Subtype] [nvarchar](30) NOT NULL, 23 | [Has Subscribers] [tinyint] NOT NULL, 24 | -- NAV Object Exists 25 | [Metadata Exists] [tinyint] NOT NULL, 26 | -- Only the filename (TAB27.TXT etc.) 27 | [FileName] [nvarchar](50) NOT NULL, 28 | [TxtFileHash] [nvarchar](32) NOT NULL, 29 | [Transaction GUID] [varchar](60) NOT NULL, 30 | CONSTRAINT [SCM.ObjectMetadata$0] PRIMARY KEY CLUSTERED 31 | ( 32 | [Object Type] ASC, 33 | [Object ID] ASC 34 | )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 35 | ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] 36 | 37 | GO 38 | 39 | 40 | -------------------------------------------------------------------------------- /git-sync/my/gitsync/SCM.tr_ObjectTrigger.sql: -------------------------------------------------------------------------------- 1 | SET ANSI_NULLS ON 2 | GO 3 | SET QUOTED_IDENTIFIER ON 4 | GO 5 | 6 | IF EXISTS (SELECT name FROM sysobjects WHERE name = 'SCM.ObjectIUD' AND type = 'TR') 7 | BEGIN 8 | DROP TRIGGER [dbo].[SCM.ObjectIUD] 9 | END 10 | GO 11 | 12 | CREATE TRIGGER [dbo].[SCM.ObjectIUD] 13 | ON [dbo].[Object] 14 | AFTER INSERT, UPDATE, DELETE 15 | AS BEGIN 16 | 17 | BEGIN TRY 18 | DECLARE 19 | @Count AS INT, 20 | @ObjId AS INT, 21 | @ObjType AS INT, 22 | @ObjIdDel AS INT, 23 | @ObjTypeDel AS INT, 24 | @ObjName AS VARCHAR(30), 25 | @ObjVList AS VARCHAR(80), 26 | @ObjMod AS TINYINT, 27 | @ObjCompiled AS TINYINT, 28 | @ObjDate AS DATETIME, 29 | @ObjTime AS DATETIME, 30 | @ObjSidString AS VARCHAR(120), 31 | @CurrOperDT AS DATETIME, 32 | @BlobContent AS VARBINARY(MAX), 33 | /* 34 | @TriggerAction = 1 => INSERT 35 | @TriggerAction = 2 => UPDATE 36 | @TriggerAction = 3 => DELETE 37 | */ 38 | @TriggerAction AS TINYINT = 0, 39 | @ObjRenumbered AS BIT = 0, 40 | @ErrorMessage NVARCHAR(4000), 41 | @ErrorSeverity INT, 42 | @ErrorState INT; 43 | 44 | SET @CurrOperDT = GETUTCDATE(); 45 | 46 | -- Detect which action has been triggered 47 | -- Set Action to Insert by default. 48 | SET @TriggerAction = 1; 49 | IF EXISTS(SELECT * FROM deleted) 50 | BEGIN 51 | SET @TriggerAction = 52 | CASE 53 | WHEN EXISTS(SELECT * FROM inserted) THEN 2 -- Set Action to Updated. 54 | ELSE 3 -- Set Action to Deleted. 55 | END; 56 | SELECT @ObjIdDel = OD.[ID], @ObjTypeDel = OD.[Type] FROM deleted AS OD; 57 | END ELSE BEGIN 58 | IF NOT EXISTS(SELECT * FROM inserted) RETURN; -- Nothing updated or inserted. 59 | END; 60 | 61 | IF (@TriggerAction BETWEEN 1 AND 2) BEGIN 62 | -- INSERT/UPDATE 63 | SELECT 64 | @ObjId = CO.ID, 65 | @ObjType = CO.[Type], 66 | @ObjName = CO.Name, 67 | @ObjVList = CO.[Version List], 68 | @ObjMod = CO.Modified, 69 | @ObjCompiled = CO.Compiled, 70 | @ObjDate = CO.Compiled, 71 | @ObjDate = CO.[Date], 72 | @ObjTime = CO.[Time] 73 | FROM inserted AS CO; 74 | 75 | IF (@ObjType = 0) 76 | -- We will skip DataTable 77 | RETURN; 78 | 79 | SELECT 80 | @BlobContent = O2.[BLOB Reference] 81 | FROM inserted AS CO2 82 | JOIN [dbo].[Object] AS O2 ON 83 | (CO2.[Type] = O2.[Type]) AND (CO2.[ID] = O2.[ID]) 84 | 85 | END ELSE BEGIN 86 | -- DELETE 87 | SELECT 88 | @ObjId = DO.ID, 89 | @ObjType = DO.[Type], 90 | @ObjName = DO.Name, 91 | @ObjVList = DO.[Version List], 92 | @ObjMod = DO.Modified, 93 | @ObjCompiled = DO.Compiled, 94 | @ObjDate = DO.[Date], 95 | @ObjTime = DO.[Time] 96 | FROM deleted AS DO; 97 | 98 | IF (@ObjType = 0) 99 | -- We will skip DataTable 100 | RETURN; 101 | 102 | SELECT 103 | @BlobContent = O2.[BLOB Reference] 104 | FROM deleted AS DO2 105 | JOIN [dbo].[Object] AS O2 ON 106 | (DO2.[Type] = O2.[Type]) AND (DO2.[ID] = O2.[ID]); 107 | 108 | END; 109 | 110 | IF (@TriggerAction = 2) AND (@ObjId != @ObjIdDel) BEGIN 111 | SET @ObjRenumbered = 1; 112 | END; 113 | 114 | EXEC [dbo].[SCM.InsertToObjLog] 115 | @ObjType = @ObjType, 116 | @ObjId = @ObjId, 117 | @ObjName = @ObjName, 118 | @ObjVList = @ObjVList, 119 | @ObjMod = @ObjMod, 120 | @ObjCompiled = @ObjCompiled, 121 | @ObjDate = @ObjDate, 122 | @ObjTime = @ObjTime, 123 | @CurrOperDT = @CurrOperDT, 124 | @BlobContent = @BlobContent, 125 | @TriggerAction = @TriggerAction 126 | ; 127 | 128 | IF (@ObjRenumbered = 1) BEGIN 129 | EXEC [dbo].[SCM.InsertToObjLog] 130 | @ObjType = @ObjType, 131 | @ObjId = @ObjIdDel, 132 | @ObjName = @ObjName, 133 | @ObjVList = @ObjVList, 134 | @ObjMod = @ObjMod, 135 | @ObjCompiled = @ObjCompiled, 136 | @ObjDate = @ObjDate, 137 | @ObjTime = @ObjTime, 138 | @CurrOperDT = @CurrOperDT, 139 | @BlobContent = @BlobContent, 140 | @TriggerAction = 3 -- DELETE 141 | ; 142 | END; 143 | 144 | END TRY 145 | BEGIN CATCH 146 | 147 | SELECT 148 | @ErrorMessage = ERROR_MESSAGE(), 149 | @ErrorSeverity = ERROR_SEVERITY(), 150 | @ErrorState = ERROR_STATE(); 151 | 152 | SET @ErrorMessage = 153 | CHAR(13) + CHAR(13) + 154 | '===============================================' + 155 | CHAR(13) + 156 | @ErrorMessage + 157 | CHAR(13) + 158 | '===============================================' + 159 | CHAR(13); 160 | 161 | RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState); 162 | 163 | END CATCH 164 | END; 165 | 166 | 167 | -------------------------------------------------------------------------------- /git-sync/my/ps2exe.ps1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/git-sync/my/ps2exe.ps1 -------------------------------------------------------------------------------- /git-sync/run.ps1: -------------------------------------------------------------------------------- 1 | $hostname = "navex-sqltrace" 2 | 3 | if ([System.String]::IsNullOrEmpty(${NAV_DOCKER_IMAGE})) { 4 | Write-Warning "You have to specify the image using 'NAV_DOCKER_IMAGE' variable. Please, read the main README.md file" 5 | Write-Warning "Example: `$NAV_DOCKER_IMAGE = 'microsoft/dynamics-nav'" 6 | Write-Warning "Exiting..." 7 | exit 1 8 | } 9 | 10 | # SETTINGS: 11 | # Create AES key to encrypt the password: 12 | $KeyFile = ".\my\myAES.key" 13 | $Key = New-Object Byte[] 16 # You can use 16, 24, or 32 for AES 14 | [Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($Key) 15 | $Key | out-file $KeyFile 16 | 17 | $passsec = Read-Host 'Input the user`s password' -AsSecureString 18 | $passsec = ConvertFrom-SecureString $passsec -Key $Key 19 | 20 | docker run ` 21 | -m 4G ` 22 | --name $hostname ` 23 | --hostname $hostname ` 24 | -v $PSScriptRoot\my:c:\run\my ` 25 | -v $PSScriptRoot\repo:c:\gitrepo ` 26 | -e Accept_eula=Y ` 27 | -e Auth=Windows ` 28 | -e username=$env:USERNAME ` 29 | -e securePassword=$passsec ` 30 | -e passwordKeyFile='c:\run\my\myAES.key' ` 31 | -e licensefile='c:\run\my\_license.flf' ` 32 | -e objRepoPath=c:\gitrepo ` 33 | -e enableSymbolLoading=Y ` 34 | -e ExitOnError=N ` 35 | ${NAV_DOCKER_IMAGE} 36 | -------------------------------------------------------------------------------- /gmsa/CredentialSpec.psm1: -------------------------------------------------------------------------------- 1 | # This requires the ActiveDirectory module. Run Add-WindowsFeature rsat-ad-powershell to install it 2 | Import-Module ActiveDirectory 3 | $Script:CredentialSpecPath="$($env:ProgramData)\Docker\CredentialSpecs" 4 | 5 | <# 6 | .Synopsis 7 | Creates and stores a credential specification file 8 | 9 | .Description 10 | Windows containers are able to run with an Active Directory identity. This enables applications running 11 | in the container to use Windows authentication instead of stored username/password combinations. 12 | 13 | Each credential spec contains: 14 | - A default account used that will be mapped to LocalSystem & Network Service in the container 15 | - (Optional) Additional group Managed Service Accounts that may be used in the container 16 | 17 | .Parameter Name 18 | The name for the new credential specification 19 | 20 | .Parameter AccountName 21 | The group Managed Service Account name used for the default account 22 | 23 | 24 | .Parameter Domain 25 | The Active Directory domain used for the default account. If not specified, will use the domain of the host. 26 | 27 | 28 | .Parameter AdditionalAccounts 29 | A list of additional group Managed Service Accounts that will be available to running services 30 | 31 | 32 | .Example 33 | # Create a new credential spec named "ContainerApp1" 34 | New-CredentialSpec -Name "ContainerApp1" -Domain (Get-ADDomain) -AccountName "AppAccount1" 35 | 36 | .Example 37 | # Create a new credential spec named "CS1" 38 | # - with default account "WebApp1" 39 | # - and an additional account "acct1" on domain "domain1" 40 | New-CredentialSpec -Name CS1 -AccountName WebApp1 -AdditionalAccounts @{DomainName = "domain1"; AccountName = "acct1" }, @{DomainName = "domain1"; AccountName="acct2"} 41 | 42 | #> 43 | function New-CredentialSpec 44 | { 45 | param( 46 | [Parameter(Mandatory=$true)] [String] $Name, 47 | [Parameter(Mandatory=$true)] [String] $AccountName, 48 | [Parameter(Mandatory=$false)] [Microsoft.ActiveDirectory.Management.ADDomain] $Domain = (Get-ADDomain), 49 | [Parameter(Mandatory=$false)] $AdditionalAccounts 50 | ) 51 | 52 | 53 | # TODO: verify $Script:CredentialSpecPath exists 54 | 55 | # Start hash table for output 56 | $output = @{} 57 | 58 | 59 | 60 | # Create ActiveDirectoryConfig Object 61 | $output.ActiveDirectoryConfig = @{} 62 | $output.ActiveDirectoryConfig.GroupManagedServiceAccounts = @( @{"Name" = $AccountName; "Scope" = $Domain.DNSRoot } ) 63 | $output.ActiveDirectoryConfig.GroupManagedServiceAccounts += @{"Name" = $AccountName; "Scope" = $Domain.NetBIOSName } 64 | if ($AdditionalAccounts) { 65 | $AdditionalAccounts | ForEach-Object { 66 | $output.ActiveDirectoryConfig.GroupManagedServiceAccounts += @{"Name" = $_.AccountName; "Scope" = $_.DomainName } 67 | } 68 | } 69 | 70 | # Create CmsPlugins Object 71 | $output.CmsPlugins = @("ActiveDirectory") 72 | 73 | 74 | # Create DomainJoinConfig Object 75 | $output.DomainJoinConfig = @{} 76 | $output.DomainJoinConfig.DnsName = $Domain.Forest 77 | $output.DomainJoinConfig.Guid = $Domain.ObjectGUID 78 | $output.DomainJoinConfig.DnsTreeName = $Domain.DNSRoot 79 | $output.DomainJoinConfig.NetBiosName = $Domain.NetBIOSName 80 | $output.DomainJoinConfig.Sid = $Domain.DomainSID.Value 81 | $output.DomainJoinConfig.MachineAccountName = $AccountName 82 | 83 | 84 | $output | ConvertTo-Json -Depth 5 | Out-File -FilePath "$($Script:CredentialSpecPath)\\$($Name).json" -encoding ascii 85 | 86 | } 87 | 88 | 89 | 90 | <# 91 | .Synopsis 92 | Gets all credential specs on current system 93 | 94 | .Description 95 | Windows containers are able to run with an Active Directory identity. This enables applications running 96 | in the container to use Windows authentication instead of stored username/password combinations. 97 | #> 98 | function Get-CredentialSpec 99 | { 100 | Get-ChildItem $Script:CredentialSpecPath | Select-Object @{ 101 | Name='Name' 102 | Expression = { $_.BaseName } 103 | }, 104 | @{ 105 | Name='Path' 106 | Expression = { $_.FullName } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /gmsa/New-gMSA.ps1: -------------------------------------------------------------------------------- 1 | function New-gMSA 2 | { 3 | param ( 4 | 5 | [Parameter(Mandatory=$true)] 6 | [String[]]$HostNames, 7 | 8 | [Parameter(Mandatory=$false)] 9 | [String]$SecGroupPath = 'OU=gMSA for Windows Containers,DC=mydomain,DC=com', 10 | 11 | [Parameter(Mandatory=$false)] 12 | [String[]]$PrincipalsAllowedToRetrieveManagedPassword = @( 'DockerGMSAGroup' ) 13 | 14 | ) 15 | 16 | Import-Module (Join-Path $PSScriptRoot CredentialSpec.psm1) 17 | 18 | foreach ($hostname in $HostNames) 19 | { 20 | $account = $null 21 | $dnsroot = (Get-ADDomain).DNSRoot 22 | $dnsHostName = $hostName + '.' + $dnsroot 23 | 24 | $account = Get-ADServiceAccount -Filter { cn -eq $hostName } 25 | 26 | if ($account -eq $null) 27 | { 28 | Write-Verbose "Creating ADServiceAccount..." 29 | $account = New-ADServiceAccount -name $hostName ` 30 | -DnsHostName $dnsHostName ` 31 | -Path $SecGroupPath ` 32 | -PrincipalsAllowedToRetrieveManagedPassword $PrincipalsAllowedToRetrieveManagedPassword ` 33 | -PassThru 34 | 35 | 36 | foreach ($group in $PrincipalsAllowedToRetrieveManagedPassword) 37 | { 38 | Add-ADGroupMember $group $account 39 | } 40 | 41 | } else 42 | { 43 | Write-Verbose "ADServiceAccount already exists." 44 | } 45 | 46 | New-CredentialSpec -Name $hostName -AccountName $hostName 47 | } 48 | } -------------------------------------------------------------------------------- /gmsa/README.md: -------------------------------------------------------------------------------- 1 | # Examples and use-cases for MS Dynamics NAV on Docker 2 | 3 | ## gMSA APPROACH - WORK IN PROGRESS!!! -------------------------------------------------------------------------------- /gmsa/my/AdditionalSetup.ps1: -------------------------------------------------------------------------------- 1 | # Invoke default behavior 2 | . (Join-Path $runPath $MyInvocation.MyCommand.Name) 3 | 4 | if ($exportClientFolder -eq "Y") { 5 | Export-ClientFolder $exportClientFolderPath 6 | } 7 | 8 | . 'C:\Run\Prompt.ps1' 9 | 10 | if (!$restartingInstance) { 11 | . (Join-Path $PSScriptRoot 'MyCustomScripts\InstallModules.ps1') 12 | } 13 | 14 | Write-Host "Importing NAV users" 15 | . (Join-Path $PSScriptRoot 'MyCustomScripts\SetupMyUsers.ps1') *>$null -------------------------------------------------------------------------------- /gmsa/my/HelperFunctions.ps1: -------------------------------------------------------------------------------- 1 | # Invoke default behavior 2 | . (Join-Path $runPath $MyInvocation.MyCommand.Name) 3 | 4 | function Export-ClientFolder 5 | { 6 | [CmdletBinding()] 7 | param( 8 | [string]$Path 9 | ) 10 | 11 | if ([System.String]::IsNullOrEmpty($Path)) { 12 | return 13 | } 14 | 15 | if (!(Test-Path "$Path\RoleTailored Client" -PathType Container)) { 16 | Write-Host "Copy RoleTailoted Client files" 17 | Copy-Item -path $roleTailoredClientFolder -destination $Path -force -Recurse -ErrorAction Ignore 18 | 19 | $sqlServerName = if ($databaseServer -eq "localhost") { $hostname } else { $databaseServer } 20 | if (!([System.String]::IsNullOrEmpty($databaseInstance))) { 21 | $sqlServerName = "$sqlServerName\$databaseInstance" 22 | } 23 | 24 | $ntAuth = if ($auth -eq "Windows") { $true } else { $false } 25 | 26 | $ClientUserSettingsFileName = "$runPath\ClientUserSettings.config" 27 | [xml]$ClientUserSettings = Get-Content $clientUserSettingsFileName 28 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='Server']").value = "$hostname" 29 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='ServerInstance']").value="NAV" 30 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='ServicesCertificateValidationEnabled']").value="false" 31 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='ClientServicesPort']").value="$publicWinClientPort" 32 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='ACSUri']").value = "" 33 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='DnsIdentity']").value = "$dnsIdentity" 34 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='ClientServicesCredentialType']").value = "$Auth" 35 | $clientUserSettings.Save("$Path\RoleTailored Client\ClientUserSettings.config") 36 | 37 | New-FinSqlExeRunner -FileFullPath "$Path\RoleTailored Client\_finsql-on-docker.exe" ` 38 | -SqlServerName $sqlServerName ` 39 | -DbName "$databaseName" ` 40 | -NtAuth $ntAuth ` 41 | -Id "docker_$hostname" ` 42 | -GenerateSymbolRef ($enableSymbolLoadingAtServerStartup -eq $true) 43 | } 44 | } 45 | 46 | function New-FinSqlExeRunner 47 | { 48 | [CmdletBinding()] 49 | param( 50 | [Parameter(Mandatory=$True)] 51 | [string]$FileFullPath, 52 | [Parameter(Mandatory=$True)] 53 | [string]$SqlServerName, 54 | [Parameter(Mandatory=$True)] 55 | [string]$DbName, 56 | [Parameter(Mandatory=$True)] 57 | [bool]$NtAuth, 58 | [Parameter(Mandatory=$True)] 59 | [string]$Id, 60 | [Parameter()] 61 | [bool]$GenerateSymbolRef=$false 62 | ) 63 | 64 | $useNtAuth = If ($NtAuth) { 1 } Else { 0 } 65 | $fileName = Split-Path $FileFullPath -Leaf 66 | $buildFolder = Join-Path $PSScriptRoot '_buildfinsqlrunner' 67 | $iconFile = 'finsqlicon.ico' 68 | 69 | New-Item -ItemType Directory -Path $buildFolder -Force | Out-Null 70 | 71 | $generateSymbolRefStr = "" 72 | if ($GenerateSymbolRef -and (IsEnableSymbolLoadingSupported)) { 73 | $generateSymbolRefStr = ', generatesymbolreference=yes ' 74 | } 75 | 76 | # Extract and prepare icon file 77 | $icon = [System.IO.FileStream]::new("$buildFolder\$iconFile", [System.IO.FileMode]::OpenOrCreate) 78 | (Get-CsideIcon).Save($icon) 79 | $icon.Close() 80 | Copy-Item "$buildFolder\$iconFile" "c:\$iconFile" 81 | 82 | Set-Content "$buildFolder\$fileName.ps1" "Start-Process '.\finsql.exe' -ArgumentList ""servername=$SqlServerName, database=$DbName, ntauthentication=$useNtAuth, id=$Id $generateSymbolRefStr""" -Force 83 | & (Join-Path $PSScriptRoot 'ps2exe.ps1') -inputFile "$buildFolder\$fileName.ps1" -outputFile "$buildFolder\$fileName" -iconFile "$iconFile" -noconsole -runtime40 -wait -end *>$null 84 | 85 | Copy-Item "$buildFolder\$fileName" $FileFullPath -Force | Out-Null 86 | 87 | # Cleanup 88 | Remove-Item $buildFolder -Recurse -Force | Out-Null 89 | Remove-Item "c:\$iconFile" -Force | Out-Null 90 | } 91 | 92 | function Get-CsideIcon { 93 | [CmdletBinding()] 94 | param( 95 | ) 96 | 97 | Add-Type -AssemblyName System.Drawing 98 | return ([Drawing.Icon]::ExtractAssociatedIcon((Get-ChildItem $roleTailoredClientFolder 'finsql.exe').FullName)) 99 | } -------------------------------------------------------------------------------- /gmsa/my/MyCustomScripts/InstallModules.ps1: -------------------------------------------------------------------------------- 1 | Write-Host "Installing Custom Modules" 2 | 3 | Write-Host " => Installing: RSAT-AD-PowerShell" 4 | Import-Module ServerManager 5 | Add-WindowsFeature RSAT-AD-PowerShell 6 | 7 | Write-Host "Custom Modules Installation Finished" -------------------------------------------------------------------------------- /gmsa/my/MyCustomScripts/SetupMyUsers.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [String]$ServerInstance="NAV", 3 | [String]$permissionSetId = 'SUPER', 4 | [String]$groupIdentity = "[TO-DO:Put your default distribution group if you want]" 5 | ) 6 | 7 | Write-Host "INSTANCE NAME:" $ServerInstance 8 | if ($ServerInstance -eq "") { 9 | Write-Host "Instance has not been specified. Exiting." 10 | Exit 11 | } 12 | Write-Host "PERMISSIONS:" $permissionSetId 13 | if ($permissionSetId -eq "") { 14 | Write-Host "Permission set ID has not been specified. Exiting." 15 | Exit 16 | } 17 | Write-Host "AD GROUP IDENTITY:" $groupIdentity 18 | if ($groupIdentity -eq "") { 19 | Write-Host "Group Identity has not been specified. Exiting." 20 | Exit 21 | } 22 | 23 | # Get users based on the AD security group: 24 | $usersToAdd = Get-ADGroupMember -Identity $groupIdentity 25 | 26 | # Create NAV users: 27 | foreach ($userToAdd in $usersToAdd) { 28 | 29 | Write-Verbose "`n === Adding $($userToAdd.Name) === " 30 | 31 | if (-not (Get-NAVServerUser -ServerInstance $ServerInstance | Where-Object WindowsSecurityID -eq $userToAdd.SID)) { 32 | New-NAVServerUser -ServerInstance $ServerInstance -Sid $userToAdd.SID -FullName $userToAdd.Name 33 | } else { 34 | Write-Warning "User $($userToAdd.Name) already exists." 35 | } 36 | if (-not(Get-NAVServerUserPermissionSet -ServerInstance $ServerInstance -Sid $userToAdd.SID -PermissionSetId $PermissionSetId)) { 37 | New-NAVServerUserPermissionSet -ServerInstance $ServerInstance -PermissionSetId $PermissionSetId -Sid $userToAdd.SID 38 | } else { 39 | Write-Warning "Permissionset $($PermissionSetId) already assigned to user $($userToAdd.Name)." 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /gmsa/my/SetupConfiguration.ps1: -------------------------------------------------------------------------------- 1 | # Invoke default behavior 2 | . (Join-Path $runPath $MyInvocation.MyCommand.Name) 3 | 4 | Write-Host "Running Custom SetupConfiguration.ps1" -ForegroundColor Yellow 5 | 6 | $customConfig.SelectSingleNode("//appSettings/add[@key='BufferedInsertEnabled']").Value = "false" 7 | # $customConfig.SelectSingleNode("//appSettings/add[@key='EnableTaskScheduler']").Value = "true" 8 | $customConfig.SelectSingleNode("//appSettings/add[@key='ServicesLanguage']").Value = "es-ES" 9 | $customConfig.SelectSingleNode("//appSettings/add[@key='ServicesDefaultTimeZone']").Value = "Server Time Zone" 10 | 11 | $CustomConfig.Save($CustomConfigFile) 12 | 13 | Write-Host "Custom SetupConfiguration.ps1 has been successfully finished." -ForegroundColor Yellow -------------------------------------------------------------------------------- /gmsa/my/SetupVariables.ps1: -------------------------------------------------------------------------------- 1 | # Invoke default behavior 2 | . (Join-Path $runPath $MyInvocation.MyCommand.Name) 3 | 4 | $exportClientFolder = 'N' 5 | if (("$env:exportClientFolder") -and ("$env:exportClientFolder" -eq 'Y')) { 6 | $exportClientFolder = 'Y' 7 | } 8 | $exportClientFolderPath = "$env:exportClientFolderPath" 9 | if (!$exportClientFolderPath) { 10 | $exportClientFolderPath = $myPath 11 | } 12 | -------------------------------------------------------------------------------- /gmsa/my/ps2exe.ps1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/gmsa/my/ps2exe.ps1 -------------------------------------------------------------------------------- /helpers/Get-NavVersion.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | 4 | ) 5 | 6 | . (Join-Path $PSScriptRoot 'Test-NavImage.ps1') 7 | 8 | $navVersion = docker inspect -f '{{ index .Config.Labels \"version\" }}' ${NAV_DOCKER_IMAGE} 9 | 10 | if ([System.String]::IsNullOrEmpty($navVersion)) { 11 | Write-Error "The image $NAV_DOCKER_IMAGE does not contain label 'version'." 12 | return -1 13 | } 14 | 15 | $navVersion = [version]$navVersion 16 | 17 | return $navVersion -------------------------------------------------------------------------------- /helpers/Get-NavVersionDir.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | 4 | ) 5 | 6 | $majorPart = . (Join-Path $PSScriptRoot 'Get-NavVersionMajor.ps1') 7 | $navVersionDir = -join ($majorPart, "0") 8 | 9 | return $navVersionDir; -------------------------------------------------------------------------------- /helpers/Get-NavVersionMajor.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | 4 | ) 5 | 6 | $navVersion = . (Join-Path $PSScriptRoot 'Get-NavVersion.ps1') 7 | $majorPart = $navVersion.Major 8 | 9 | return $majorPart; -------------------------------------------------------------------------------- /helpers/Get-Pwd.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | 4 | ) 5 | 6 | $passsec = Read-Host 'Input the user`s password' -AsSecureString 7 | $passplain = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($passsec)) 8 | 9 | return $passplain -------------------------------------------------------------------------------- /helpers/Get-PwdSecured.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [string]$keyFile 4 | ) 5 | 6 | 7 | # Create AES key to encrypt the password: 8 | $Key = New-Object Byte[] 16 # You can use 16, 24, or 32 for AES 9 | [Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($Key) 10 | $Key | out-file $KeyFile 11 | 12 | $passsec = Read-Host 'Input the user`s password' -AsSecureString 13 | $passsec = ConvertFrom-SecureString $passsec -Key $Key 14 | 15 | return $passsec -------------------------------------------------------------------------------- /helpers/Init-Environment.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | 4 | ) 5 | 6 | $ErrorActionPreference = 'Stop' 7 | . (Join-Path $PSScriptRoot 'Test-NavImage.ps1') -------------------------------------------------------------------------------- /helpers/Test-NavImage.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | 4 | ) 5 | 6 | if ([System.String]::IsNullOrEmpty(${NAV_DOCKER_IMAGE})) { 7 | Write-Error "You have to specify the image using 'NAV_DOCKER_IMAGE' variable. Please, read the main README.md file" 8 | return -1 9 | } -------------------------------------------------------------------------------- /local_cside/README.md: -------------------------------------------------------------------------------- 1 | # Examples and use-cases for MS Dynamics NAV on Docker 2 | 3 | ## COPY C/SIDE TO BE ABLE RUN TABLE SYNCHRONIZATION 4 | 5 | This approach aims to solve the problem of the table synchronization in the case you are using *Windows credential mapping hack*. In this particular case you can\`t use *C/SIDE* distributed via *ClickOnce* to sync tables without *gMSA* to achieve schema synchronization. 6 | 7 | We will actually copy the content of the *RoleTailored Client* folder back into the host system. Then we will be able to open NAV from the locally provided *C/SIDE* and also sync schema of the tables correctly. 8 | 9 | You can see the next overrides here in this example. There are two standard files being overridden (in the folder `my`): 10 | - `AdditionalSetup.ps1` - This one executes `Export-ClientFolder` function defined in the following file. 11 | - `HelperFunctions.ps1` - You can see I add two new functions here. `Export-ClientFolder` copies the content of *RoleTailored Client* folder down to your *docker host*. It also `ClientUserSettings.config` file properly configured to be able to run objects directly from *C/SIDE*. The second function called `New-FinSqlExeRunner` creates an executable that will be copied to the same folder. This new `_finsql-on-docker.exe` file will let you run *C/SIDE* with all required parameters (you won\`t need to configure *C/SIDE* parameters like *server name*, *server instance*, *database name* etc.). 12 | 13 | --- 14 | ## This solution is **Running C/SIDE and AL Side-by-Side** aware and it will let you run new NAV versions (since November update) side by side. 15 | --- 16 | ### Specific `docker run` parameters in the example are: 17 | 18 | - `-v $PSScriptRoot\my:c:\run\my ` - Required in this example. Specifies the definition of the Docker mount. In this way, you activate the override mechanism. 19 | 20 | - `-e Auth=Windows` - Required in this example. Was described in the previous examples. 21 | 22 | - `-e username=$env:USERNAME` - Required in this example. Was described in the previous examples. 23 | 24 | - `-e securePassword=$passsec` - Required in this example. Was described in the previous examples. 25 | 26 | - `-e passwordKeyFile='c:\run\my\myAES.key'` - Required in this example. Was described in the previous examples. 27 | 28 | - `-e password=$passplain` - Required in this example. Was described in the previous examples. 29 | 30 | - `-e enableSymbolLoading=Y` - Valid option since **NAV dev-preview November update**. This will activate **Enable Symbol Loading At Startup** on the service tier and also **generate symbol reference** on C/SIDE. This is important for **running C/SIDE and AL Side-by-Side**. 31 | 32 | --- 33 | 34 | ## The output of the `run.ps1` script: 35 | 36 | ![](../media/local_cside_containerStarted.jpg) 37 | 38 | After the container was successfully started you should be able to see one message line in the output: 39 | ``` 40 | Copy RoleTailoted Client files 41 | ``` 42 | This one confirms the files were copied down to your *docker host*. Now, you can enter `my` folder and see the new subfolder called `RoleTailored Client`: 43 | 44 | ![](../media/local_cside_myFolderContent.jpg) 45 | 46 | You can enter the folder and run standard `finsql.exe` or the newly created `_finsql-on-docker.exe` which will run pre-configured `finsql.exe`. This will automatically add `generatesymbolreference=yes` when `-e enableSymbolLoading=Y` was specified. 47 | 48 | ![](../media/local_cside_rtcFolderContent.jpg) 49 | 50 | ![](../media/local_cside_runningCside.jpg) 51 | 52 | You should be able to run objects directly from the *C/SIDE*: 53 | 54 | ![](../media/local_cside_runningRtc.jpg) 55 | 56 | --- 57 | ## Running C/SIDE and AL Side-by-Side 58 | 59 | **Note:** 60 | You have to run the container with this flag: `-e enableSymbolLoading=Y` 61 | 62 | Create new table in *C/SIDE*: 63 | 64 | ![](../media/local_cside_myTableCSIDE.jpg) 65 | 66 | *C/SIDE* will automatically publish all necessary metadata to reflect your changes in **Visual Studio Code**. 67 | 68 | Now you can download symbols from *Visual Studio Code* and you should be able to see your table: 69 | 70 | ![](../media/local_cside_myTableExtension.jpg) -------------------------------------------------------------------------------- /local_cside/my/AdditionalSetup.ps1: -------------------------------------------------------------------------------- 1 | # Invoke default behavior 2 | . (Join-Path $runPath $MyInvocation.MyCommand.Name) 3 | 4 | Export-ClientFolder 5 | -------------------------------------------------------------------------------- /local_cside/my/HelperFunctions.ps1: -------------------------------------------------------------------------------- 1 | # Invoke default behavior 2 | . (Join-Path $runPath $MyInvocation.MyCommand.Name) 3 | 4 | function Export-ClientFolder 5 | { 6 | [CmdletBinding()] 7 | param( 8 | [string]$Path 9 | ) 10 | 11 | if ([System.String]::IsNullOrEmpty($Path)) { 12 | $Path = $myPath; 13 | } 14 | 15 | if (!(Test-Path "$Path\RoleTailored Client" -PathType Container)) { 16 | Write-Host "Copy RoleTailoted Client files" 17 | Copy-Item -path $roleTailoredClientFolder -destination $Path -force -Recurse -ErrorAction Ignore 18 | 19 | $sqlServerName = if ($databaseServer -eq "localhost") { $hostname } else { $databaseServer } 20 | if (!([System.String]::IsNullOrEmpty($databaseInstance))) { 21 | $sqlServerName = "$sqlServerName\$databaseInstance" 22 | } 23 | 24 | $ntAuth = if ($auth -eq "Windows") { $true } else { $false } 25 | 26 | $ClientUserSettingsFileName = "$runPath\ClientUserSettings.config" 27 | [xml]$ClientUserSettings = Get-Content $clientUserSettingsFileName 28 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='Server']").value = "$hostname" 29 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='ServerInstance']").value="NAV" 30 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='ServicesCertificateValidationEnabled']").value="false" 31 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='ClientServicesPort']").value="$publicWinClientPort" 32 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='ACSUri']").value = "" 33 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='DnsIdentity']").value = "$dnsIdentity" 34 | $clientUserSettings.SelectSingleNode("//configuration/appSettings/add[@key='ClientServicesCredentialType']").value = "$Auth" 35 | $clientUserSettings.Save("$Path\RoleTailored Client\ClientUserSettings.config") 36 | 37 | New-FinSqlExeRunner -FileFullPath "$Path\RoleTailored Client\_finsql-on-docker.exe" ` 38 | -SqlServerName $sqlServerName ` 39 | -DbName "$databaseName" ` 40 | -NtAuth $ntAuth ` 41 | -Id "docker_$hostname" ` 42 | -GenerateSymbolRef ($enableSymbolLoadingAtServerStartup -eq $true) 43 | } 44 | } 45 | 46 | function New-FinSqlExeRunner 47 | { 48 | [CmdletBinding()] 49 | param( 50 | [Parameter(Mandatory=$True)] 51 | [string]$FileFullPath, 52 | [Parameter(Mandatory=$True)] 53 | [string]$SqlServerName, 54 | [Parameter(Mandatory=$True)] 55 | [string]$DbName, 56 | [Parameter(Mandatory=$True)] 57 | [bool]$NtAuth, 58 | [Parameter(Mandatory=$True)] 59 | [string]$Id, 60 | [Parameter()] 61 | [bool]$GenerateSymbolRef=$false 62 | ) 63 | 64 | $useNtAuth = If ($NtAuth) { 1 } Else { 0 } 65 | $fileName = Split-Path $FileFullPath -Leaf 66 | $buildFolder = Join-Path $PSScriptRoot '_buildfinsqlrunner' 67 | $iconFile = 'finsqlicon.ico' 68 | 69 | New-Item -ItemType Directory -Path $buildFolder -Force | Out-Null 70 | 71 | $generateSymbolRefStr = "" 72 | if ($GenerateSymbolRef -and (IsEnableSymbolLoadingSupported)) { 73 | $generateSymbolRefStr = ', generatesymbolreference=yes ' 74 | } 75 | 76 | # Extract and prepare icon file 77 | $icon = [System.IO.FileStream]::new("$buildFolder\$iconFile", [System.IO.FileMode]::OpenOrCreate) 78 | (Get-CsideIcon).Save($icon) 79 | $icon.Close() 80 | Copy-Item "$buildFolder\$iconFile" "c:\$iconFile" 81 | 82 | Set-Content "$buildFolder\$fileName.ps1" "Start-Process '.\finsql.exe' -ArgumentList ""servername=$SqlServerName, database=$DbName, ntauthentication=$useNtAuth, id=$Id $generateSymbolRefStr""" -Force 83 | & (Join-Path $PSScriptRoot 'ps2exe.ps1') -inputFile "$buildFolder\$fileName.ps1" -outputFile "$buildFolder\$fileName" -iconFile "$iconFile" -noconsole -runtime40 -wait -end *>$null 84 | 85 | Copy-Item "$buildFolder\$fileName" $FileFullPath -Force | Out-Null 86 | 87 | # Cleanup 88 | Remove-Item $buildFolder -Recurse -Force | Out-Null 89 | Remove-Item "c:\$iconFile" -Force | Out-Null 90 | } 91 | 92 | function IsEnableSymbolLoadingSupported 93 | { 94 | [CmdletBinding()] 95 | param( 96 | ) 97 | 98 | return ($(Get-NavVersion) -ge [System.Version]'11.0.19097.0') 99 | } 100 | 101 | function Get-NavVersion 102 | { 103 | [CmdletBinding()] 104 | param( 105 | ) 106 | 107 | $finSql = Get-ChildItem $roleTailoredClientFolder 'finsql.exe' 108 | 109 | return $finSql.VersionInfo.ProductVersionRaw 110 | } 111 | function Get-CsideIcon { 112 | [CmdletBinding()] 113 | param( 114 | ) 115 | 116 | Add-Type -AssemblyName System.Drawing 117 | return ([Drawing.Icon]::ExtractAssociatedIcon((Get-ChildItem $roleTailoredClientFolder 'finsql.exe').FullName)) 118 | } -------------------------------------------------------------------------------- /local_cside/my/ps2exe.ps1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/local_cside/my/ps2exe.ps1 -------------------------------------------------------------------------------- /local_cside/run.ps1: -------------------------------------------------------------------------------- 1 | $hostname = "navex-cside" 2 | 3 | # SETTINGS: 4 | # Create AES key to encrypt the password: 5 | $KeyFile = ".\my\myAES.key" 6 | $Key = New-Object Byte[] 16 # You can use 16, 24, or 32 for AES 7 | [Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($Key) 8 | $Key | out-file $KeyFile 9 | 10 | $passsec = Read-Host 'Input the user`s password' -AsSecureString 11 | $passsec = ConvertFrom-SecureString $passsec -Key $Key 12 | 13 | docker run ` 14 | -m 2G ` 15 | --name $hostname ` 16 | --hostname $hostname ` 17 | -v $PSScriptRoot\my:c:\run\my ` 18 | -e Accept_eula=Y ` 19 | -e Auth=Windows ` 20 | -e username=$env:USERNAME ` 21 | -e securePassword=$passsec ` 22 | -e passwordKeyFile='c:\run\my\myAES.key' ` 23 | -e enableSymbolLoading=Y ` 24 | ${NAV_DOCKER_IMAGE} 25 | -------------------------------------------------------------------------------- /media/basic_containerStarted.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/basic_containerStarted.jpg -------------------------------------------------------------------------------- /media/basic_userpwd_containerList.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/basic_userpwd_containerList.jpg -------------------------------------------------------------------------------- /media/basic_userpwd_containerStarted_01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/basic_userpwd_containerStarted_01.jpg -------------------------------------------------------------------------------- /media/basic_userpwd_containerStarted_02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/basic_userpwd_containerStarted_02.jpg -------------------------------------------------------------------------------- /media/basic_userpwd_dockerInspect_01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/basic_userpwd_dockerInspect_01.jpg -------------------------------------------------------------------------------- /media/basic_winauth_clickOnceInstallation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/basic_winauth_clickOnceInstallation.jpg -------------------------------------------------------------------------------- /media/basic_winauth_clickOnce_RTC.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/basic_winauth_clickOnce_RTC.jpg -------------------------------------------------------------------------------- /media/basic_winauth_containerStarted.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/basic_winauth_containerStarted.jpg -------------------------------------------------------------------------------- /media/basic_winauth_securePwd_inspect.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/basic_winauth_securePwd_inspect.jpg -------------------------------------------------------------------------------- /media/basic_winauth_vsCode_changeLocale.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/basic_winauth_vsCode_changeLocale.jpg -------------------------------------------------------------------------------- /media/basic_winauth_vsCode_installExtensionCmd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/basic_winauth_vsCode_installExtensionCmd.jpg -------------------------------------------------------------------------------- /media/basic_winauth_vsCode_launchConfigAndDownloadSymbols.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/basic_winauth_vsCode_launchConfigAndDownloadSymbols.jpg -------------------------------------------------------------------------------- /media/git-sync_demo_01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/git-sync_demo_01.gif -------------------------------------------------------------------------------- /media/local_cside_containerStarted.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/local_cside_containerStarted.jpg -------------------------------------------------------------------------------- /media/local_cside_myFolderContent.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/local_cside_myFolderContent.jpg -------------------------------------------------------------------------------- /media/local_cside_myTableCSIDE.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/local_cside_myTableCSIDE.jpg -------------------------------------------------------------------------------- /media/local_cside_myTableExtension.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/local_cside_myTableExtension.jpg -------------------------------------------------------------------------------- /media/local_cside_rtcFolderContent.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/local_cside_rtcFolderContent.jpg -------------------------------------------------------------------------------- /media/local_cside_runningCside.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/local_cside_runningCside.jpg -------------------------------------------------------------------------------- /media/local_cside_runningRtc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/local_cside_runningRtc.jpg -------------------------------------------------------------------------------- /media/share_mount_addins_listFolderContent.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/share_mount_addins_listFolderContent.jpg -------------------------------------------------------------------------------- /media/share_mount_addins_viewFolder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/share_mount_addins_viewFolder.jpg -------------------------------------------------------------------------------- /media/swarm_winauth_containersList.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/swarm_winauth_containersList.jpg -------------------------------------------------------------------------------- /media/swarm_winauth_createSecret.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/swarm_winauth_createSecret.jpg -------------------------------------------------------------------------------- /media/swarm_winauth_dockerInfo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/swarm_winauth_dockerInfo.jpg -------------------------------------------------------------------------------- /media/swarm_winauth_mulitreplicasListContainers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/swarm_winauth_mulitreplicasListContainers.jpg -------------------------------------------------------------------------------- /media/swarm_winauth_mulitreplicasLogs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/swarm_winauth_mulitreplicasLogs.jpg -------------------------------------------------------------------------------- /media/swarm_winauth_mulitreplicasPingA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/swarm_winauth_mulitreplicasPingA.jpg -------------------------------------------------------------------------------- /media/swarm_winauth_mulitreplicasPingB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/swarm_winauth_mulitreplicasPingB.jpg -------------------------------------------------------------------------------- /media/swarm_winauth_scaleReplicas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/swarm_winauth_scaleReplicas.jpg -------------------------------------------------------------------------------- /media/swarm_winauth_servicesListAndLog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/swarm_winauth_servicesListAndLog.jpg -------------------------------------------------------------------------------- /media/swarm_winauth_verifyPwdFromTheContainer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koubek/nav-docker-examples/d35eb62eecee0a1db6d050a49c114eff36638b0c/media/swarm_winauth_verifyPwdFromTheContainer.jpg -------------------------------------------------------------------------------- /share_mount_addins/Add-ins/README.md: -------------------------------------------------------------------------------- 1 | ## Put your add-ins here. -------------------------------------------------------------------------------- /share_mount_addins/README.md: -------------------------------------------------------------------------------- 1 | # Examples and use-cases for MS Dynamics NAV on Docker 2 | 3 | ## SHARE DATA BETWEEN YOUR DOCKER HOST AND A CONTAINER. 4 | 5 | Sharing data between a *Docker host* and containers can be achieved using [Docker Volumes](https://docs.docker.com/engine/admin/volumes/volumes/). We can use this approach for several things like sharing data (data persistance - in case you host your SQL db in a container), scripts or add-ins. 6 | 7 | Will demonstrate the technology in the following example where we will share a folder with *add-ins*. 8 | 9 | You can see there is a folder called **Add-ins** in the same folder the file `run.ps1` is being located. And we want to share this folder. 10 | 11 | ### Specific `docker run` parameters in the example are: 12 | 13 | - ` -v $PSScriptRoot\Add-ins:"C:\Program Files\Microsoft Dynamics NAV\$navVersionDir\Service\Add-ins\Docker-Share"` - Required in this example. Specifies the definition of the Docker mount. 14 | On the left side you configure the host folder (**this MUST exist**). 15 | On the right side you specify the folder in the container (**this MUST NOT exist** as it **will be created automatically**). 16 | You can see we use `$PSScriptRoot` on the host to adjust the absolute path. 17 | On the container\`s part we use `$navVersionDir` variable that is being assigned on the beginning of the script. This actually sets automatically the NAV version folder. You can replace it manually (in case of NAV 2017 you have to set **100**). 18 | 19 | ```PowerShell 20 | -v host_path:container_path 21 | # A simple example (host:container) 22 | -v c:\docker\share:c:\global_share 23 | ``` 24 | 25 | 26 | --- 27 | 28 | ## The result of the `run.ps1` script: 29 | 30 | We can validate that the folder we specified before has been created in the container and that any changes propagates in both directions. 31 | 32 | - Let\`s enter the container: 33 | ```PowerShell 34 | # Run the following on the docker host: 35 | docker exec -it navex-share-mount-addins powershell 36 | ``` 37 | 38 | - Run the following in the container to validate that everything works correctly: 39 | ```PowerShell 40 | # Go to the "add-ins" folder (the path is version-based!!!): 41 | cd '.\Program Files\microsoft dynamics nav\100\Service\Add-ins\' 42 | 43 | # Find the folder (we know that the name contains 'docker' substring): 44 | ls *dock* 45 | ``` 46 | 47 | ![](../media/share_mount_addins_viewFolder.jpg) 48 | 49 | ```PowerShell 50 | # Enter the shared folder: 51 | cd .\docker-share\ 52 | 53 | # Show the content of the folder: 54 | ls 55 | ``` 56 | 57 | You should be able to see all files you can see on the host (in my case I can see `README.md` file, this is OK). 58 | 59 | ![](../media/share_mount_addins_listFolderContent.jpg) 60 | 61 | 62 | -------------------------------------------------------------------------------- /share_mount_addins/run.ps1: -------------------------------------------------------------------------------- 1 | $hostname = "navex-share-mount-addins" 2 | 3 | # SETTINGS: 4 | . (Join-Path $PSScriptRoot '..\helpers\Init-Environment.ps1') 5 | $navVersionDir = & (Join-Path $PSScriptRoot '..\helpers\Get-NavVersionDir.ps1') 6 | $passplain = & (Join-Path $PSScriptRoot '..\helpers\Get-Pwd.ps1') 7 | 8 | # DOCKER RUN: 9 | docker run ` 10 | --rm ` 11 | -m 3G ` 12 | --name $hostname ` 13 | --hostname $hostname ` 14 | -v $PSScriptRoot\Add-ins:"C:\Program Files\Microsoft Dynamics NAV\$navVersionDir\Service\Add-ins\Docker-Share" ` 15 | -e Accept_eula=Y ` 16 | -e Auth=Windows ` 17 | -e username=Jakub ` 18 | -e password=$passplain ` 19 | ${NAV_DOCKER_IMAGE} 20 | 21 | $passplain = $null -------------------------------------------------------------------------------- /swarm_winauth/README.md: -------------------------------------------------------------------------------- 1 | # Examples and use-cases for MS Dynamics NAV on Docker 2 | 3 | ## WINDOWS AUTHENTICATION ON DOCKER SWARM (USING DOCKER SECRETS) 4 | 5 | **Note:** Docker 17.06 or higher is required. 6 | 7 | This example guide you through the Docker Swarm initialization process. Next we will use [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/) to securely store our password in a strongly encrypted storage provided by Docker. Then we will create a [Docker Service](https://docs.docker.com/engine/reference/commandline/service_create/) that will consume previously created password and will use it in the derived container to complete windows account credentials of your Windows user. And in the last step we will scale the service to see how easily can be scaled NAV services using Docker Swarm. 8 | 9 | **Note:** What **Docker Service** concept means and how does it work can be seen here: [How services work](https://docs.docker.com/engine/swarm/how-swarm-mode-works/services/). 10 | 11 | ### Init Docker Swarm 12 | 13 | **Note:** You can skip this step in case you are already running Docker Swarm (version 17.06) and have access to the manager node. 14 | 15 | I will promote my local Win10 on Docker Swarm Manager node. I am considering this step only for testing purposes so I won\`t any other nodes to be added into the swarm. In this case I will **advertise** my node using the **loopback interface**: 16 | 17 | ``` 18 | docker swarm init --advertise-addr=127.0.0.1 19 | ``` 20 | 21 | **Note:** In case you expect another nodes will be added you should probably use a static IP address to advertise your node. 22 | 23 | Now you can run `docker info`. You should be able something similar to confirm you are using an active swarm node: 24 | 25 | ![](../media/swarm_winauth_dockerInfo.jpg) 26 | 27 | 28 | ### Set your password as a Docker Secret 29 | 30 | This step is very simple. You should modify and run the following command: 31 | 32 | ``` 33 | echo '[my_win_pwd]' | docker secret create mywinuser_win_pwd - 34 | ``` 35 | 36 | - `[my_win_pwd]` - Change this dummy value and put your real password. 37 | 38 | - `mywinuser_win_pwd` - You can keep this value as it is. In this case this will work using the `run.ps1` example. If you change the value, you should change also `-e secretPassword=mywinuser_win_pwd` and `--secret=mywinuser_win_pwd`. The right side has to match the new value. This value simply work as *a pointer* to the secure storage where the secrets are being stored in. 39 | 40 | And in the following screenshot you can see how simple it really is: 41 | 42 | ![](../media/swarm_winauth_createSecret.jpg) 43 | 44 | 45 | ### Specific `docker service create` parameters in the example are: 46 | 47 | - `--secret=mywinuser_win_pwd` - Specify secret identifier. This secret will be shared in a unencrypted form with the container. Docker Swarm Engine will create (by default) `mywinuser_win_pwd` fie in `C:\ProgramData\docker\secrets`. This file - `C:\ProgramData\docker\secrets\mywinuser_win_pwd` - will contain your password. We will consume it later. 48 | 49 | - `-e secretPassword=mywinuser_win_pwd` - Assigns the name of the file containing the secret to the environmental variable named `secretPassword`. `mywinuser_win_pwd` here must match `mywinuser_win_pwd` in the `--secret` flag. This parameter could be avoided in some circumstances (will be explained bellow). 50 | 51 | - `--mount type=bind,source=$PSScriptRoot\my,destination=c:\run\my` - Share our local **my** sub-folder with the container created by the docker service. You can see there is a file in the folder. `SetupVariables.ps1` is just an override we will use to read the password passed by the engine into the container. 52 | 53 | ```PowerShell 54 | # Invoke default behavior 55 | . (Join-Path $runPath $MyInvocation.MyCommand.Name) 56 | 57 | if (!([System.String]::IsNullOrEmpty($env:secretPassword))) { 58 | $password = Get-Content(Join-Path 'C:\ProgramData\docker\secrets' $env:secretPassword) 59 | Remove-Item (Join-Path 'C:\ProgramData\docker\secrets' $env:secretPassword) -Force 60 | } 61 | ``` 62 | 63 | - When I have mentioned we could avoid some sort of duplicity presented by the second parameter (`-e secretPassword=mywinuser_win_pwd`) I was actually pointing to `SetupVariables.ps1` file. 64 | 65 | - You could simply adjust you `SetupVariables.ps1` and change the logic to avoid using `$env:secretPassword`. Instead, you should define a fixed name of the secret being used by the script. Simply said - you should always use `--secret=mywinuser_win_pwd` (regards my current definitions). 66 | 67 | - I can imagine this approach could be used for storing your/*single* WinAccount pwd on your *single node local cluster* but won\`t be acceptable in a multi-user environment on a real cluster (where you need to define multiple password etc. and so need to define multiple secret identifier). 68 | 69 | - My current solution is more flexible (the positive aspect) in the costs *'duplicity'* (the negative aspect). 70 | 71 | **Note:** You can read about *Mounts* [here](https://docs.docker.com/engine/reference/commandline/service_create/#add-bind-mounts-or-volumes). 72 | 73 | - `-e username=MyWinUser` - **Set your Windows account!!!** Don\`t forget to change this value to match your own credential (eg: `-e username=Jakub` in my particular case). 74 | 75 | **Note:** You can see I override the **health check** parameters. I had to change them because the containers were starting a bit slower than expected by the internal health check definitions. You can keep them as they are right now or you can do some changes (change/remove my overrides or even disable **health checks** completely using `--no-healthcheck`). 76 | 77 | 78 | ## The output of the `run.ps1` script: 79 | 80 | **Nothe:** Please, be patient and wait some time. The service need to do some work (schedule a task, create a container etc.). It is possible you will see for some specific time (one, two or three minutes **0/1 REPLICAS** but this should change after some short period). 81 | 82 | In this case we want to list **running services** using `docker service ls` rather then listing running containers (however this is possible as well). We are going to check the logs in the same manner, against the service we have created using `docker service logs [service_name]` (in our particular example you can run `docker service logs navex-swarm-winauth`). And the output of the both command can be seen in the following screenshot: 83 | 84 | ![](../media/swarm_winauth_servicesListAndLog.jpg) 85 | 86 | As I have already mentioned, you can see list the containers as well as the service actually creates containers as well. The names of the containers won\`t exactly match the service name, instead they will contain the name of the service + some postfix added by Docker Swarm engine. 87 | 88 | Let\`s run `docker ps -a` command: 89 | 90 | ![](../media/swarm_winauth_containersList.jpg) 91 | 92 | If you want to work with the container somehow (to see the logs of a specific container only) you need to reference the compounded name you can see in the previous screenshot. We will use this approach practically in the next section. 93 | 94 | 95 | ## Access the container to confirm the theory about the secrets: 96 | 97 | **Note:** To be able to pass through the *verification process* described in this section you will need adjust the script `.\my\SetupVariables.ps1"` like this (remove the line where I delete the secret file from the disk to enhance even more the security aspect): 98 | 99 | ```PowerShell 100 | # Invoke default behavior 101 | . (Join-Path $runPath $MyInvocation.MyCommand.Name) 102 | 103 | if (!([System.String]::IsNullOrEmpty($env:secretPassword))) { 104 | $password = Get-Content(Join-Path 'C:\ProgramData\docker\secrets' $env:secretPassword) 105 | # REMOVE THE FOLLOWING LINE TO BE ABLE VERIFY THE THEORY ABOUT OUR DOCKER SECRET: 106 | # Remove-Item (Join-Path 'C:\ProgramData\docker\secrets' $env:secretPassword) -Force 107 | } 108 | ``` 109 | 110 | 111 | Let\`s use the container name we have seen in the previous step and let\`s access the container using `docker exec` command. This will redirect us into the container and everything we will be running will be executed in the context of the container. 112 | 113 | ```PowerShell 114 | # navex-swarm-winauth.1.osje6acpecmt9e6mkckh9b6hj is the name of the container 115 | # thas has been created. 116 | docker exec -it navex-swarm-winauth.1.osje6acpecmt9e6mkckh9b6hj powershell 117 | ``` 118 | 119 | Then in the container navigate to `C:\ProgramData\Docker\secrets` path and list the content of the directory. You should be able to see the file `mywinuser_win_pwd` I have mentioned before. You can display its content. All these three steps has been captured on the following screenshot: 120 | 121 | ![](../media/swarm_winauth_verifyPwdFromTheContainer.jpg) 122 | 123 | 124 | ## Scale our services using Docker Swarm: 125 | 126 | **Note:** In this case we will actually scale the whole image with all services inside. This mean we won\`t emulate the real scenario with one SQL DB and multiple NAV services around. To do so you need to adjust our current solution and probably the ideal way would be using also **gMSA** and wait until **Docker 17.06 EE** will be released. 127 | 128 | You probably know you can use Docker Swarm to scale your service across all Swarm nodes. This can be achieved in a really simple way running the following command: 129 | 130 | ``` 131 | docker service scale navex-swarm-winauth=COUNT 132 | ``` 133 | ![](../media/swarm_winauth_scaleReplicas.jpg) 134 | 135 | Where **COUNT** = count of replicas you want to distribute across the swarm. 136 | 137 | You can see service logs to understand better what is happening under the hood. You can see there are actually two instances, each has its own IP address but both shares the same hostname. You can also see that the log mixes the outputs of the both replicas (that are running in my case locally on the same node, of course). 138 | 139 | ![](../media/swarm_winauth_mulitreplicasLogs.jpg) 140 | 141 | You can list the containers and see that there are just two instances/containers belonging to the service. 142 | 143 | ![](../media/swarm_winauth_mulitreplicasListContainers.jpg) 144 | 145 | You can confirm balancing capabilities using simple `ping navex-swarm-winauth` with some time lags between the *pings* (sorry, I have IPv6 active so it is less transparent to see the differences): 146 | 147 | ![](../media/swarm_winauth_mulitreplicasPingA.jpg) 148 | 149 | ![](../media/swarm_winauth_mulitreplicasPingB.jpg) 150 | 151 | 152 | ## Conclusions: 153 | 154 | - You can simply promote your local Docker Engine into the Docker Swarm node using one command. 155 | 156 | - You can use Docker on Docker Swarm on your Windows 10 without any troubles because the version **17.06 CE** solves many problems and adds many features that were missing in the previous versions. On the Windows Server 2016 the situation is different as the version **17.06 EE** has not been delivered yet. 157 | 158 | - You can use *Docker Secrets* in a simple way to secure your sensitive data being passed into the container. Especially from the docker host perspective (you can see the secrets only from the explicitly defined containers). 159 | 160 | - You should control access to your **Docker API**. In this manner nobody can access your containers directly using `docker exec`. Watch [Set Docker Security Group](https://docs.microsoft.com/virtualization/windowscontainers/manage-docker/configure-docker-daemon#set-docker-security-group). 161 | 162 | **Note:** You can access your containers from the host using `Enter-PSSession -ContainerId [container_name_or_id]` but each user accessing in this manner is not a container administrators and can\`t access the folders requiring admin privileges. 163 | 164 | - You can use Docker Swarm to scale your service across the nodes in a really simple way -------------------------------------------------------------------------------- /swarm_winauth/my/SetupVariables.ps1: -------------------------------------------------------------------------------- 1 | # Invoke default behavior 2 | . (Join-Path $runPath $MyInvocation.MyCommand.Name) 3 | 4 | if (!([System.String]::IsNullOrEmpty($env:secretPassword))) { 5 | $password = Get-Content(Join-Path 'C:\ProgramData\docker\secrets' $env:secretPassword) 6 | Remove-Item (Join-Path 'C:\ProgramData\docker\secrets' $env:secretPassword) -Force 7 | } -------------------------------------------------------------------------------- /swarm_winauth/run.ps1: -------------------------------------------------------------------------------- 1 | $hostname = "navex-swarm-winauth" 2 | 3 | docker service create ` 4 | --name=$hostname ` 5 | --hostname=$hostname ` 6 | --limit-memory=2G ` 7 | --secret=mywinuser_win_pwd ` 8 | --mount type=bind,source=$PSScriptRoot\my,destination=c:\run\my ` 9 | --health-interval=60s ` 10 | --health-timeout=20s ` 11 | --health-retries=5 ` 12 | -e Accept_eula=Y ` 13 | -e Auth=Windows ` 14 | -e clickonce=Y ` 15 | -e username=$env:USERNAME ` 16 | -e secretPassword=mywinuser_win_pwd ` 17 | ${NAV_DOCKER_IMAGE} --------------------------------------------------------------------------------