csharp comments edit

I was doing some AutoMapper-ing the other day, converting my data object…

public class Source
{
  public Source();
  public string Description { get; set; }
  public DateTimeOffset? ExpireDateTime { get; set; }
  public string Value { get; set; }
}

…into an object needed for a system we’re integrating with.

public class Destination
{
  public Destination();
  public Destination(string value, DateTime? expiration = null);
  public Destination(string value, string description, DateTime? expiration = null);
  public string Description { get; set; }
  public DateTime? Expiration { get; set; }
  public string Value { get; set; }
}

It appeared to me that the most difficult thing here was going to be mapping ExpireDateTime to Expiration. Unfortunately, this was more like a three-hour tour.

I started out creating the mapping like this (in a mapping Profile):

// This is not the answer.
this.CreateMap<Source, Destination>()
    .ForMember(dest => dest.Expiration, opt.MapFrom(src => src.ExpireDateTime));

This didn’t work because there’s no mapping from DateTimeOffset? to DateTime?. I next made a mistake that I think I make every time I run into this and have to relearn it, which is that I created that mapping, too.

// Still not right.
this.CreateMap<Source, Destination>()
    .ForMember(dest => dest.Expiration, opt.MapFrom(src => src.ExpireDateTime));
this.CreateMap<DateTimeOffset?, DateTime?>()
    .ConvertUsing(input => input.HasValue ? input.Value.DateTime : null);

It took a few tests to realize that AutoMapper handles nullable for you, so I was able to simplify a bit.

// Getting closer - don't map nullable, map the base type.
this.CreateMap<Source, Destination>()
    .ForMember(dest => dest.Expiration, opt.MapFrom(src => src.ExpireDateTime));
this.CreateMap<DateTimeOffset, DateTime>()
    .ConvertUsing(input => input.DateTime);

However, it seemed that no matter what I did, the Destination.Expiration was always null. For the life of me, I couldn’t figure it out.

Then I had one of those “eureka” moments when I was thinking about how Autofac handles constructors: It chooses the constructor with the most parameters that it can fulfill from the set of registered services.

I looked again at that Destination object and realized there were three constructors, two of which default the Expiration value to null. AutoMapper also handles constructors in a way similar to Autofac. From the docs about ConstructUsing:

AutoMapper will automatically match up destination constructor parameters to source members based on matching names, so only use this method if AutoMapper can’t match up the destination constructor properly, or if you need extra customization during construction.

That’s it! The answer is to pick the zero-parameter constructor so the mapping isn’t skipped.

// This is the answer!
this.CreateMap<Source, Destination>()
    .ForMember(dest => dest.Expiration, opt.MapFrom(src => src.ExpireDateTime))
    .ConstructUsing((input, context) => new Destination());
this.CreateMap<DateTimeOffset, DateTime>()
    .ConvertUsing(input => input.DateTime);

Hopefully that will save you some time if you run into it. Also, hopefully it will save me some time next time I’m stumped because I can search and find my own blog… which happens more often than you might think.

halloween, costumes comments edit

This year we had 140 trick-or-treaters. This is pretty low for us, but I can’t complain since it’s the first year after COVID-19.

2021: 140 trick-or-treaters.

Average Trick-or-Treaters by Time Block

Year-Over-Year Trick-or-Treaters

Halloween was on a Sunday and it was chilly and windy. It had been raining a bit but didn’t rain during prime trick-or-treat time.

We didn’t hand out candy last year due to the COVID-19 outbreak. Looking up and down our street, it appeared a lot of people chose again this year to not hand out candy. We also saw some “take one” bowls on porches and various creative “candy torpedo tubes” that would send candy from the porch to the kid in a distanced fashion.

Cumulative data:

  Time Block
Year 6:00p - 6:30p 6:30p - 7:00p 7:00p - 7:30p 7:30p - 8:00p 8:00p - 8:30p Total
2006 52 59 35 16 0 162
2007 5 45 39 25 21 139
2008 14 71 82 45 25 237
2009 17 51 72 82 21 243
2010 19 77 76 48 39 259
2011 31 80 53 25 0 189
2013 28 72 113 80 5 298
2014 19 54 51 42 10 176
2015 13 14 30 28 0 85
2016 1 59 67 57 0 184
2019 1 56 59 41 33 190
2021 16 37 30 50 7 140

Our costumes this year:

  • Me: Prisoner Loki from the Disney+ Loki show
  • Jenn: Medusa
  • Phoenix: Cerise Hood from Ever After High

