Unzip Docker image and contents

If you ever need to see the files inside a docker image, you can save the image locally and then unzip all the contents.

image_tag=repository:tag

docker save ${image_tag} > image.tar
tar xf image.tar
rm image.tar

for f in */; do
  if [ -d "${f}" ]; then
    cd "${f}" ||
        # unzip each of the layers
        find ./ -type f -name "*.tar" -exec tar xf "{}" \;
    cd ../
  fi
done

read more

AWS CloudFront create redirect using CloudFormation

When decommissioning a website, it’s ideal to set up a permanent redirect for the current domain, so users aren’t left in the dark. Below is code to redirect a user from an existing CloudFront distribution to a new URL.

You can use any statusCode, but in this instance a 301 is appropriate because this is a permanent redirect.

# The Distribution should already exist. We just need to add the FunctionAssociations
Resources:
  rDistribution:
    Type: AWS::CloudFront::Distribution # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-distributionconfig.html
    Properties:
      DistributionConfig:
        Enabled: True
        DefaultRootObject: index.html
        DefaultCacheBehavior:
          TargetOriginId: BuscheOrigin
          ViewerProtocolPolicy: redirect-to-https
        ### new code ###
        FunctionAssociations:
          - EventType: viewer-request
            FunctionARN: !GetAtt BuscheRedirectFunction.FunctionMetadata.FunctionARN #name needs to match redirect function
        ### end new code ###
        HttpVersion: http2

### new function ###
BuscheRedirectFunction: # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudfront-function.html
    Type: AWS::CloudFront::Function
    Properties:
      Name: "busche-redirect"
      AutoPublish: true
      FunctionCode: |
        function handler(event) {
          return {
            statusCode: 301,
            statusDescription: 'Found',
            headers: {
              'cloudfront-functions': { value: 'generated-by-CloudFront-Functions' },
              'location': { value: 'https://mrbusche.com' }
            }
          };
        }
      FunctionConfig:
        Comment: rewrite requests from busche to mrbusche.com
        Runtime: cloudfront-js-1.0
### end new function ###

read more

Concourse push commit to pull request branch

We run prettier against our repository, but rather than fail a pull-request check if users don’t run it locally, we wanted to update the branch, push a commit, and run the other checks. Here’s what we were able to get working

resource_types:
  - name: pull-request
    type: registry-image
    source:
      repository: teliaoss/github-pr-resource

resources:
  - name: pull-request
    type: pull-request
    icon: source-pull
    check_every: 8760h
    webhook_token: token
    public: true
    source:
      repository: github.com/mrbusche/your-repository
      access_token: ((git-access-token))
      v3_endpoint: https://github.com/api/v3/
      v4_endpoint: https://github.com/api/graphql

jobs:
  - name: test-pull-request
    build_log_retention:
      builds: 30
      days: 30
      minimum_succeeded_builds: 10
    public: true
    plan:
    - get: pull-request
      trigger: true
      version: every
    - put: pull-request
      params:
        path: pull-request
        context: UI Tests
        status: pending
    - task: prettier
      config:
        platform: linux
        image_resource:
          type: docker-image
          source:
            repository: image-with-node-git-and-jq
        inputs:
        - name: pull-request
        # this makes pull-request available to subsequent tasks
        outputs:
          - name: pull-request
        params:
          USERNAME: ((git-username))
          ACCESS_TOKEN: ((git-access-token))
        run:
          path: sh
          dir: pull-request
          args:
          - -exc
          - |

            prNumber=$(cat .git/resource/metadata.json | jq -r '.[] | select(.name=="pr") | .value')
            branchName=$(curl 'https://'${USERNAME}:${ACCESS_TOKEN}'@github.com/api/v3/repos/mrbusche/your-repository/pulls/'${prNumber} | jq -r '.head.ref')

            git fetch
            git checkout ${branchName}

            # find last committer email and username, concourse-ci by default
            commitUserId=$(curl 'https://'${USERNAME}:${ACCESS_TOKEN}'@github.com/api/v3/repos/mrbusche/your-repository/pulls/'${prNumber} | jq -r '.user.login')
            git config user.email ${commitUserId}
            git config user.name "$(git log -1 --pretty=format:'%an')"

            # run some command that modifies files
            npm install -g prettier
            npm run prettier:fix

            git add .

            # if you try to commit and push without changes the task fails
            if [[ ! -z "$(git status --porcelain)" ]]; then
              git commit -m "prettier fix"

              git push --set-upstream origin ${branchName}
            else
              echo "no changes found"
            fi

    - task: test-ui
      config:
        platform: linux
        image_resource:
        type: registry-image
        source:
          repository: timbru31/node-chrome
          tag: 14-slim
        inputs:
        - name: pull-request
        run:
        path: /bin/sh
        args:
          - -exc
          - |
          npm config set cache $(pwd)/.npm --global
          cd pull-request
          export NG_CLI_ANALYTICS=false
          CYPRESS_INSTALL_BINARY=0 npm ci --quiet
          npm run test:headless
        caches:
        - path: .npm
        - path: pull-request/node_modules
      on_success:
        put: pull-request
        params:
        path: pull-request
        context: UI Tests
        status: success
      on_failure:
        put: pull-request
        params:
        path: pull-request
        context: UI Tests
        status: failure
    - put: pull-request
      params:
        path: pull-request
        status: success

