Send fully-resolved variable values to the runner

Merged Pedro Pombeiro requested to merge pedropombeiro/variable_inside_variable into master

What does this MR do?

This MR does the following:

  1. Changes Ci::BuildRunnerPresenter.variables to leverage ExpandVariables.expand_variables_collection so that variables sent in the job response are expanded;
  2. Documents the new nested variable value resolution feature and the :variable_inside_variable FF.

TODO

List of MRs

  • MR 1: Make Collection::Sorted class take and output a Variables::Collection;
  • MR 2: Add #sorted_collection method and #errors property to Variables::Collection;
  • MR 3: Add indexing/lookup of variables to Variables::Collection;
  • MR 4: Add support for depends_on property to Variables::Collection::Item;
  • MR 5: Add support for raw property to Variables::Collection::Item;
  • MR 6: Add an #expand_variables_collection function to ExpandVariables to perform a full expansion of a Variables::Collection, gated by the new project-scoped :variable_inside_variable feature flag. It will return the original object if a cyclic reference is detected;
  • MR 7: Add error reporting to Gitlab::Ci::Pipeline::Seed::Build to let the user know when a problem occurs with variable expansion;
  • MR 7.5: Implement expression parser to be able to handle escaping in strings;
  • MR 8: Change Ci::BuildRunnerPresenter#variables to leverage ExpandVariables#expand_variables_collection so that variables sent in the job response are expanded;

Screenshots (strongly suggested)

Job definition
variables:
  VAR3: "test-$SECRET_VAR"
  VAR2: "test-${VAR1}" 
  VAR1: "${PROTECTED_VAR}-${CI_PIPELINE_ID}"

start_evaluation:
  script:
    - printenv
    - echo "VAR1=${VAR1}"
    - echo "VAR2=${VAR2}"
    - echo "VAR3=${VAR3}"
    - echo "PROTECTED_VAR=${PROTECTED_VAR}"
    - echo "SECRET_VAR=${SECRET_VAR}"
    - echo Done
  tags: [local-gdk, shell]

Here's a screenshot from a debugging session in GitLab Runner showing the received job response's variables. It shows that:

  • the VAR1 variable value is sent from GitLab already resolved, as opposed to what would be the result from the main branch (it would show as ${PROTECTED_VAR}-${CI_PIPELINE_ID};
  • the protected variable is not expanded, since the job ran on an unprotected branch;
  • the masked variable was expanded and is later masked by the UI.
Screenshot image

Does this MR meet the acceptance criteria?

Conformity

Availability and Testing

Test suite executed by contributor

1. Resolving/masking of referenced variables

.gitlab-ci.yml
variables:
  variable_1: build-dir-$CI_BUILD_DIR
  A: var-$B
  B: var-$C
  C: var-$SECRET_VAR

start_evaluation:
  script:
    - printenv
    - echo Done
  tags: [local-gdk, shell]
Feature flag disabled

In Rails console (rails c):

Feature.disable(:variable_inside_variable)

A masked variable SECRET_VAR is defined in CI / CD Settings / Variables with value maskedvalue:

Output
...
SECRET_VAR=[MASKED]
C=var-[MASKED]
B=var-var-$SECRET_VAR
A=var-var-$C
...
++ echo '$ echo Done'
$ echo Done
++ echo Done
Done
+ exit 0
Job succeeded
Feature flag enabled

In Rails console (rails c):

Feature.enable(:variable_inside_variable)

All dependent values get recursively masked:

Output
...
SECRET_VAR=[MASKED]
C=var-[MASKED]
B=var-var-[MASKED]
A=var-var-var-[MASKED]
...
++ echo '$ echo Done'
$ echo Done
++ echo Done
Done
+ exit 0
Job succeeded

2. Resolving variables with cyclic reference

Start a pipeline with a value override C: var-$A to create a cyclic reference

Feature flag disabled

In Rails console (rails c):

Feature.disable(:variable_inside_variable)

The build succeeds.

Output
...
SECRET_VAR=[MASKED]
C=var-$B
B=var-$A
A=var-var-$C
...
++ echo '$ echo Done'
$ echo Done
++ echo Done
Done
+ exit 0
Job succeeded
Feature flag enabled

NOTE: If a job has already succeeded and the FF is then enabled, the build will be marked as non-retryable.

In Rails console (rails c):

Feature.enable(:variable_inside_variable)
Output

The build fails with an error message:

image

3. Pass variable value unmodified to Runner

The variable references that cannot be resolved should be passed as-is to the runner for expansion there (e.g. for variables that only exist on the runner).

.gitlab-ci.yml
variables:
  variable_1: build-dir-$CI_BUILD_DIR

start_evaluation:
  script:
    - printenv
    - echo Done
  tags: [local-gdk]
Feature flag disabled

In Rails console (rails c):

Feature.disable(:variable_inside_variable)
Output
...
variable_1=build-dir-/Users/pedropombeiro/src/gitlab.com/gitlab-org/gitlab-runner/builds
...
++ echo '$ echo Done'
$ echo Done
++ echo Done
Done
+ exit 0
Job succeeded
Feature flag enabled

In Rails console (rails c):

Feature.enable(:variable_inside_variable)

Runner variable is still expanded:

Output
...
variable_1=build-dir-/Users/pedropombeiro/src/gitlab.com/gitlab-org/gitlab-runner/builds
...
++ echo '$ echo Done'
$ echo Done
++ echo Done
Done
+ exit 0
Job succeeded

Security

If this MR contains changes to processing or storing of credentials or tokens, authorization and authentication methods and other items described in the security review guidelines:

  • Label as security and @ mention @gitlab-com/gl-security/appsec
  • The MR includes necessary changes to maintain consistency between UI, API, email, or other methods
  • Security reports checked/validated by a reviewer from the AppSec team

Closes gitlab-runner#26345 (closed)

Closes gitlab-runner#4319 (closed)

Edited by Pedro Pombeiro