Me as Prisoner Loki

linux, mac, windows comments edit

I used to think setting up your PATH for your shell - whichever shell you like - was easy. But then I got into a situation where I started using more than one shell on a regular basis (both PowerShell and Bash) and things started to break down quickly.

Specifically, I have some tools that are installed in my home directory. For example, .NET global tools get installed at ~/.dotnet/tools and I want that in my path. I would like this to happen for any shell I use, and I have multiple user accounts on my machine for testing scenarios so I’d like it to ideally be a global setting, not something I have to configure for every user.

This is really hard.

I’ll gather some of my notes here on various tools and strategies I use to set paths. It’s (naturally) different based on OS and shell.

This probably won’t be 100% complete, but if you have an update, I’d totally take a PR on this blog entry.

Shell Tips

Each shell has its own mechanism for setting up profile-specific values. In most cases this is the place you’ll end up setting user-specific paths - paths that require a reference to the user’s home directory. On Mac and Linux, the big takeaway is to use /etc/profile. Most shells appear to interact with that file on some level.

PowerShell

PowerShell has a series of profiles that range from system level (all users, all hosts) through user/host specific (current user, current host). The one I use the most is “current user, current host” because I store my profile in a Git repo and pull it into the correct spot on my local machine. I don’t currently modify the path from my PowerShell profile.

  • On Windows, PowerShell will use the system/user path setup on launch and then you can modify it from your profile.
  • On Mac and Linux, PowerShell appears to evaluate the /etc/profile and ~/.profile, then subsequently use its own profiles for the path. On Mac this includes evaluation of the path_helper output. (See the Mac section below for more on path_helper.) I say “appears to evaluate” because I can’t find any documentation on it, yet that’s the behavior I’m seeing. I gather this is likely due to something like a login shell (say zsh) executing first and then having that launch pwsh, which inherits the variables. I’d love a PR on this entry if you have more info.

If you want to use PowerShell as a login shell, on Mac and Linux you can provide the -Login switch (as the first switch when running pwsh!) and it will execute sh to include /etc/profile and ~/.profile execution before launching the PowerShell process. See Get-Help pwsh for more info on that.

Bash

Bash has a lot of profiles and rules about when each one gets read. Honestly, it’s pretty complex and seems to have a lot to do with backwards compatibility with sh along with need for more flexibility and override support.

/etc/profile seems to be the way to globally set user-specific paths. After /etc/profile, things start getting complex, like if you have a .bash_profile then your .profile will get ignored.

zsh

zsh is the default login shell on Mac. It has profiles at:

  • /etc/zshrc and ~/.zshrc
  • /etc/zshenv and ~/.zshenv
  • /etc/zprofile and ~/.zprofile

It may instead use /etc/profile and ~/.profile if it’s invoked in a compatibility mode. In this case, it won’t execute the zsh profile files and will use the sh files instead. See the manpage under “Compatibility” for details or this nice Stack Overflow answer.

I’ve set user-specific paths in /etc/profile and /etc/zprofile, which seems to cover all the bases depending on how the command gets invoked.

Operating System Tips

Windows

Windows sets all paths in the System => Advanced System Settings => Environment Variables control panel. You can set system or user level environment variables there.

The Windows path separator is ;, which is different than Mac and Linux. If you’re building a path with string concatenation, be sure to use the right separator.

Mac and Linux

I’ve lumped these together because, with respect to shells and setting paths, things are largely the same. The only significant difference is that Mac has a tool called path_helper that is used to generate paths from a file at /etc/paths and files inside the folder /etc/paths.d. Linux doesn’t have path_helper.

The file format for /etc/paths and files in /etc/paths.d is plain text where each line contains a single path, like:

/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin

Unfortunately, path_helper doesn’t respect the use of variables - it will escape any $ it finds. This is a good place to put global paths, but not great for user-specific paths.

In /etc/profile there is a call to path_helper to evaluate the set of paths across these files and set the path. I’ve found that just after that call is a good place to put “global” user-specific paths.

if [ -x /usr/libexec/path_helper ]; then
  eval `/usr/libexec/path_helper -s`
fi

PATH="$PATH:$HOME/go/bin:$HOME/.dotnet/tools:$HOME/.krew/bin"

Regardless of whether you’re on Mac or Linux, /etc/profile seems to be the most common place to put these settings. Make sure to use $HOME instead of ~ to indicate the home directory. The ~ won’t get expanded and can cause issues down the road.