read more

Setting up an SNS trigger and processing the request in AWS Lambda

Setting up an SNS topic is pretty well documented, but I struggled with how to take action when the event is triggered.

Turns out it’s pretty straightforward. The event['detail-type'] will be Scheduled Event.

myFunction:
  Type: AWS::Serverless::Function
  Properties:
    Handler: app.handler
    Events:
      SNSTopicTrigger:
        Type: SNS
        Properties:
          Topic: !Ref rSNSTopic

rSNSTopic:
  Type: AWS::SNS::Topic
  Properties:
    DisplayName: SNSTrigger

anotherFunction:
    Properties:
      Policies:
        - AWSLambdaExecute
        - Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
                - sns:Publish
              Resource: !Ref rSNSTopic

SNS calling function

const message = {
  default: JSON.stringify({
    name: 'John Doe',
    timestamp: new Date(),
  }),
};
var params = {
  TopicArn: YOUR_SNS_TOPIC,
  Subject: 'An Important Event',
  MessageStructure: 'json',
  Message: JSON.stringify(message),
};
await sns.publish(params).promise();

SNS processing function

if (event.Records && event.Records[0].EventSource === 'aws:sns') {
  const message = JSON.parse(event.Records[0].Sns.Message);
  console.log(['SNS event', message]);
  doSomething(message);
}

read more

Setting up a cron job and processing the request in AWS Lambda

Setting up a cron job in AWS lambda is pretty trivial, but I struggled with how to take action when the event is triggered. Turns out it’s pretty straightforward. The event['detail-type'] will be Scheduled Event.

myFunction:
  Type: AWS::Serverless::Function
  Properties:
    Handler: app.handler
    Events:
      myCron:
        Type: Schedule
        Properties:
          Schedule: "cron(? 10 * * Mon *)"
if (event['detail-type'] === 'Scheduled Event') {
  console.log('cron triggered');
  return await somethingThatWasScheduled(event);
}

read more

Generating a Cybersource Flex Key using ColdFusion

Cybersource is a payment provider with poor examples and poor documentation. I struggled through this for about 25 hours before I found the right combination of settings.

First you need to include the cybersource-rest-client-java and AuthenticationSDK jars on your classpath. You can do this by adding the following to your Application.cfc or by dropping them into the \cfusion\lib folder

this.javaSettings = {LoadPaths = [".\libs\cybersource-rest-client-java-0.0.35.jar", ".\libs\AuthenticationSdk-0.0.17.jar"], loadColdFusionClassPath = true, reloadOnChange = false};

I’ve created a struct to house the default Cybersource credentials as the sample environment. You’ll want to replace test and production with your credentials, ideally pulling them from secrets and not hardcoded and placed into source control. The second parameter is your target origin, which varies by environment and potentially by user workstation as well.

<cfscript>
writeDump(retrieveFlexKey('sample', 'http://localhost:8500'));

public String function retrieveFlexKey(String environment, String targetOrigin) throws Exception {

  var flexPublicKey = "NoKeyReturned";

  var environmentDetails = {
    sample: {
      merchantID: "testrest",
      runEnvironment: "apitest.cybersource.com",
      merchantKeyId: "08c94330-f618-42a3-b09d-e1e43be5efda",
      merchantsecretKey: "yBJxy6LjM2TmcPGu+GaJrHtkke25fPpUX+UY6/L/1tE="
    },
    test: {
      merchantID: "testrest",
      runEnvironment: "apitest.cybersource.com",
      merchantKeyId: "08c94330-f618-42a3-b09d-e1e43be5efda",
      merchantsecretKey: "yBJxy6LjM2TmcPGu+GaJrHtkke25fPpUX+UY6/L/1tE="
    },
    production: {
      merchantID: "testrest",
      runEnvironment: "apitest.cybersource.com",
      merchantKeyId: "08c94330-f618-42a3-b09d-e1e43be5efda",
      merchantsecretKey: "yBJxy6LjM2TmcPGu+GaJrHtkke25fPpUX+UY6/L/1tE="
    }
  };

  try {
    var details = structKeyExists(environmentDetails, environment) ? environmentDetails[environment] : environmentDetails['sample'];
    props = createObject('java', 'java.util.Properties');
    props.setProperty("authenticationType", "http_signature");
    props.setProperty("merchantID", details['merchantID']);
    props.setProperty("runEnvironment", details['runEnvironment']);
    props.setProperty("merchantKeyId", details['merchantKeyId']);
    props.setProperty("merchantsecretKey", details['merchantsecretKey']);

    requestInfo = createObject('java', 'Model.GeneratePublicKeyRequest');
    requestInfo.encryptionType("RsaOaep256");
    requestInfo.targetOrigin(arguments.targetOrigin);

    merchantConfig = createObject('java', 'com.cybersource.authsdk.core.MerchantConfig').init(props);
    apiClient = createObject('java', 'Invokers.ApiClient');
    apiClient.merchantConfig = merchantConfig;

    keyGenerationApi = createObject('java', 'Api.KeyGenerationApi').init(apiClient);
    response = keyGenerationApi.generatePublicKey("JWT", requestInfo);

    responseCode = apiClient.responseCode;
    status = apiClient.status;
    writeDump(responseCode);
    writedump(status);
    if (responseCode == '200' && status == 'OK') {
      return response.getKeyId();
    }
  } catch (Exception e) {
    writeDump(e);
    // you'll want to login any errors somewhere
  }

  return flexPublicKey;
}
</cfscript>

read more

Cypress - Failed to deserialize the snapshot blob

Trying to run npx cypress open and I received an error of

It looks like this is your first time using Cypress: 8.2.0

Cypress failed to start.

# Fatal error in , line 0
# Failed to deserialize the V8 snapshot blob. This can mean that the snapshot blob file is corrupted or missing.

Deleting node_modules and running npm install had no effect, but I was able to get it working again running npx cypress install --force.

If that doesn’t work you can also rename the Cypress cache folder. On windows it’s \AppData\Local\Cypress\Cache\<version>

read more

Converting a Roth IRA to a Traditional IRA at Vanguard

I mistakenly put money into my Traditional IRA at Vanguard this year instead of my Roth IRA. I couldn’t for the life me find directions on how to change this, so I called Vanguard. It’s actually very simple.

  1. Login to your account
  2. Click on FORMS in the header
  3. Click Add or remove money, trade within your account
  4. Click Remove excess distributions or contributions, convert from a traditional to a Roth IRA, or recharacterize contributions Click Remove excess contributions, convert assets, or recharacterize contributions
  5. Fill out the rest of form

Disclaimer: I’m not a tax professional, just a dude who messed up his contributions.

read more

Concourse build angular app on pull request with cache

resource_types:
  - name: pull-request
    type: registry-image
    source:
      repository: teliaoss/github-pr-resource

resources:
  - name: pull-request
    type: pull-request
    icon: source-pull
    check_every: 8760h
    webhook_token: ((webhook-token))
    public: true
    source:
      repository: ((your-repository))
      access_token: ((access-token.git-access-token))

jobs:
  - name: test-pull-request
    plan:
      - get: pull-request
        trigger: true
        version: every
      - put: pull-request
        params:
          path: pull-request
          status: pending
      - task: unit-test
        config:
          platform: linux
          image_resource:
            type: registry-image
            source:
              repository: node
              tag: alpine # you can use any node image, alpine is the smallest
          inputs:
            - name: pull-request
          run:
            path: /bin/sh
            args:
              - -exc
              - |
                npm config set cache $(pwd)/.npm --global
                cd pull-request
                export NG_CLI_ANALYTICS=false
                # execute whatever commands you need here
                npm install --quiet
                npm run build:test
          caches:
            - path: .npm
            - path: pull-request/node_modules
        on_failure:
          put: pull-request
          params:
            path: pull-request
            status: failure
      - put: pull-request
        params:
          path: pull-request
          status: success

Github webhook URL - https://${concourse-url}/api/v1/teams/${concourse-team-name}/pipelines/{pipeline-name}/resources/pull-request/check/webhook?webhook_token=((webhook-token)) ((webhook-token)) can be anything you want it to be as long as it’s used consistently in your webhook URL and in your pipeline

read more

Concourse rename job and retain history

To rename a Concourse job and retain history, you can use the old_name attribute.

jobs:
  - name: build-8-jdk-centos
    old_name: 8-jdk-centos

Once you’ve fly’d the pipeline with the new and old_name attributes you can remove old_name and fly it again, it’s no longer needed.

A good reason to rename a job would be because of recent concourse deprecations with valid identifiers. Our existing job started with a number, which will stop being allowed in a future Concourse version.

DEPRECATION WARNING:

jobs.8-jdk-centos: '8-jdk-centos' is not a valid identifier: must start with a lowercase letter

read more