Blog

  • Proof-of-Concept: OpenShift on Power: Configuring an OpenID Connect identity provider

    This document outlines the installation of the OpenShift on Power, the installation of the Red Hat Single Sign-On Operator and configuring the two to work together on OCP.

    Thanks to Zhimin Wen who helped in my setup of the OIDC with his great work.

    Steps

    1. Setup OpenShift Container Platform (OCP) 4.x on IBM® Power Systems™ Virtual Server on IBM Cloud using the Terraform based automation code using the documentation provided. You’ll need to update var.tfvars to match your environment and PowerVS Service settings.
    terraform init --var-file=var.tfvars
    terraform apply --var-file=var.tfvars
    
    1. At the end of the deployment, you see an output pointing to the Bastion Server.
    bastion_private_ip = "192.168.*.*"
    bastion_public_ip = "158.*.*.*"
    bastion_ssh_command = "ssh -i data/id_rsa root@158.*.*.*"
    bootstrap_ip = "192.168.*.*"
    cluster_authentication_details = "Cluster authentication details are available in 158.*.*.* under ~/openstack-upi/auth"
    cluster_id = "ocp-oidc-test-cb68"
    install_status = "COMPLETED"
    master_ips = [
      "192.168.*.*",
      "192.168.*.*",
      "192.168.*.*",
    ]
    oc_server_url = "https://api.ocp-oidc-test-cb68.*.*.*.*.xip.io:6443"
    storageclass_name = "nfs-storage-provisioner"
    web_console_url = "https://console-openshift-console.apps.ocp-oidc-test-cb68.*.*.*.*.xip.io"
    worker_ips = [
      "192.168.*.*",
      "192.168.*.*",
    ]
    
    1. Add Hosts Entry
    127.0.0.1 console-openshift-console.apps.ocp-oidc-test-cb68.*.xip.io api.ocp-oidc-test-cb68.*.xip.io oauth-openshift.apps.ocp-oidc-test-cb68.*.xip.io
    
    1. Connect via SSH
    sudo ssh -i data/id_rsa -L 5900:localhost:5901 -L443:localhost:443 -L6443:localhost:6443 -L8443:localhost:8443 root@*
    

    You’re connecting on the commandline for a reason with ports forwarded since not all ports are open on the Bastion Server.

    1. Find the OpenShift kubeadmin password in openstack-upi/auth/kubeadmin-password
    cat openstack-upi/auth/kubeadmin-password
    eZ2Hq-JUNK-JUNKB4-JUNKZN
    
    1. From Login into the web_console_url, navigate to https://console-openshift-console.apps.ocp-oidc-test-cb68.*.xip.io/

    If prompted, accept Security Warnings

    1. Login with the Kubeadmin credentials when promtped
    2. Click OperatorHub
    3. Search for Keycloak
    4. Select Red Hat Single Sign-On Operator
    5. Click Install
    6. On the Install Operator Screen:
      1. Select alpha channel
      2. Select namespace default (if you prefer an alternative namespace, that’s fine this is just a demo)
      3. Click Install
    7. Click on Installed Operators
    8. Watch rhsso-operator for a completed installation, the status should show Succeeded
    9. Once ready, click on the Operator > Red Hat Single Sign-On Operator
    10. Click on Keycloak, create Keycloak
    11. Enter the following YAML:
    apiVersion: keycloak.org/v1alpha1
    kind: Keycloak
    metadata:
      name: example-keycloak
      labels:
        app: sso
    spec:
      instances: 1
      externalAccess:
        enabled: true
    
    1. Once it’s deployed, click on example-keycloak > YAML. Look for status.externalURL.
    status:
      credentialSecret: credential-example-keycloak
      externalURL: 'https://keycloak-default.apps.ocp-oidc-test-cb68.*.xip.io'
    
    1. Update the /etc/hosts with
    127.0.0.1 keycloak-default.apps.ocp-oidc-test-cb68.*.xip.io 
    
    1. Click Workloads > Secrets
    2. Click on credential-example-keycloak
    3. Click Reveal values
    U: admin
    P: <<hidden>>
    
    1. For Keycloak, login to https://keycloak-default.apps.ocp-oidc-test-cb68.*.xip.io/auth/admin/master/console/#/realms/master using the revealed secret
    2. Click Add Realm
    3. Enter name test.
    4. Click Create
    5. Click Client
    6. Click Create
    7. Enter ClientId – test
    8. Select openid-connect
    9. Click Save
    10. Click Keys
    11. Click Generate new keys and certificate
    12. Click Settings > Access Type
    13. Select confidential
    14. Enter Valid Redirect URIs https://* we could set this as the OAuth url such as https://oauth-openshift.apps.ocp-oidc-test-cb68.*.xip.io/*
    15. Click Credentials (Copy the Secret), such as:
    43f4e544-fa95-JUNK-a298-JUNK
    
    1. Under Generate Private Key…
      1. Select Archive Format JKS
      2. Key Password: password
      3. Store Password: password
      4. Click Generate and Download
    2. On the Bastion server, create the keycloak secret
    oc -n openshift-config create secret generic keycloak-client-secret --from-literal=clientSecret=43f4e544-fa95-JUNK-a298-JUNK
    configmap "keycloak-ca" deleted
    
    1. Grab the ingress CA
    oc -n openshift-ingress-operator get secret router-ca -o jsonpath="{ .data.tls\.crt }" | base64 -d -i > ca.crt
    
    1. Create the keycloak CA secret
    oc -n openshift-config create cm keycloak-ca --from-file=ca.crt
    configmap/keycloak-ca created
    
    1. Create the openid Auth Provider
    apiVersion: config.openshift.io/v1
    kind: OAuth
    metadata:
      name: cluster
    spec:
      identityProviders:
        - name: keycloak 
          mappingMethod: claim 
          type: OpenID
          openID:
            clientID: console
            clientSecret:
              name: keycloak-client-secret
            ca:
              name: keycloak-ca
            claims: 
              preferredUsername:
              - preferred_username
              name:
              - name
              email:
              - email
            issuer: https://keycloak-default.apps.ocp-oidc-test-cb68.*.xip.io/auth/realms/test
    
    1. Logout of the Kubeadmin
    2. On Keycloak, Manage > Users, Click add a user with an email and password. Click Save
    3. Click Credentials
    4. Enter a new password and confirm
    5. Turn Temporary Password off
    6. Navigate to the web_console_url
    7. Select the new IdP
    8. Login with the new user

    There is a clear support for OIDC Connect already enabled on OpenShift, and this document outlines how to test with Keycloak.

    A handy link for debugging is the openid-configuration

    Reference

    Blog: Keycloak OIDC Identity Provider for OpenShift

    Proof-of-Concept: OpenShift on Power: Configuring an OpenID Connect identity provider

  • OpenShift RequestHeader Identity Provider with a Test IdP: My GoLang Test

    I built a demonstration using GoLang, JSON, bcrypt, http client, http server to model an actual IDP. This is a demonstration only; it really helped me setup/understand what’s happening in the RequestHeader.

    OpenShift 4.10: Configuring a request header identity provider enables an external service to act as an identity provider where a X-Remote-User header to identify the user’s identity.

    This document outlines the flow using the haproxy and Apache Httpd already installed on the Bastion server as part of the installation process and a local Go Test IdP to demonstrate the feature.

    The rough flow between OpenShift, the User and the Test IdP is:

    My Code is available at https://github.com/prb112/openshift-auth-request-header

  • Debugging Network Traffic

    Debugging weird traffic patterns on the mac, you can use nettop. It shows the actual amount of data transferred by the process. It’s very helpful.

    Commandline

    nettop -m tcp

    Example

    kernel_task.0                                                                                                      1512 MiB        1041 MiB   387 KiB    11 MiB  1823 KiB
       tcp4 1.1.1.30:52104<->1.1.1.29:548                                                     en0   Established        1512 MiB        1041 MiB   387 KiB    11 MiB  1823 KiB 145.12 ms   791 KiB  1545 KiB    BK_SYS
    vpnagentd.88                                                                                                        158 KiB         554 MiB     0 B       0 B      74 B
       tcp4 1.1.1.30:56141<->1.1.1.12:443                                                  en0   Established          26 KiB          12 KiB     0 B       0 B      74 B    77.25 ms   128 KiB    32 KiB        BE
       tcp4 127.0.0.1:29754<->localhost:49229                                                 lo0   Established         131 KiB         554 MiB     0 B       0 B       0 B     1.22 ms   266 KiB   379 KiB        BE
    com.crowdstrike.341                                                                                                 995 KiB        5615 KiB   675 B     279 B      29 KiB
       tcp4 1.1.1.30:51978<->ec2-50-18-194-39.us-west-1.compute.amazonaws.com:443        en0   Established         995 KiB        5615 KiB   675 B     279 B      29 KiB  93.69 ms   128 KiB    55 KiB        RD
    
  • Using OpenShift Plugin for oc

    For those managing OpenShift clusters, the oc tool manages all the OpenShift resources with handy commands for OpenShift and Kubernetes. The OpenShift Client CLI (oc) project is built on top of kubectl adding built-in features to simplify interactions with an OpenShift cluster.

    Much like the kubectl, the oc cli tool provides a feature to Extend the OpenShift CLI with plug-ins. The oc plugins feature is a client-side feature to faciliate interactions with extensions commands; found in the current user’s path. There is an ecosystem of plugins through the community and the Krew Plugin List.

    These plugins include:

    1. cost accessess Kubernetes cost allocation metrics
    2. outdated displays all out-of-date images running in a Kubernetes cluster
    3. pod-lens shows pod-related resource information
    4. k9s is a terminal based UI to interact with your Kubernetes clusters.
    5. sample-cli-plugin which is a simple example to show how to switch namespaces in k8s. I’m not entirely certain that this works with OpenShift.

    These plugins have a wide range of support and code. Some of the plugins are based on python, others are based on go and bash.

    oc expands the plugin search path pkg/cli/kubectlwrappers/wrappers.go in plugin.ValidPluginFilenamePrefixes = []string{"oc", "kubectl"} so whole new OpenShift specific plugins are supported. The OpenShift team has also released a number of plugins:

    1. oc-mirror manages OpenShift release, operator catalog, helm charts, and associated container images for mirror registries that support OpenShift environments
    2. oc-compliance facilitates using the OpenShift Compliance operator.

    Many of these extensions/plugins are installed using krew; krew is a plugin manager for kubectl. Some users create a directory .kube/plugins and install their plugins in that folder. The plugins folder is then added to the user’s path.

    Creating your own Extension

    1. Check to see if any plugins exist:
    $ oc plugin list
    The following compatible plugins are available:
    
    /Users/user/.kube/plugins/oc-test
    

    If none exist, it’ll prompt you that none are found in the path, and you can install from krew.

    1. Create a new file oc-test
    #! /usr/bin/env bash
    
    echo "Execution Time: $(date)"
    
    echo ""
    ps -Sf
    echo ""
    
    echo "Arguments: $@"
    
    echo "Environment Variables: "
    env
    echo ""
    
    oc version --client
    
    1. Add the file to the path.
    export PATH=~/.kube/plugins:$PATH
    
    1. Execute the oc plugin test (note the oc is stripped off)
    Execution Time: Wed Mar 30 11:22:19 EDT 2022
    
      UID   PID  PPID   C STIME   TTY           TIME CMD
      501  3239  3232   0 15Mar22 ttys000    0:01.39 -zsh
      501 80267  3239   0 17Mar22 ttys000    0:00.03 tmux
      501 54273 11494   0 Tue10AM ttys001    0:00.90 /bin/zsh -l
      501 80319 80269   0 17Mar22 ttys002    0:00.30 -zsh
      501  2430  2429   0 15Mar22 ttys003    0:03.17 -zsh
      501 78925  2430   0 11:22AM ttys003    0:00.09 bash /Users/user/.kube/plugins/oc-test test
      501 80353 80269   0 17Mar22 ttys004    0:02.07 -zsh
      501 91444 11494   0 18Mar22 ttys005    0:01.55 /bin/zsh -l
    
    Arguments: test
    Environment Variables: 
    SHELL=/bin/zsh
    TERM=xterm-256color
    ZSH=/Users/user/.oh-my-zsh
    USER=user
    PATH=/Users/user/.kube/plugins:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/go/bin
    PWD=/Users/user/Downloads
    LANG=en_US.UTF-8
    HOME=/Users/user
    LESS=-R
    LOGNAME=user
    SECURITYSESSIONID=user
    _=/usr/bin/env
    
    Client Version: 4.10.6
    

    For the above, a simple plugin demonstration is shown.

    Reference

    1. Getting started with the OpenShift CLI
    2. Extending the OpenShift CLI with plug-ins
    3. https://cloud.redhat.com/blog/augmenting-openshift-cli-with-plugins
    4. https://cloudcult.dev/tcpdump-for-openshift-workloads/
  • Learning Resources for Operators – First Two Weeks Notes

    To quote the Kubernetes website, “The Operator pattern captures how you can write code to automate a task beyond what Kubernetes itself provides.” The following is an compendium to use while Learning Operators.

    The defacto SDK to use is the Operator SDK which provides HELM, Ansible and GO scaffolding to support your implementation of the Operator pattern.

    The following are education classes on the OperatorSDK

    When Running through the CO0201EN intermediate operators course, I did hit the case where I had to create a ClusterRole and ClusterRoleBinding for the ServiceAccount, here is a snippet that might helper others:

    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      namespace: memcached-operator-system
      name: service-reader-cr-mc
    rules:
    - apiGroups: ["cache.bastide.org"] # "" indicates the core API group
      resources: ["memcacheds"]
      verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      namespace: memcached-operator-system
      name: ext-role-binding
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: service-reader-cr-mc
    subjects:
    - kind: ServiceAccount
      namespace: memcached-operator-system
      name: memcached-operator-controller-manager

    The reason for the above, I missed adding a kubebuilder declaration:

    //+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
    //+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch

    Thanks to https://stackoverflow.com/a/60334649/1873438

    The following are articles worth reviewing:

    The following are good Go resources:

    1. Go Code Comments – To write idiomatic Go, you should review the Code Review comments.
    2. Getting to Go: The Journey of Go’s Garbage Collector – The reference for Go and Garbage Collection in go
    3. An overview of memory management in Go – good overview of Go Memory Management
    4. Golang: Cost of using the heap – net 1M allocation seems to stay in the stack, outside it seems to be on the heap
    5. golangci-lint – The aggregated linters project is worthy of an installation and use. It’ll catch many issues and has a corresponding GitHub Action.
    6. Go in 3 Weeks A comprehensive training for Go. Companion to GitHub Repo
    7. Defensive Coding Guide: The Go Programming Language

    The following are good OpenShift resources:

    1. Create OpenShift Plugins – You must have a CLI plug-in file that begins with oc- or kubectl-. You create a file and put it in /usr/local/bin/
    2. Details on running Code Ready Containers on Linux – The key hack I learned awas to ssh -i ~/.crc/machines/crc/id_ecdsa core@<any host in the /etc/hosts>
      1. I ran on VirtualBox Ubuntu 20.04 with Guest Additions Installed
      2. Virtual Box Settings for the Machine – 6 CPU, 18G
        1. System > Processor > Enable PAE/NX and Enable Nested VT-X/AMD-V (which is a must for it to work)
        1. Network > Change Adapter Type to virtio-net and Set Promiscuous Mode to Allow VMS
      3. Install openssh-server so you can login remotely
      4. It will not install without a windowing system, so I have the default windowing environment installed.
      5. Note, I still get a failure on startup complaining about a timeout. I waited about 15 minutes post this, and the command oc get nodes –context admin –cluster crc –kubeconfig .crc/cache/crc_libvirt_4.10.3_amd64/kubeconfig now works.
    3. CRC virsh cheatsheet – If you are running Code Ready Containers and need to debug, you can use the virsh cheatsheet.
  • Hack: Fast Forwarding a Video

    I had to watch 19 hours of slow paced videos for a training on a new software product (at least new to me). I like fast paced trainings… enter a browser hack.

    In Firefox, Navigate to Tools > Browser Tools > Web Developer Tools

    Click Console

    Type the following snippet to find the first video on a page, and change the playback rate, and Click Enter.

    document.getElementById(document.getElementsByTagName('video').item(0).id).playbackRate = 4.0

    Note, 4.0 can be unintelligible, you’ll need to tweak the speed to match what you need. I found 2.5 to 3.0 to be very comfortable (you just can’t multitask).

  • The Grit in Processing Unicode Strings with NDJSON

    Unicode is pretty amazing, you can encode strings in single or multibyte characters. Perhaps a smile… 😀 which is U+1F600. It’s pretty cool, so cool you should read If UTF-8 is an 8-bit encoding, why does it need 1-4 bytes? which has four key sequences for UTF8:

       Char. number range  |        UTF-8 octet sequence
          (hexadecimal)    |              (binary)
       --------------------+---------------------------------------------
       0000 0000-0000 007F | 0xxxxxxx
       0000 0080-0000 07FF | 110xxxxx 10xxxxxx
       0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
       0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
    

    Until recently, I’ve been working with NDJSON files as part of the HL7 FHIR: Bulk Data Access IG to export healthcare data and the proposed Import specification to import healthcare data. These files store one JSON per line and delimite with a \n, such as:

    {"resourceType":"Patient"}
    {"resourceType":"Patient"}
    {"resourceType":"Patient"}
    

    The following Java snippet generates a substation set of lines that can be injected into a stream for testing with unicode (and are Newline Delimited).

    StringBuilder line = new StringBuilder();
    for (int codePoint = 32; codePoint <= 0x1F64F; codePoint++) {
        line.append(Character.toChars(codePoint));
        if (codePoint % 64 == 0) {
            line.append("\n");
        }
    }
    System.out.println(line.toString());
    

    As this data is processed aynchronously in on OpenLiberty: JavaBatch as a set of jobs. These jobs process data through a Read(Source)-Checkpoint-Write(Sink) pattern. The pattern ensures enough data is read from the source before a write action on the sink.

    I found that processing the variable data with an unknown unicode set needed a counting stream to keep track of the bytes. The CountingStream acted as a delegate to accumulate bytes, length of the processed values and find the end of a line or end of the file.

    public static class CountingStream extends InputStream {
            private static int LF = '\n';
            private static final long MAX_LENGTH_PER_LINE = 2147483648l;
    
            // 256kb block
            private ByteArrayOutputStream out = new ByteArrayOutputStream(256000);
            private boolean eol = false;
            private long length = 0;
    
            private InputStream delegate;
    
            /**
             * ctor
             * @param in
             */
            public CountingStream(InputStream in) {
                this.delegate = in;
            }
    
            /**
             * reset the line
             */
            public void resetLine() {
                out.reset();
                eol = false;
            }
    
            /**
             * @return the length of the resources returned in the reader
             */
            public long getLength() {
                return length;
            }
    
            /**
             * Gets the String representing the line of bytes.
             * 
             * @return
             * @throws UnsupportedEncodingException
             */
            public String getLine() throws UnsupportedEncodingException {
                String str = new String(out.toByteArray(), "UTF-8");
                if (str.isEmpty()) {
                    str = null;
                }
                return str;
            }
    
            public boolean eol() {
                return eol;
            }
    
            /**
             * Returns the line that is aggregated up until a new line character
             * @return
             * @throws IOException
             */
            public String readLine() throws IOException {
                int r = read();
                while (r != -1) {
                    if (eol()) {
                        eol = false;
                        return getLine();
                    }
                    r = read();
                }
                if (r == -1 && length > 0) {
                    return getLine();
                }
                return getLine();
            }
    
            @Override
            public int read() throws IOException {
                int r = delegate.read();
                if (r == -1) {
                    return -1;
                }
                byte b = (byte) r;
                if (LF == (int) b) {
                    length++;
                    eol = true;
                } else {
                    length++;
                    if (length == MAX_LENGTH_PER_LINE) {
                        throw new IOException("Current Line in NDJSON exceeds limit " + MAX_LENGTH_PER_LINE);
                    }
                    out.write(b);
                }
                return b;
            }
        }
    

    I found one important thing in the delegate, with thanks from a colleague and CERT – you must accumulate the bytes and have a maximum size per line. The CERT article is at STR50-J. Use the appropriate method for counting characters in a string

    The grit here is:

    1. Accumulate: Don’t process a character int read() at a time, accumulate your bytes and defer to the String creation in Java to ensure it’s processed in your project’s encoding.
    2. Set a limit: Don’t infinitely process the data, stop when it violates a set contract.

    If you are doing more complicated processing, say you are streaming from Azure Blob, Amazon S3 or HTTPS and need to process the stream as chunks. You’ll want to do something a bit more complicated.

    The grit here is:

    1. Read Blocks and not the whole stream: Read a block of bytes at a time instead of ‘draining’ the bytes when a sufficient block is retrieved.
    2. Assemble Lines in multiple Block reads.

    The code looks like this:

        public static class CountingStream extends InputStream {
            private static int LF = '\n';
            private static final long MAX_LENGTH_PER_LINE = 2147483648l;
    
            // 256kb block
            private ByteArrayOutputStream out;
            private long length = 0;
    
            private InputStream delegate;
    
            /**
             * 
             * @param out ByteArrayOutputStream caches the data cross reads
             * @param in InputStream is generally the S3InputStream
             */
            public CountingStream(ByteArrayOutputStream out, InputStream in) {
                this.out = out;
                this.delegate = in;
            }
    
            /**
             * Gets the String representing the line of bytes.
             * 
             * @return
             * @throws UnsupportedEncodingException
             */
            public String getLine() throws UnsupportedEncodingException {
                String str = new String(out.toByteArray(), "UTF-8");
                if (str.isEmpty()) {
                    str = null;
                }
                return str;
            }
    
            @Override
            public int read() throws IOException {
                return delegate.read();
            }
    
            /**
             * drains the stream so we don't leave a hanging connection
             * @throws IOException
             */
            public void drain() throws IOException {
                int l = delegate.read();
                while (l != -1) {
                    l = delegate.read();
                }
            }
    
            /**
             * 
             * @param counter
             * @return
             * @throws IOException
             */
            public String readLine() throws IOException {
                int r = read();
                if (r == -1) {
                    return null;
                } else {
                    String result = null;
                    while (r != -1) {
                        byte b = (byte) r;
                        if (LF == (int) b) {
                            length++;
                            r = -1;
                            result = getLine();
                            out.reset();
                        } else {
                            length++;
                            if (length == MAX_LENGTH_PER_LINE) {
                                throw new IOException("Current Line in NDJSON exceeds limit " + MAX_LENGTH_PER_LINE);
                            }
                            out.write(b);
                            r = read();
                        }
                    }
                    return result;
                }
            }
        }
    

    Importantly, the code defers the caching to the EXTERNAL caller, and in this case assembles a window of resources:

        protected void readFromObjectStoreWithLowMaxRange(AmazonS3 c, String b, String workItem) throws FHIRException {
    
            // Don't add tempResources to resources until we're done (we do retry), it's a temporary cache of the Resources
            List<Resource> tempResources = new ArrayList<>();
    
            // number of bytes read.
            long numberOfBytesRead = 0l;
            int totalReads = 0;
            int mux = 0;
    
            // The cached FHIRParserException
            FHIRParserException fpeDownstream = null;
    
            // Closed when the Scope is out. The size is double the read window.
            // The backing array is allocated at creation.
            ByteArrayOutputStream cacheOut = new ByteArrayOutputStream(512000);
            boolean complete = false;
            while (!complete) {
                // Condition: At the end of the file... and it should never be more than the file Size
                // however, in rare circumstances the person may have 'grown' or added to the file
                // while operating on the $import and we want to defensively end rather than an exact match
                // Early exit from the loop...
                long start = this.transientUserData.getCurrentBytes();
                if (this.transientUserData.getImportFileSize() <= start) {
                    complete = true; // NOP
                    break;
                }
    
                // Condition: Window would exceed the maximum File Size
                // Prune the end to -1 off the maximum.
                // The following is 256K window. 256K is used so we only drain a portion of the inputstream.
                // and not the whole file's input stream.
                long end = start + 256000;
                if (end >= this.transientUserData.getImportFileSize()) {
                    end = this.transientUserData.getImportFileSize() - 1;
                    complete = true; // We still need to process the bytes.
                }
    
                // Request the start and end of the S3ObjectInputStream that's going to be retrieved
                GetObjectRequest req = new GetObjectRequest(b, workItem)
                                                .withRange(start, end);
    
                if (LOG.isLoggable(Level.FINE)) {
                    // Useful when debugging edge of the stream problems
                    LOG.fine("S3ObjectInputStream --- " + start + " " + end);
                }
    
                boolean parsedWithIssue = false;
                try (S3Object obj = c.getObject(req);
                        S3ObjectInputStream in = obj.getObjectContent();
                        BufferedInputStream buffer = new BufferedInputStream(in);
                        CountingStream reader = new CountingStream(cacheOut, in)) {
    
                    // The interior block allows a drain operation to be executed finally.
                    // as a best practice we want to drain the remainder of the input
                    // this drain should be at worst 255K (-1 for new line character)
                    try {
                        String resourceStr = reader.readLine();
                        // The first line is a large resource
                        if (resourceStr == null) {
                            this.transientUserData.setCurrentBytes(this.transientUserData.getCurrentBytes() + reader.length);
                            reader.length = 0;
                            mux++;
                        }
    
                        while (resourceStr != null && totalReads < maxRead) {
                            try (StringReader stringReader = new StringReader(resourceStr)) {
                                tempResources.add(FHIRParser.parser(Format.JSON).parse(stringReader));
                            } catch (FHIRParserException fpe) {
                                // Log and skip the invalid FHIR resource.
                                parseFailures++;
                                parsedWithIssue = true;
                                fpeDownstream = fpe;
                            }
    
                            long priorLineLength = reader.length;
                            reader.length = 0;
                            resourceStr = reader.readLine();
    
                            if (!parsedWithIssue) {
                                this.transientUserData.setCurrentBytes(this.transientUserData.getCurrentBytes() + priorLineLength);
                                numberOfBytesRead += reader.length;
                                totalReads++;
                            } else if ((parsedWithIssue && resourceStr != null)
                                    || (parsedWithIssue && 
                                            (this.transientUserData.getImportFileSize() <= this.transientUserData.getCurrentBytes() + priorLineLength))) { 
                                // This is potentially end of bad line
                                // -or-
                                // This is the last line failing to parse
                                long line = this.transientUserData.getNumOfProcessedResources() + totalReads;
                                LOG.log(Level.SEVERE, "readResources: Failed to parse line " + totalReads + " of [" + workItem + "].", fpeDownstream);
                                String msg = "readResources: " + "Failed to parse line " + line + " of [" + workItem + "].";
    
                                ConfigurationAdapter adapter = ConfigurationFactory.getInstance();
                                String out = adapter.getOperationOutcomeProvider(source);
                                boolean collectImportOperationOutcomes = adapter.shouldStorageProviderCollectOperationOutcomes(source)
                                        && !StorageType.HTTPS.equals(adapter.getStorageProviderStorageType(out));
                                if (collectImportOperationOutcomes) {
                                    FHIRGenerator.generator(Format.JSON)
                                        .generate(generateException(line, msg),
                                                transientUserData.getBufferStreamForImportError());
                                    transientUserData.getBufferStreamForImportError().write(NDJSON_LINESEPERATOR);
                                }
                            }
                        }
                    } catch (Exception ex) {
                        LOG.warning("readFhirResourceFromObjectStore: Error proccesing file [" + workItem + "] - " + ex.getMessage());
                        // Throw exception to fail the job, the job can be continued from the current checkpoint after the
                        // problem is solved.
                        throw new FHIRException("Unable to read from S3 during processing", ex);
                    } finally {
                        try {
                            reader.drain();
                        } catch (Exception s3e) {
                            LOG.fine(() -> "Error while draining the stream, this is benign");
                            LOG.throwing("S3Provider", "readFromObjectStoreWithLowMaxRange", s3e);
                        }
                    }
    
                    // Increment if the last line fails
                    if (this.transientUserData.getImportFileSize() <= this.transientUserData.getCurrentBytes()) {
                        parseFailures++;
                    }
                } catch (FHIRException fe) {
                    throw fe;
                } catch (Exception e) {
                    throw new FHIRException("Unable to read from S3 File", e);
                }
    
                // Condition: The optimized block and the number of Resources read
                // exceed the minimum thresholds or the maximum size of a single resource
                if (tempResources.size() >= maxRead) {
                    LOG.fine("TempResourceSize " + tempResources.size());
                    complete = true;
                }
    
                // Condition: The optimized block is exceeded and the number of resources is
                // only one so we want to threshold a maximum number of resources
                // 512K * 5 segments (we don't want to repeat too much work) = 2.6M
                if (numberOfBytesRead > 2621440 && tempResources.size() >= 1) {
                    complete = true;
                }
    
                // Condition: The maximum read block is exceeded and we have at least one Resource
                // 2147483648 / (256*1024*1024) = 8192 Reads
                if (mux == 8193) {
                    throw new FHIRException("Too Long a Line");
                }
    
                // We've read more than one window
                if (mux > 1 && tempResources.size() >=1) {
                    break;
                }
            }
    
            // Condition: There is no complete resource to read.
            if (totalReads == 0) {
                LOG.warning("File grew since the start");
                this.transientUserData.setCurrentBytes(this.transientUserData.getImportFileSize());
            }
    
            // Add the accumulated resources
            this.resources.addAll(tempResources);
        }
    

    The above code was created and licensed as part of the IBM/FHIR project.

    Net, carefully approach Unicode formats, becareful on reassembling bytes and reading windows from Channels.

  • Moving on…

    In 2019, I joined the IBM FHIR Server team. A team tasked with engineering an internal FHIR server (DSTU2) as an updated and upgrade open source HL7 FHIR R4 Server. The open sourced code, on GitHub IBM® FHIR® Server – IBM/FHIR is a product of many contributors since it’s inception in 2016 (the project history goes back to the DSTU2 days). I contributed over a 1000 commits over my time working on the project, authored over 300 issues, opened-updated-closed 600 plus Pull Requests, and triaged/reviewed and designed many more.

    Today I’m moving on to IBM Power Systems and working on OpenShift.

    Contributions to the Project – Automation, Search, Hard Erase, Performance, Data Storage, Bulk Data

  • GitHub Actions Braindump

    The following are from a braindump I did for my teamn (everything here is public knowledge):

    Getting Setup to Building and Developing with the Workflows

    This section outlines setting up your development environment for working with workflows:

    1. Download the Visual Code.  This tool is best to sit outside of your environment.
    2. Click Extensions > Search for PowerShell and install the PowerShell. This feature will also install PowerShell local to your system.  PowerShell is used in the Windows workflow.
    3. Install ShellCheck. This feature is used to check your code and make sure you are following best practices when generating the shell scripts.
    4. Add an alias to your terminal settings:

    alias code=’/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/code’

    For me, this is in the .zshrc file.

    You should also have Docker installed (or aliased nerdctl aliased to docker). You should also have IBM Java 11 installed (v11.0.14.1).

    You’ll also need access to:

    Debugging GitHub Actions

    If the GitHub Action fails, you should check the following:

    1. Review the PR Workflow
    1. Click on Details
      1. Select the Job that is failing
    • Click on the Settings > View raw logs
    • Navigate down to the end of the file to the BUILD FAILURE
      • Scroll up from the BUILD FAILURE point (at least in Maven)
      • Look for the failing tests
    • If there are not enough details, go back to Step (b)
      • Click on Summary
    • Scroll down to Artifacts
      • Download the Artifacts related to the build. These files are kept for 90 days or until we hit a storage limit. Note, we purposefully use the upload-artifacts step.
    1. Extract the files and you should have the Docker Console log file, test results any other workflow logging.
    2. IBM/FHIR Repository Actions – You can filter through list of Actions.
      1. Navigate to https://github.com/IBM/FHIR/actions?query=is%3Afailure
      1. You can filter on the failing workflows and get a good big picture.
    3. GitHub Status – You should subscribe to the site’s updates (via email).  This is going to be very helpful to figure out what’s going on with GitHub.  You should also check this when the transient errors appear systemic or non-deterministic – e.g. not failing in the same spot. At least one person on the team should sign up for the GitHub Status.
    4. GitHub Community: Actions – I rarely go here, but sometimes I find that someone has posted with the same question, it may have an answer.  Very rarely, I post directly there.

    Debugging

    If you encounter anything that looks transient – e.g., network download (Wagon Failure), disk space, filesystem failure – you can choose to rerun the failing workflow.

    1. Navigate to the failing Job-Workflow
    2. Click Re-run all jobs
    3. Repeat for all Workflows that failed

    If you see a failure in a particular GitHub Step, such as actions/checkout or actions/setup-java, you should go search that actions issues:

    1. If actions/setup-java is failing, navigate to https://github.com/actions/setup-java
    2. Check the Issues (Search for the issue)
    3. Search for settings that may help

    Note, sometimes they intentionally fail a GitHub Action workflow to signal that you should upgrade or change.

    How to do Local Development with GitHub Actions

    1. Navigate to https://github.com/ibm/fhir
    2. Click Fork
      1. If a Fork already exists, be sure to Fetch Upstream > Fetch and Merge
    • Click Pull Requests and Create the ci-skip label
      • Click Labels
      • Click New Label
      • Click Create label
    • Clone the fork – git clone https://github.com/prb112/FHIR.git
      • I typically create a local folder called prb112 then clone into it.
    • Once the main branch is active, git checkout -b new-ci
    • Open your code using your alias: code ~/dev/fhir
    • You’ll see:
    • Edit your automation files in .github and build
    • Click Terminal > New Terminal
    1. Update the .github/workflows/<file> you are changing so the job.<workflow_job>.if condition is invalid:

    jobs:

      e2e-audit:

        runs-on: ubuntu-latest

        if: “!contains(github.event.pull_request.labels.*.name, ‘ci-skip’)”

    I change ci-skip to ci-skip-ignore so that I can run just that one targeted workflow.

    1. Test the steps locally by executing the steps in the workflow line-by-line in the terminal session.
    2. Once you are comfortable with the changes:
      1. git add <files>
      1. git commit -s -m “My edits for issue #2102010”
    3. Push your new branch – git push -u origin new-ci
    4. Create your new Pull Request targeting the IBM:fhir main branch and add ci-skip.

    The targeted workflow you are building with is the only one that runs. Note, you have a LIMITED number of execution minutes for GitHub Workflows.

    Finding GitHub Actions to use/tips in Use

    There are many folks using GitHub Actions, and many have figured out better patterns (or evolved to have better patterns).

    1. Go here – https://github.com/actions/
    2. Search: <my query> file:yml site:github.com
    Workflow Details

    Each workflow runs in a hosted-runner (or virtual-environment).  These hosted-runners are containers specified declaratively in the workflow:

    FlavorVirtual-Environment
    windowswindows-2019
    All OtherUbuntu2004

    These hosted-runners have a number of pre-installed libraries and tools – Docker, podman, java-11, jq, perl, python, yarn et cetra.

    These workflows (except the site, javadocs and release) follow a similar pattern:

    1. Setup Prerequisites
    2. Run the Pre-integration Steps
    3. Execute the Tests
    4. Run the Post Integration Steps
    5. Archive the Results

    This pattern evolved from build.yml and integration.yml as the starting point all the way to the most recent migration.yaml. Migration.yml is the most sophisticated workflow-jobs that are created.

  • Using the IBM FHIR Server and Implementation Guide as Java Modules

    The IBM FHIR Server is an extensible HL7 FHIR Server. The IBM FHIR server supports complicated ImplementationGuides (IGs), a set of rules of how a particular problem is solved using FHIR Resources. The implementation guides include a set of Profiles, ValueSets, CodeSystems and supporting resources (Examples, CapabilityStatements).

    The IBM FHIR Server supports the loading of NPM packages – stored in the (package.tgz). You see the package at the https://www.hl7.org/fhir/us/core/package.tgz (One just appends package.tgz to any IG site).

    The IBM FHIR Server includes a number of IGs built-tested-released with each tag.

    The IGs are Java modules which are specially formed to support the resources in the NPM packages. The Java modules use a ServiceLoader (activated at startup when the Java Module is in the classpath).

    The best way to start is to copy and existing fhir-ig, such as fhir-ig-us-core, and modify as needed (package rename and updated files).

    The service loader uses the META-INF/services/com.ibm.fhir.registry.spi.FHIRRegistryResourceProvider interface to activate the list of classes in the file.

    Each of the corresponding classes need to be in src/main/java under the corresponding package (com.ibm.fhir.ig.us.core as above).

    The PackageRegistryResourceProvider navigates the src/main/resources to find the folder hl7/fhir/us/core/311 and loads the files referenced in the NPM packages index (.index.json).

    You might not see the .index.json file by default in Eclipse, and you should unhide the .resource file at the Enterprise Explorer View > Filters and Customization, Select .*resources, Click OK

    When you open .index.json, you should see:

    These are the resources which will be loaded when the fhir-ig-us-core is added to the userlib folder.

    The US-Core and CarinBB support multiple versions of the same IG – v3.1.1 and v4.0.0 in the same Java module. To control this behavior, one needs to set the configuration in order to map to a default version (the default is always the latest or newest version).  With cross IG dependencies, they are updated to point to the correct one, or the latest one as the IG specifies.

    To make these files viewable, we do like to format the contents of these folders so they are pretty JSON. When the IGs are built and released, the JSON files are compressed and saves a good chunk of Memory. 

    We also like to remove Narrative texts:

    "text": {
         "status": "empty",
         "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Redacted for size</div>"
    },

    There is an examples folder included with most package.tgz files.  You should copy this into the src/test/resources/JSON folder and update the index.txt so you have the latest examples in place, and these will be loaded the ProfileTest (ConformaceTest is used for usecase specific tests).  The below is an example of loading the 400/index.txt and failing when issues exceed a limit or a severity of error.

    public class ProfileTest {
    
        private static final String INDEX = "./src/test/resources/JSON/400/index.txt";
    
        private String path = null;
    
        public ProfileTest() {
            // No Operation
        }
    
        public ProfileTest(String path) {
            this.path = path;
        }
    
        @Test
        public void testUSCoreValidation() throws Exception {
            try (Reader r = Files.newBufferedReader(Paths.get(path))) {
                Resource resource = FHIRParser.parser(Format.JSON).parse(r);
                List<Issue> issues = FHIRValidator.validator().validate(resource);
                issues.forEach(item -> {
                    if (item.getSeverity().getValue().equals("error")) {
                        System.out.println(path + " " + item);
                    }
                });
                assertEquals(countErrors(issues), 0);
            } catch (Exception e) {
                System.out.println("Exception with " + path);
                fail(e.toString());
            }
        }
    
        @Factory
        public Object[] createInstances() {
            List<Object> result = new ArrayList<>();
    
            try (BufferedReader br = Files.newBufferedReader(Paths.get(INDEX))) {
                String line;
                while ((line = br.readLine()) != null) {
                    result.add(new ProfileTest(line));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return result.toArray();
        }
    
    }

    The ProviderTest checks the provider loads successfully, and that the number of resources returned is correct, and tests one exemplary resource is returned.

    @Test
        public void testRegistry() {
            StructureDefinition definition = FHIRRegistry.getInstance()
                    .getResource("http://hl7.org/fhir/us/core/StructureDefinition/pediatric-bmi-for-age", StructureDefinition.class);
            assertNotNull(definition);
            assertEquals(definition.getVersion().getValue(), "4.0.0");
        }
    
        @Test
        public void testUSCoreResourceProvider() {
            FHIRRegistryResourceProvider provider = new USCore400ResourceProvider();
            assertEquals(provider.getRegistryResources().size(), 148);
        }

    There is a very important test which is testConstraintGenerator any issue in the structure definition will be reported when the Constraints are compiled, and you’ll get some really good warnings.

        @Test
        public static void testConstraintGenerator() throws Exception {
            FHIRRegistryResourceProvider provider = new USCore400ResourceProvider();
            for (FHIRRegistryResource registryResource : provider.getRegistryResources()) {
                if (StructureDefinition.class.equals(registryResource.getResourceType())) {
                    assertEquals(registryResource.getVersion().toString(), "4.0.0");
                    String url = registryResource.getUrl() + "|4.0.0";
                    System.out.println(url);
                    Class<?> type = ModelSupport.isResourceType(registryResource.getType()) ? ModelSupport.getResourceType(registryResource.getType()) : Extension.class;
                    for (Constraint constraint : ProfileSupport.getConstraints(url, type)) {
                        System.out.println("    " + constraint);
                        if (!Constraint.LOCATION_BASE.equals(constraint.location())) {
                            compile(constraint.location());
                        }
                        compile(constraint.expression());
                    }
                }
            }
        }

    For example, you might get a pediatric BMI set of constraints:

    http://hl7.org/fhir/us/core/StructureDefinition/pediatric-bmi-for-age|4.0.0
        @com.ibm.fhir.model.annotation.Constraint(id="vs-1", level="Rule", location="Observation.effective", description="if Observation.effective[x] is dateTime and has a value then that value shall be precise to the day", expression="($this as dateTime).toString().length() >= 8", source="http://hl7.org/fhir/StructureDefinition/vitalsigns", modelChecked=false, generated=false, validatorClass=interface com.ibm.fhir.model.annotation.Constraint$FHIRPathConstraintValidator)
        @com.ibm.fhir.model.annotation.Constraint(id="vs-2", level="Rule", location="(base)", description="If there is no component or hasMember element then either a value[x] or a data absent reason must be present.", expression="(component.empty() and hasMember.empty()) implies (dataAbsentReason.exists() or value.exists())", source="http://hl7.org/fhir/StructureDefinition/vitalsigns", modelChecked=false, generated=false, validatorClass=interface com.ibm.fhir.model.annotation.Constraint$FHIRPathConstraintValidator)

    The above constraints can be tested using the FHIRPath expression against a failing test resource, and confirm the validity of the StructureDefinition.

    I hope this document help you build your own IGs for the IBM FHIR Server.