If you want to use zsh, you’ll want the PATH set block in both /etc/profile and /etc/zprofile so it handles any invocation.

The Mac and Linux path separator is :, which is different than Windows. If you’re building a path with string concatenation, be sure to use the right separator.

kubernetes comments edit

I have a situation that is possibly kind of niche, but it was a real challenge to figure out so I thought I’d share the solution in case it helps you.

I have a Kubernetes cluster with Istio installed. My Istio ingress gateway is connected to an Apigee API management front-end via mTLS. Requests come in to Apigee then get routed to a secured public IP address where only Apigee is authorized to connect.

Unfortunately, this results in all requests coming in with the same Host header:

  1. Client requests api.services.com/v1/resource/operation.
  2. Apigee gets that request and routes to 1.2.3.4/v1/resource/operation via the Istio ingress gateway and mTLS.
  3. An Istio VirtualService answers to hosts: "*" (any host header at all) and matches entirely on URL path - if it’s /v1/resource/operation it routes to mysvc.myns.svc.cluster.local/resource/operation.

This is how the ingress tutorial on the Istio site works, too. No hostname-per-service.

However, there are a couple of wrenches in the works, as expected:

  • There are some API endpoints on the service that aren’t exposed through Apigee. They’re internal-only operations that allow for service-to-service communications in the cluster but aren’t for outside callers.
  • I want to do canary deployments and route traffic slowly from an existing version of the service to a new, canary version. I need both the external and internal traffic routed this way to get accurate results.

The combination of these things is a problem. I can’t assume that the match-on-path-regex setting will work for internal traffic - I need any internal service to route properly based on host name. However, you also can’t match on host: "*" for internal traffic that doesn’t come through an ingress. That means I would need two different VirtualService instances - one for internal traffic, one for external.

But if I have two different VirtualService objects to manage, it means I need to keep them in sync over the canary, which kind of sucks. I’d like to set the traffic balancing in one spot and have it work for both internal and external traffic.

I asked how to do this on the Istio discussion forum and thought for a while that a VirtualService delegate would be the answer - have one VirtualService with the load balancing information, a second service for internal traffic (delegating to the load balancing service), and a third service for external traffic (delegating to the load balancing service). It’s more complex, but I’d get the ability to control traffic in one spot.

Unfortunately (the word “unfortunately” shows up a lot here, doesn’t it?), you can’t use delegates on a VirtualService that doesn’t also connect to a gateway. That is, if it’s internal/mesh traffic, you don’t get the delegate support. This issue in the Istio repo touches on that.

Here’s where I landed.

First, I updated Apigee so it takes care of two things for me:

  1. It adds a Service-Host header with the internal host name of the target service, like Service-Host: mysvc.myns.svc.cluster.local. It more tightly couples the Apigee part of things to the service internal structure, but it frees me up from having to route entirely by regex in the cluster. (You’ll see why in a second.) I did try to set the Host header directly, but Apigee overwrites this when it issues the request on the back end.
  2. It does all the path manipulation before issuing the request. If the internal service wants /v1/resource/operation to be /resource/operation, that path update happens in Apigee so the inbound request will have the right path to start.

I did the Service-Host header with an “AssignMessage” policy.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<AssignMessage async="false" continueOnError="false" enabled="true" name="Add-Service-Host-Header">
    <DisplayName>Add Service Host Header</DisplayName>
    <Set>
        <Headers>
            <Header name="Service-Host">mysvc.myns.svc.cluster.local</Header>
        </Headers>
    </Set>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
    <AssignTo createNew="false" transport="http" type="request"/>
</AssignMessage>

Next, I added an Envoy filter to the Istio ingress gateway so it knows to look for the Service-Host header and update the Host header accordingly. Again, I used Service-Host because I couldn’t get Apigee to properly set Host directly. If you can figure that out and get the Host header coming in correctly the first time, you can skip the Envoy filter.

The filter needs to run first thing in the pipeline, before Istio tries to route traffic. I found that pinning it just before the istio.metadata_exchange stage got the job done.

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: propagate-host-header-from-apigee
  namespace: istio-system
