Uploaded image for project: 'Jenkins'
  1. Jenkins
  2. JENKINS-40734

Shell step cannot use environment variables that contain $$

    Details

    • Type: Bug
    • Status: Resolved (View Workflow)
    • Priority: Minor
    • Resolution: Fixed
    • Component/s: durable-task-plugin
    • Environment:
    • Similar Issues:

      Description

      When I run a Jenkinsfile that has a sh step that uses an environment variable (such as a password) that has two $$ in a row, they get replaced with one $.

      Here's the steps to reproduce:
      1. Make a global credential id "foo", username "foo", password "bar$$baz"
      2. Use this Jenkinsfile:

      node('linux') {
          withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'foo', passwordVariable: 'PASSWORD', usernameVariable: 'USERNAME']]) {            
              echo "Username: ${env.USERNAME}"
              echo "Password: ${env.PASSWORD}"
              sh 'echo Username: $USERNAME, Password: $PASSWORD'
          }
      }
      

      When the build runs, the echo steps properly echo the user/pass which are then masked. But the shell step doesn't mask the password, which is incorrect. It has lost a $

      [Pipeline] echo
      Username: ****
      [Pipeline] echo
      Password: ****
      [Pipeline] sh
      [s_example] Running shell script
      + echo Username: ****, Password: bar$baz
      Username: ****, Password: bar$baz
      

        Attachments

          Issue Links

            Activity

            Hide
            jglick Jesse Glick added a comment -

            Your shell script is wrong. Try for example

            sh '''
            echo Username: '$USERNAME', Password: '$PASSWORD'
            '''
            
            Show
            jglick Jesse Glick added a comment - Your shell script is wrong. Try for example sh ''' echo Username: '$USERNAME' , Password: '$PASSWORD' '''
            Hide
            b_dean Ben Dean added a comment -

            Jesse Glick, the shell script you suggested doesn't work either. Putting single quotes around the environment variables will cause bash (or sh, or dash, or whatever the shell is) to not evaluate them and the output of the build is:

            + echo Username: $USERNAME, Password: $PASSWORD
            Username: $USERNAME, Password: $PASSWORD
            

            If I change those to double quotes it will evaluate them, but it will have the same problem it had before

            sh '''
            echo Username: "$USERNAME", Password: "$PASSWORD"
            '''
            

            outputs:

            + echo Username: ****, Password: bar$baz
            Username: ****, Password: bar$baz
            

            And don't think to much about the echo command in the shell either. In the real place where I'm running into this problem, I'm passing the username and password to some CLI:

            sh '''
            some-cli do stuff "$USERNAME" "$PASSWORD"
            '''
            

            and they have to be double quoted there so bash will correctly pass the args to the command line if they contain spaces or other characters that have to be quoted.

            Show
            b_dean Ben Dean added a comment - Jesse Glick , the shell script you suggested doesn't work either. Putting single quotes around the environment variables will cause bash (or sh, or dash, or whatever the shell is) to not evaluate them and the output of the build is: + echo Username: $USERNAME, Password: $PASSWORD Username: $USERNAME, Password: $PASSWORD If I change those to double quotes it will evaluate them, but it will have the same problem it had before sh ''' echo Username: "$USERNAME", Password: "$PASSWORD" ''' outputs: + echo Username: ****, Password: bar$baz Username: ****, Password: bar$baz And don't think to much about the echo command in the shell either. In the real place where I'm running into this problem, I'm passing the username and password to some CLI: sh ''' some-cli do stuff "$USERNAME" "$PASSWORD" ''' and they have to be double quoted there so bash will correctly pass the args to the command line if they contain spaces or other characters that have to be quoted.
            Hide
            jglick Jesse Glick added a comment -

            Yes the ' was wrong, I meant ".

            Reproduced and tracked down to this code. Not specific to credentials.

            Show
            jglick Jesse Glick added a comment - Yes the ' was wrong, I meant " . Reproduced and tracked down to this code . Not specific to credentials.
            Hide
            scm_issue_link SCM/JIRA link daemon added a comment -

            Code changed in jenkins
            User: Jesse Glick
            Path:
            src/main/java/org/jenkinsci/plugins/durabletask/BourneShellScript.java
            src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java
            src/main/java/org/jenkinsci/plugins/durabletask/WindowsBatchScript.java
            src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java
            src/test/java/org/jenkinsci/plugins/durabletask/WindowsBatchScriptTest.java
            http://jenkins-ci.org/commit/durable-task-plugin/6e63ad16450aed47a03080eeb0ff3edbe0b0960d
            Log:
            [FIXED JENKINS-40734] Escape $ in environment variable values so it does not get expanded by Launcher.

            Show
            scm_issue_link SCM/JIRA link daemon added a comment - Code changed in jenkins User: Jesse Glick Path: src/main/java/org/jenkinsci/plugins/durabletask/BourneShellScript.java src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java src/main/java/org/jenkinsci/plugins/durabletask/WindowsBatchScript.java src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java src/test/java/org/jenkinsci/plugins/durabletask/WindowsBatchScriptTest.java http://jenkins-ci.org/commit/durable-task-plugin/6e63ad16450aed47a03080eeb0ff3edbe0b0960d Log: [FIXED JENKINS-40734] Escape $ in environment variable values so it does not get expanded by Launcher.
            Hide
            ericson2314 John Ericson added a comment - - edited

            I think this is a rather glaring breaking change? I and others on IRC did things like set a global custom PATH "extension", redefining path as $PATH:/some/other/fallback. This now breaks and $PATH is not expanded.

            The aforementioned double-quote solution seems to work fine---I'd be very happy if this PR was reverted and a new commit made to restore comparability. The old way is strictly more expressive, as one can, worse case (and not required for this example), escape the env var definition manually.

            Show
            ericson2314 John Ericson added a comment - - edited I think this is a rather glaring breaking change? I and others on IRC did things like set a global custom PATH "extension", redefining path as $PATH:/some/other/fallback . This now breaks and $PATH is not expanded. The aforementioned double-quote solution seems to work fine---I'd be very happy if this PR was reverted and a new commit made to restore comparability. The old way is strictly more expressive, as one can, worse case (and not required for this example), escape the env var definition manually.
            Hide
            opi Alexander Opitz added a comment -

            This fix leads to following issue: JENKINS-41339

            Show
            opi Alexander Opitz added a comment - This fix leads to following issue: JENKINS-41339
            Hide
            b_dean Ben Dean added a comment -

            John Ericson, what aforementioned double-quote solution? None of the things Jesse Glick mentioned as corrections to my Jenkinsfile actually worked. The problem is that if the password or environment variable has a double $ (such as "bar$$baz"), the durable task plugin is changing that to a single $ in the actual environment variable set for the sh step. I don't know how any change in quoting will fix that.

            Show
            b_dean Ben Dean added a comment - John Ericson , what aforementioned double-quote solution? None of the things Jesse Glick mentioned as corrections to my Jenkinsfile actually worked. The problem is that if the password or environment variable has a double $ (such as "bar$$baz" ), the durable task plugin is changing that to a single $ in the actual environment variable set for the sh step. I don't know how any change in quoting will fix that.
            Hide
            ericson2314 John Ericson added a comment - - edited

            Ben Dean

            Oh, I thought you said that

            sh '''
            some-cli do stuff "$USERNAME" "$PASSWORD"
            '''
            

            did work.

            I can indeed see how that wouldn't work. However, you should be able to programmatically escape the definition of those environment variables---as opposed to forcing Jenkins to escape such definitions in all cases as the PR does. If you could show/summarize the code that sets those environment variables I'd be happy to try to figure out how.

            Show
            ericson2314 John Ericson added a comment - - edited Ben Dean Oh, I thought you said that sh ''' some-cli do stuff "$USERNAME" "$PASSWORD" ''' did work. I can indeed see how that wouldn't work. However, you should be able to programmatically escape the definition of those environment variables---as opposed to forcing Jenkins to escape such definitions in all cases as the PR does. If you could show/summarize the code that sets those environment variables I'd be happy to try to figure out how.
            Hide
            b_dean Ben Dean added a comment - - edited

            The environment variables are being set by withCredentials. If you look at the original description on this issue, you'll find complete reproduction steps. The scenario where I was running into this bug was with credentials, but Jesse Glick said that it would be a problem with any environment variables. A Jenkinsfile like this would also not have the correct values for the environment variables:

            node{
                withEnv(['USERNAME=foo', 'PASSWORD=bar$$baz']) {
                    echo "Username: ${env.USERNAME}"
                    echo "Password: ${env.PASSWORD}"
                    sh 'echo Username: "$USERNAME", Password: "$PASSWORD"'
                }
            }
            

            which outputs:

            Started by user admin
            [Pipeline] node
            Running on master in /var/jenkins_home/workspace/test
            [Pipeline] {
            [Pipeline] withEnv
            [Pipeline] {
            [Pipeline] echo
            Username: foo
            [Pipeline] echo
            Password: bar$$baz
            [Pipeline] sh
            [test] Running shell script
            + echo Username: foo, Password: bar$baz
            Username: foo, Password: bar$baz
            [Pipeline] }
            [Pipeline] // withEnv
            [Pipeline] }
            [Pipeline] // node
            [Pipeline] End of Pipeline
            Finished: SUCCESS
            

            note that the environment variables are correct on the env global variable, but they are wrong in the shell step. That output is using durable-task v1.12

            Show
            b_dean Ben Dean added a comment - - edited The environment variables are being set by withCredentials . If you look at the original description on this issue, you'll find complete reproduction steps. The scenario where I was running into this bug was with credentials, but Jesse Glick said that it would be a problem with any environment variables. A Jenkinsfile like this would also not have the correct values for the environment variables: node{ withEnv(['USERNAME=foo', 'PASSWORD=bar$$baz']) { echo "Username: ${env.USERNAME}" echo "Password: ${env.PASSWORD}" sh 'echo Username: "$USERNAME", Password: "$PASSWORD"' } } which outputs: Started by user admin [Pipeline] node Running on master in /var/jenkins_home/workspace/test [Pipeline] { [Pipeline] withEnv [Pipeline] { [Pipeline] echo Username: foo [Pipeline] echo Password: bar$$baz [Pipeline] sh [test] Running shell script + echo Username: foo, Password: bar$baz Username: foo, Password: bar$baz [Pipeline] } [Pipeline] // withEnv [Pipeline] } [Pipeline] // node [Pipeline] End of Pipeline Finished: SUCCESS note that the environment variables are correct on the env global variable, but they are wrong in the shell step. That output is using durable-task v1.12
            Hide
            ericson2314 John Ericson added a comment -

            Ben Dean Ah, my apologies with regards to the OP---I did not realize withCredentials was itself defining the environment variables.

            What I and others do is like

            node{
                withEnv(['PATH=$PATH:/extra/directory/bin']) {
                    sh 'some-cmd' // which happens to be in `/extra/directory/bin` 
                }
            }
            

            which works today and becomes impossible if sh escapes environment variables.

            Both in the interest of not making breaking changes, and in the interest of maximum expressiveness, I say the solution here is to change withCredentials so that it escapes the username and password. Adding manual escaping to functions like withCredentials may feel like whack-a-mole, but so is adding escaping to sh, and it is not possible for me and others to "un0escape" for our use-case.

            Show
            ericson2314 John Ericson added a comment - Ben Dean Ah, my apologies with regards to the OP---I did not realize withCredentials was itself defining the environment variables. What I and others do is like node{ withEnv([ 'PATH=$PATH:/extra/directory/bin' ]) { sh 'some-cmd' // which happens to be in `/extra/directory/bin` } } which works today and becomes impossible if sh escapes environment variables. Both in the interest of not making breaking changes, and in the interest of maximum expressiveness, I say the solution here is to change withCredentials so that it escapes the username and password. Adding manual escaping to functions like withCredentials may feel like whack-a-mole, but so is adding escaping to sh , and it is not possible for me and others to "un0escape" for our use-case.
            Hide
            b_dean Ben Dean added a comment -

            I just tested how this would work with the envinject plugin in a freestyle job and it also replaces the $$ with a single $ in a password. Here's the config.xml

            <project>
              <actions/>
              <description/>
              <keepDependencies>false</keepDependencies>
              <properties/>
              <scm class="hudson.scm.NullSCM"/>
              <canRoam>true</canRoam>
              <disabled>false</disabled>
              <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
              <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
              <triggers/>
              <concurrentBuild>false</concurrentBuild>
              <builders>
                <EnvInjectBuilder plugin="envinject@1.93.1">
                  <info>
                    <propertiesContent>DEF=$ABC:456</propertiesContent>
                  </info>
                </EnvInjectBuilder>
                <hudson.tasks.Shell>
                  <command>
                    echo Password: "$PASSWORD" echo ABC: $ABC echo DEF: $DEF
                  </command>
                </hudson.tasks.Shell>
              </builders>
              <publishers/>
              <buildWrappers>
                <EnvInjectBuildWrapper plugin="envinject@1.93.1">
                  <info>
                    <propertiesContent>ABC=123</propertiesContent>
                    <loadFilesFromMaster>false</loadFilesFromMaster>
                  </info>
                </EnvInjectBuildWrapper>
                <EnvInjectPasswordWrapper plugin="envinject@1.93.1">
                  <injectGlobalPasswords>false</injectGlobalPasswords>
                  <maskPasswordParameters>true</maskPasswordParameters>
                  <passwordEntries>
                    <EnvInjectPasswordEntry>
                      <name>PASSWORD</name>
                      <value>kGFMn/q9EkRqYVDkmuj/tQMlyX/YuesVmq7bZqTGuBs=</value>
                    </EnvInjectPasswordEntry>
                  </passwordEntries>
                </EnvInjectPasswordWrapper>
              </buildWrappers>
            </project>
            

            that outputs:

            Started by user admin
            [EnvInject] - Loading node environment variables.
            Building in workspace /var/jenkins_home/workspace/test
            [EnvInject] - Executing scripts and injecting environment variables after the SCM step.
            [EnvInject] - Injecting as environment variables the properties content 
            ABC=123
            
            [EnvInject] - Variables injected successfully.
            [EnvInject] - Mask passwords that will be passed as build parameters.
            [EnvInject] - Injecting environment variables from a build step.
            [EnvInject] - Injecting as environment variables the properties content 
            DEF=123:456
            
            [EnvInject] - Variables injected successfully.
            [test] $ /bin/sh -xe /tmp/hudson7658063203774926176.sh
            + echo Password: bar$baz
            Password: bar$baz
            + echo ABC: 123
            ABC: 123
            + echo DEF: 123:456
            DEF: 123:456
            Finished: SUCCESS
            

            I also tried this using the plain-credentials and credentials-binding plugins in a freestyle job. Same thing, $$ in the password gets replaced with $. Here's that config.xml

            <project>
              <actions/>
              <description/>
              <keepDependencies>false</keepDependencies>
              <properties/>
              <scm class="hudson.scm.NullSCM"/>
              <canRoam>true</canRoam>
              <disabled>false</disabled>
              <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
              <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
              <triggers/>
              <concurrentBuild>false</concurrentBuild>
              <builders>
                <hudson.tasks.Shell>
                  <command>echo Username: "$USERNAME" Password: "$PASSWORD"</command>
                </hudson.tasks.Shell>
              </builders>
              <publishers/>
              <buildWrappers>
                <org.jenkinsci.plugins.credentialsbinding.impl.SecretBuildWrapper plugin="credentials-binding@1.10">
                  <bindings>
                    <org.jenkinsci.plugins.credentialsbinding.impl.UsernamePasswordMultiBinding>
                      <credentialsId>foo</credentialsId>
                      <usernameVariable>USERNAME</usernameVariable>
                      <passwordVariable>PASSWORD</passwordVariable>
                    </org.jenkinsci.plugins.credentialsbinding.impl.UsernamePasswordMultiBinding>
                  </bindings>
                </org.jenkinsci.plugins.credentialsbinding.impl.SecretBuildWrapper>
              </buildWrappers>
            </project>
            

            which outputs:

            Started by user admin
            [EnvInject] - Loading node environment variables.
            Building in workspace /var/jenkins_home/workspace/test2
            [test2] $ /bin/sh -xe /tmp/hudson6763543920526348076.sh
            + echo Username: **** Password: bar$baz
            Username: **** Password: bar$baz
            Finished: SUCCESS
            

            So I guess durable-task was at least consistent with how things work in the older plugins and freestyle jobs. I'm not really sure what the answer is for dealing with passwords that look like they have bash variable expansion in them.

            Show
            b_dean Ben Dean added a comment - I just tested how this would work with the envinject plugin in a freestyle job and it also replaces the $$ with a single $ in a password. Here's the config.xml <project> <actions/> <description/> <keepDependencies> false </keepDependencies> <properties/> <scm class= "hudson.scm.NullSCM" /> <canRoam> true </canRoam> <disabled> false </disabled> <blockBuildWhenDownstreamBuilding> false </blockBuildWhenDownstreamBuilding> <blockBuildWhenUpstreamBuilding> false </blockBuildWhenUpstreamBuilding> <triggers/> <concurrentBuild> false </concurrentBuild> <builders> <EnvInjectBuilder plugin= "envinject@1.93.1" > <info> <propertiesContent> DEF=$ABC:456 </propertiesContent> </info> </EnvInjectBuilder> <hudson.tasks.Shell> <command> echo Password: "$PASSWORD" echo ABC: $ABC echo DEF: $DEF </command> </hudson.tasks.Shell> </builders> <publishers/> <buildWrappers> <EnvInjectBuildWrapper plugin= "envinject@1.93.1" > <info> <propertiesContent> ABC=123 </propertiesContent> <loadFilesFromMaster> false </loadFilesFromMaster> </info> </EnvInjectBuildWrapper> <EnvInjectPasswordWrapper plugin= "envinject@1.93.1" > <injectGlobalPasswords> false </injectGlobalPasswords> <maskPasswordParameters> true </maskPasswordParameters> <passwordEntries> <EnvInjectPasswordEntry> <name> PASSWORD </name> <value> kGFMn/q9EkRqYVDkmuj/tQMlyX/YuesVmq7bZqTGuBs= </value> </EnvInjectPasswordEntry> </passwordEntries> </EnvInjectPasswordWrapper> </buildWrappers> </project> that outputs: Started by user admin [EnvInject] - Loading node environment variables. Building in workspace /var/jenkins_home/workspace/test [EnvInject] - Executing scripts and injecting environment variables after the SCM step. [EnvInject] - Injecting as environment variables the properties content ABC=123 [EnvInject] - Variables injected successfully. [EnvInject] - Mask passwords that will be passed as build parameters. [EnvInject] - Injecting environment variables from a build step. [EnvInject] - Injecting as environment variables the properties content DEF=123:456 [EnvInject] - Variables injected successfully. [test] $ /bin/sh -xe /tmp/hudson7658063203774926176.sh + echo Password: bar$baz Password: bar$baz + echo ABC: 123 ABC: 123 + echo DEF: 123:456 DEF: 123:456 Finished: SUCCESS I also tried this using the plain-credentials and credentials-binding plugins in a freestyle job. Same thing, $$ in the password gets replaced with $ . Here's that config.xml <project> <actions/> <description/> <keepDependencies> false </keepDependencies> <properties/> <scm class= "hudson.scm.NullSCM" /> <canRoam> true </canRoam> <disabled> false </disabled> <blockBuildWhenDownstreamBuilding> false </blockBuildWhenDownstreamBuilding> <blockBuildWhenUpstreamBuilding> false </blockBuildWhenUpstreamBuilding> <triggers/> <concurrentBuild> false </concurrentBuild> <builders> <hudson.tasks.Shell> <command> echo Username: "$USERNAME" Password: "$PASSWORD" </command> </hudson.tasks.Shell> </builders> <publishers/> <buildWrappers> <org.jenkinsci.plugins.credentialsbinding.impl.SecretBuildWrapper plugin= "credentials-binding@1.10" > <bindings> <org.jenkinsci.plugins.credentialsbinding.impl.UsernamePasswordMultiBinding> <credentialsId> foo </credentialsId> <usernameVariable> USERNAME </usernameVariable> <passwordVariable> PASSWORD </passwordVariable> </org.jenkinsci.plugins.credentialsbinding.impl.UsernamePasswordMultiBinding> </bindings> </org.jenkinsci.plugins.credentialsbinding.impl.SecretBuildWrapper> </buildWrappers> </project> which outputs: Started by user admin [EnvInject] - Loading node environment variables. Building in workspace /var/jenkins_home/workspace/test2 [test2] $ /bin/sh -xe /tmp/hudson6763543920526348076.sh + echo Username: **** Password: bar$baz Username: **** Password: bar$baz Finished: SUCCESS So I guess durable-task was at least consistent with how things work in the older plugins and freestyle jobs. I'm not really sure what the answer is for dealing with passwords that look like they have bash variable expansion in them.
            Hide
            ericson2314 John Ericson added a comment - - edited

            I'm not really sure what the answer is for dealing with passwords that look like they have bash variable expansion in them.

            Having withCredentials escape the username and password before sticking them in the environment variables should do the trick, right?

            Show
            ericson2314 John Ericson added a comment - - edited I'm not really sure what the answer is for dealing with passwords that look like they have bash variable expansion in them. Having withCredentials escape the username and password before sticking them in the environment variables should do the trick, right?
            Hide
            b_dean Ben Dean added a comment -

            Yes, that would be answer for withCredentials. But it would make the pipelines behave a bit differently than the envinject and credentials-binding plugins which do not escape $ in passwords. Maybe they should be fixed too.

            Show
            b_dean Ben Dean added a comment - Yes, that would be answer for withCredentials . But it would make the pipelines behave a bit differently than the envinject and credentials-binding plugins which do not escape $ in passwords. Maybe they should be fixed too.
            Hide
            jglick Jesse Glick added a comment -

            Freestyle projects may always have been broken in this respect, but not my problem. This was clearly a bug, and 1.13 fixes it. I am tracking other use cases in JENKINS-41339.

            Show
            jglick Jesse Glick added a comment - Freestyle projects may always have been broken in this respect, but not my problem. This was clearly a bug, and 1.13 fixes it. I am tracking other use cases in JENKINS-41339 .
            Hide
            jglick Jesse Glick added a comment -
            withEnv(['PATH=$PATH:/extra/directory/bin'])
            

            is incorrect. You may use either

            withEnv(["PATH=$PATH:/extra/directory/bin"])
            

            (interpolating the current value in Groovy) or

            withEnv(['PATH+EXTRA=/extra/directory/bin'])
            

            as documented in the withEnv step.

            Show
            jglick Jesse Glick added a comment - withEnv([ 'PATH=$PATH:/extra/directory/bin' ]) is incorrect. You may use either withEnv([ "PATH=$PATH:/extra/directory/bin" ]) (interpolating the current value in Groovy) or withEnv([ 'PATH+EXTRA=/extra/directory/bin' ]) as documented in the withEnv step.
            Hide
            ericson2314 John Ericson added a comment -

            Jesse Glick I'm no fan of barely-planned string evaluation either, but I don't think it's not fair to simply say "freestyle jobs are broken": values in env may come from either the Jenkins GUI or groovy (e.g. withEnv) and there is no way of knowing. Long predating any pipeline work, the effective behavior of GUI-defined environment variables has been established that they may contain variable references. Freestyle jobs implement this, not define it.

            You can make that the semantics of withEnv if you want, but then to handle the GUI case correctly I see the only option being escaping the withEnv arguments before they are put in env so the GUI ones still get expanded correctly.

            Show
            ericson2314 John Ericson added a comment - Jesse Glick I'm no fan of barely-planned string evaluation either, but I don't think it's not fair to simply say "freestyle jobs are broken": values in env may come from either the Jenkins GUI or groovy (e.g. withEnv ) and there is no way of knowing. Long predating any pipeline work, the effective behavior of GUI-defined environment variables has been established that they may contain variable references. Freestyle jobs implement this, not define it. You can make that the semantics of withEnv if you want, but then to handle the GUI case correctly I see the only option being escaping the withEnv arguments before they are put in env so the GUI ones still get expanded correctly.

              People

              • Assignee:
                jglick Jesse Glick
                Reporter:
                b_dean Ben Dean
              • Votes:
                0 Vote for this issue
                Watchers:
                8 Start watching this issue

                Dates

                • Created:
                  Updated:
                  Resolved: