Search code examples
kubernetescurlescapingkubernetes-cronjobcommand-substitution

Kubernetes CronJob - Escaping a cURL command with nested JSON and command substitution


I have the following Kubernets CronJob definition:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: myCronJob
spec:
  schedule: "*/1 * * * *"
  failedJobsHistoryLimit: 1
  successfulJobsHistoryLimit: 1
  jobTemplate:
    spec:
      backoffLimit: 2
      parallelism: 1
      template:
        metadata:
          annotations:
            linkerd.io/inject: disabled
        spec:
          containers:
            - name: myCronJob
              image: curlimages/curl:7.72.0
              env:
                - name: CURRENT_TIME
                  value: $(date +"%Y-%m-%dT%H:%M:%S.%sZ")
              args:
                - /bin/sh
                - -ec
                - "curl --request POST --header 'Content-type: application/json' -d '{\"Label\":\"myServiceBusLabel\",\"Data\":\"{\'TimeStamp\':\'$(echo $CURRENT_TIME)\'}\",\"QueueName\":\"myServiceBusQueue\"}' https://mycronjobproxy.net/api/HttpTrigger?code=mySecretCode"
          restartPolicy: OnFailure

Notice I pass a dynamic date to curl via an environment variable, as described here.

However, this produces an error at runtime (copied from K9s):

curl: (3) unmatched close brace/bracket in URL position 26:                                                                                                                    
+"%Y-%m-%dT%H:%M:%S.%sZ")}","QueueName":"myServiceBusQueue"}
                         ^

I suspect this is likely an issue with combining double- and single quotes and escape characters. The curl command runs fine locally on macOS, but not when deployed using the curlimages/curl:7.72.0. There seems to be some difference in behavior.

On macOS, on my local dev machine, I can run the command like so:

curl --request POST --header "Content-type: application/json" -d "{'Label':'myServiceBusLabel','Data':{'TimeStamp':'$(echo $CURRENT_TIME)'},'QueueName':'myServiceBusQueue'}" https://mycronjobproxy.net/api/HttpTrigger?code=mySecretCode

Output:

Message was successfully sent using the following params: Label = myServiceBusLabel | Data = {"TimeStamp": "2023-05-15T15:44:45.1684158285Z"} | QueueName = myServiceBusQueue%

But when I use that version in my K8s CronJob YAML file, my IDE (JetBrains Rider) says: "Scalar value expected." It seems like the whole command must be enclosed in double quotes.

What is the correct way to quote/escape this curl command?


Solution

  • The requirement for sh -c is that whatever command you want it to run must be passed in a single argument. With the list-form args:, you're explicitly specifying the argument list, and you can use any YAML string syntax so long as it is in a single list item.

    The first thing this means is that you don't need the outermost set of double quotes. That can be useful to disambiguate some kinds of structures and values – "true", "17" – but in this case the item is pretty clearly a string and not something else. This removes a layer of quoting.

    This also means alternate YAML syntaxes are available here. Given the sheer length of this line, I might look at using a folded block scalar here: if the list item value is just >- at the end of the line, then the following (indented) lines will be combined together with a single space (>) and there will not be a final newline (-).

    Kubernetes doesn't do command substitution in env:. If you want an environment variable to hold a dynamic value like this, it needs to be embedded or computed in the command in some form. Since you're already using sh -c syntax you need to add that into the command string.

    There is one shell-syntax concern here as well. In your example, the curl -d argument is a single-quoted string curl -d '{...}'. Within that single-quoted string, command substitution and other shell processing doesn't happen. You need to change these single quotes to double quotes, which means you need to escape the double quotes in the JSON body; but if we remove the YAML double quotes as well, it is only single escaping. You also then don't need to quote the single quotes inside this string.

    (While we're here, don't $(echo $VARIABLE), just use $VARIABLE directly.)

    This should all combine to form:

    args:
      - /bin/sh
      - -ec
      - >-
          CURRENT_TIME=$(date +"%Y-%m-%dT%H:%M:%S.%sZ");
          curl
            --request POST
            --header 'Content-type: application/json'
            -d "{\"Label\":\"myServiceBusLabel\",\"Data\":\"{'TimeStamp':'$CURRENT_TIME'}\",\"QueueName\":\"myServiceBusQueue\"}"
            https://mycronjobproxy.net/api/HttpTrigger\?code=mySecretCode
    

    So note that we have two commands, explicitly separated with a semicolon (at the end of the first line). The curl arguments are split out one to a line for readability but with no additional punctuation (no backslashes at the ends of lines). The -d option is double-quoted so the variable expansion happens; the single quotes inside the double-quoted string are not escaped.


    Is the Data field itself intended to be a JSON object serialized as a string? In that case you will need to use double quotes inside of a double-quoted JSON string inside a double-quotes shell argument. The layers of escaping would look like:

    1. Create the innermost JSON-string argument
      {"TimeStamp":"$CURRENT_TIME"}
      
    2. Embed that in a JSON string and serialize it, which means escaping the double quotes
      {"Data":"{\"TimeStamp\":\"$CURRENT_TIME\"}"}
      
    3. Embed that in a shell string, escaping both the double quotes and backslashes
      -d "{\"Data\":\"{\\\"TimeStamp\\\":\\\"$CURRENT_TIME\\\"}\"}"
      

    It might be more straightforward to use a tool like jq to construct or manipulate the JSON, or to put a template string in a ConfigMap and then use sed or envsubst to replace the dynamic value.