spec:
  workloadSelector:
    labels:
      istio: ingressgateway
      app: istio-ingressgateway
  configPatches:
    - applyTo: HTTP_FILTER
      match:
        context: GATEWAY
        listener:
          filterChain:
            filter:
              name: "envoy.http_connection_manager"
            subFilter:
              # istio.metadata_exchange is the first filter in the connection
              # manager, at least in Istio 1.6.14.
              name: "istio.metadata_exchange"
      patch:
        operation: INSERT_BEFORE
        value:
          name: envoy.filters.http.lua
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
            inline_code: |
              function envoy_on_request(request_handle)
                local service_host = request_handle:headers():get("service-host")
                if service_host ~= nil then
                  request_handle:headers():replace("host", service_host)
                end
              end

Finally, the VirtualService that handles the traffic routing needs to be tied both to the ingress and to the mesh gateway. The hosts setting can just be the internal service name, though, since that’s what the ingress will use now.

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: mysvc
  namespace: myns
spec:
  gateways:
    - istio-system/apigee-mtls
    - mesh
  hosts:
    - mysvc
  http:
    - route:
        - destination:
            host: mysvc-stable
          weight: 50
        - destination:
            host: mysvc-baseline
          weight: 25
        - destination:
            host: mysvc-canary
          weight: 25

Once all these things are complete, both internal and external traffic will be routed by the single VirtualService. Now I can control canary load balancing in a single location and be sure that I’m getting correct overall test results and statistics with as few moving pieces as possible.

Disclaimer: There may be reasons you don’t want to treat external traffic the same as internal, like if you have different DestinationRule settings for traffic management inside vs. outside, or if you need to pass things through different authentication filters or whatever. Everything I’m working with is super locked down so I treat internal and external traffic with the same high levels of distrust and ensure that both types of traffic are scrutinized equally. YMMV.

mac, network comments edit

I have a Mac and my user account is attached to a Windows domain. The benefit of this is actually pretty minimal in that I can change my domain password and it propagates to the local Mac user account, but that’s about it. It seems to cause more trouble than it’s worth.

I recently had an issue where something got out of sync and I couldn’t log into my Mac using my domain account. This is sort of a bunch of tips and things I did to recover that.

First, have a separate local admin account. Make it a super complex password and never use it for anything else. This is sort of your escape hatch to try to recover your regular user account. Even if you want to have a local admin account so your regular user account can stay a user and no admin… have a dedicated “escape hatch” admin account that’s separate from the “I use this sometimes for sudo purposes” admin account. I have this, and if I hadn’t, that’d have been the end of it.

It’s good to remember for a domain-joined account there are three security tokens that all need to be kept in sync: Your domain user password, your local machine OS password, and your disk encryption token. When you reboot the computer, the first password you’ll be asked for should unlock the disk encryption. Usually the token for disk encryption is tied nicely to the machine account password so you enter the one password and it both unlocks the disk and logs you in. The problem I was running into was those got out of sync. For a domain-joined account, the domain password usually is also tied to these things.

Next, keep your disk encryption recovery code handy. Store it in a password manager or something. If things get out of sync, you can use the recovery code to unlock the disk and then your OS password to log in.

For me, I was able to log in as my separate local admin account but my machine password wasn’t working unless I was connected to the domain. Only way to connect to the domain was over a VPN. That meant I needed to enable fast user switching so I could connect to the VPN under the separate local admin and then switch - without logging out - to my domain account.

Once I got to my own account I could use the Users & groups app to change my domain password and have the domain and machine accounts re-synchronized. ALWAYS ALWAYS ALWAYS USE USERS & GROUPS TO CHANGE YOUR DOMAIN ACCOUNT PASSWORD. I have not found a way otherwise to ensure everything is in sync. Don’t change it from some other workstation, don’t change it from Azure Active Directory. This is the road to ruin. Stay with Users & Groups.

The last step was that my disk encryption token wasn’t in sync - OS and domain connection was good, but I couldn’t log in after a reboot. I found the answer in a Reddit thread:

su local_admin
sysadminctl -secureTokenStatus domain_account_username
sysadminctl -secureTokenOff domain_account_username \
  -password domain_account_password \
  interactive
sysadminctl -secureTokenOn domain_account_username \
  -password domain_account_password \
  interactive

Basically, as the standalone local admin, turn off and back on again the connection to the drive encryption. This refreshes the token and gets it back in sync.

Reboot, and you should be able to log in with your domain account again.

To test it out, you may want to try changing your password from Users & Groups to see that the sync works. If you get a “password complexity” error, it could be the sign of an issue… or it could be the sign that your domain has a “you can’t change the password more than once every X days” sort of policy and since you changed it earlier you are changing it again too soon. YMMV.

And, again, always change your password from Users & Groups.