github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/test/apiv2/test-apiv2 (about)

     1  #!/usr/bin/env bash
     2  #
     3  # Usage: test-apiv2 [PORT]
     4  #
     5  # DEVELOPER NOTE: you almost certainly don't need to play in here. See README.
     6  #
     7  ME=$(basename $0)
     8  
     9  ###############################################################################
    10  # BEGIN stuff you can but probably shouldn't customize
    11  
    12  PODMAN_TEST_IMAGE_REGISTRY=${PODMAN_TEST_IMAGE_REGISTRY:-"quay.io"}
    13  PODMAN_TEST_IMAGE_USER=${PODMAN_TEST_IMAGE_USER:-"libpod"}
    14  PODMAN_TEST_IMAGE_NAME=${PODMAN_TEST_IMAGE_NAME:-"alpine_labels"}
    15  PODMAN_TEST_IMAGE_TAG=${PODMAN_TEST_IMAGE_TAG:-"latest"}
    16  PODMAN_TEST_IMAGE_FQN="$PODMAN_TEST_IMAGE_REGISTRY/$PODMAN_TEST_IMAGE_USER/$PODMAN_TEST_IMAGE_NAME:$PODMAN_TEST_IMAGE_TAG"
    17  
    18  IMAGE=$PODMAN_TEST_IMAGE_FQN
    19  
    20  REGISTRY_IMAGE="${PODMAN_TEST_IMAGE_REGISTRY}/${PODMAN_TEST_IMAGE_USER}/registry:2.7"
    21  
    22  # END   stuff you can but probably shouldn't customize
    23  ###############################################################################
    24  # BEGIN setup
    25  
    26  TMPDIR=${TMPDIR:-/tmp}
    27  WORKDIR=$(mktemp --tmpdir -d $ME.tmp.XXXXXX)
    28  
    29  # Log of all HTTP requests and responses; always make '.log' point to latest
    30  LOGBASE=${TMPDIR}/$ME.log
    31  LOG=${LOGBASE}.$(date +'%Y%m%dT%H%M%S')
    32  ln -sf $LOG $LOGBASE
    33  
    34  HOST=localhost
    35  PORT=${PODMAN_SERVICE_PORT:-8081}
    36  
    37  # Keep track of test count and failures in files, not variables, because
    38  # variables don't carry back up from subshells.
    39  testcounter_file=$WORKDIR/.testcounter
    40  failures_file=$WORKDIR/.failures
    41  
    42  echo 0 >$testcounter_file
    43  echo 0 >$failures_file
    44  
    45  # Where the tests live
    46  TESTS_DIR=$(realpath $(dirname $0))
    47  
    48  # As of 2021-11 podman has one external helper binary, rootlessport, needed
    49  # for rootless networking.
    50  if [[ -z "$CONTAINERS_HELPER_BINARY_DIR" ]]; then
    51      export CONTAINERS_HELPER_BINARY_DIR=$(realpath ${TESTS_DIR}/../../bin)
    52  fi
    53  
    54  # Path to podman binary
    55  PODMAN_BIN=${PODMAN:-${CONTAINERS_HELPER_BINARY_DIR}/podman}
    56  
    57  # Cleanup handlers
    58  clean_up_server() {
    59      if [ -n "$service_pid" ]; then
    60          # Remove any containers and images; this prevents the following warning:
    61          #  'rm: cannot remove '/.../overlay': Device or resource busy
    62          podman rm -a
    63          podman rmi -af
    64  
    65          stop_registry
    66          stop_service
    67      fi
    68  }
    69  
    70  # Any non-test-related error, be it syntax or podman-command, fails here.
    71  err_handler() {
    72      echo "Fatal error in ${BASH_SOURCE[1]}:${BASH_LINENO[0]}"
    73      echo "Log:"
    74      sed -e 's/^/  >/' <$WORKDIR/output.log
    75      echo "Bailing."
    76      clean_up_server
    77  }
    78  
    79  trap err_handler ERR
    80  
    81  # END   setup
    82  ###############################################################################
    83  # BEGIN infrastructure code - the helper functions used in tests themselves
    84  
    85  #########
    86  #  die  #  Exit error with a message to stderr
    87  #########
    88  function die() {
    89      echo "$ME: $*" >&2
    90      clean_up_server
    91      exit 1
    92  }
    93  
    94  ########
    95  #  is  #  Simple comparison
    96  ########
    97  function is() {
    98      local actual=$1
    99      local expect=$2
   100      local testname=$3
   101  
   102      if [ "$actual" = "$expect" ]; then
   103          # On success, include expected value; this helps readers understand
   104          _show_ok 1 "$testname=$expect"
   105          return
   106      fi
   107      _show_ok 0 "$testname" "$expect" "$actual"
   108  }
   109  
   110  ##########
   111  #  like  #  Compare, but allowing patterns
   112  ##########
   113  function like() {
   114      local actual=$1
   115      local expect=$2
   116      local testname=$3
   117  
   118      if expr "$actual" : "$expect" &>/dev/null; then
   119          # On success, include expected value; this helps readers understand
   120          # (but don't show enormous multi-line output like 'generate kube')
   121          blurb=$(head -n1 <<<"$actual")
   122          _show_ok 1 "$testname ('$blurb') ~ $expect"
   123          return
   124      fi
   125      _show_ok 0 "$testname" "~ $expect" "$actual"
   126  }
   127  
   128  ##############
   129  #  _show_ok  #  Helper for is() and like(): displays 'ok' or 'not ok'
   130  ##############
   131  function _show_ok() {
   132      local ok=$1
   133      local testname=$2
   134  
   135      # If output is a tty, colorize pass/fail
   136      local red=
   137      local green=
   138      local reset=
   139      local bold=
   140      if [ -t 1 ]; then
   141          red='\e[31m'
   142          green='\e[32m'
   143          reset='\e[0m'
   144          bold='\e[1m'
   145      fi
   146  
   147      _bump $testcounter_file
   148      count=$(<$testcounter_file)
   149  
   150      # "skip" is a special case of "ok". Assume that our caller has included
   151      # the magical '# skip - reason" comment string.
   152      if [[ $ok == "skip" ]]; then
   153          # colon-plus: replace green with yellow, but only if green is non-null
   154          green="${green:+\e[33m}"
   155          ok=1
   156      fi
   157      if [ $ok -eq 1 ]; then
   158          echo -e "${green}ok $count ${TEST_CONTEXT} $testname${reset}"
   159          echo    "ok $count ${TEST_CONTEXT} $testname" >>$LOG
   160          return
   161      fi
   162  
   163      # Failed
   164      local expect=$3
   165      local actual=$4
   166      echo -e "${red}not ok $count ${TEST_CONTEXT} $testname${reset}"
   167      echo -e "${red}#  expected: $expect${reset}"
   168      echo -e "${red}#    actual: ${bold}$actual${reset}"
   169  
   170      echo    "not ok $count ${TEST_CONTEXT} $testname" >>$LOG
   171      echo    "  expected: $expect"                     >>$LOG
   172  
   173      _bump $failures_file
   174  }
   175  
   176  ###########
   177  #  _bump  #  Increment a counter in a file
   178  ###########
   179  function _bump() {
   180      local file=$1
   181  
   182      count=$(<$file)
   183      echo $(( $count + 1 )) >| $file
   184  }
   185  
   186  #############
   187  #  jsonify  #  convert 'foo=bar,x=y' to json {"foo":"bar","x":"y"}
   188  #############
   189  function jsonify() {
   190      # convert each to double-quoted form
   191      local -a settings_out
   192      for i in "$@"; do
   193          # Each argument is of the form foo=bar. Separate into left and right.
   194          local lhs
   195          local rhs
   196          IFS='=' read lhs rhs <<<"$i"
   197  
   198          if [[ $rhs =~ \" || $rhs == true || $rhs == false || $rhs =~ ^-?[0-9]+$ ]]; then
   199              # rhs has been pre-formatted for JSON or a non-string, do not change it
   200              :
   201          elif [[ $rhs == False ]]; then
   202              # JSON boolean is lowercase only
   203              rhs=false
   204          elif [[ $rhs == True ]]; then
   205              # JSON boolean is lowercase only
   206              rhs=true
   207          else
   208              rhs="\"${rhs}\""
   209          fi
   210          settings_out+=("\"${lhs}\":${rhs}")
   211      done
   212  
   213      # ...and wrap inside braces, with comma separator if multiple fields
   214      (IFS=','; echo "{${settings_out[*]}}")
   215  }
   216  
   217  #######
   218  #  t  #  Main test helper
   219  #######
   220  function t() {
   221      local method=$1; shift
   222      local path=$1; shift
   223      local -a curl_args
   224      local content_type="application/json"
   225  
   226      local testname="$method $path"
   227      # POST and PUT requests may be followed by one or more key=value pairs.
   228      # Slurp the command line until we see a 3-digit status code.
   229      if [[ $method = "POST" || $method == "PUT" ]]; then
   230          local -a post_args
   231          for arg; do
   232              case "$arg" in
   233                  *=*)              post_args+=("$arg");
   234                                    shift;;
   235                  *.tar)            curl_args+=(--data-binary @$arg);
   236                                    content_type="application/x-tar";
   237                                    shift;;
   238                  application/*)    content_type="$arg";
   239                                    shift;;
   240                  [1-9][0-9][0-9])  break;;
   241                  *)                die "Internal error: invalid POST arg '$arg'" ;;
   242              esac
   243          done
   244          if [[ -z "$curl_args" ]]; then
   245              curl_args+=(-d $(jsonify ${post_args[@]}))
   246              testname="$testname [${curl_args[@]}]"
   247          fi
   248      fi
   249  
   250      # entrypoint path can include a descriptive comment; strip it off
   251      path=${path%% *}
   252  
   253      local url=$path
   254      if ! [[ $path =~ ^'http://' ]]; then
   255          # path may include JSONish params that curl will barf on; url-encode them
   256          path="${path//'['/%5B}"
   257          path="${path//']'/%5D}"
   258          path="${path//'{'/%7B}"
   259          path="${path//'}'/%7D}"
   260          path="${path//':'/%3A}"
   261  
   262          # If given path begins with /, use it as-is; otherwise prepend /version/
   263          url=http://$HOST:$PORT
   264          case "$path" in
   265          /*) url="$url$path" ;;
   266          libpod/*) url="$url/v4.0.0/$path" ;;
   267          *)  url="$url/v1.41/$path" ;;
   268          esac
   269      fi
   270  
   271      # curl -X HEAD but without --head seems to wait for output anyway
   272      if [[ $method == "HEAD" ]]; then
   273          curl_args+=("--head")
   274      fi
   275  
   276      if [ -n "$REGISTRY_CONFIG_HEADER" ]; then
   277          curl_args+=(-H "X-Registry-Config: $REGISTRY_CONFIG_HEADER")
   278      fi
   279  
   280      local expected_code=$1; shift
   281  
   282      # Log every action we do
   283      echo "-------------------------------------------------------------" >>$LOG
   284      echo "\$ $testname"                                                  >>$LOG
   285      rm -f $WORKDIR/curl.*
   286      # -s = silent, but --write-out 'format' gives us important response data
   287      # The hairy "{ ...;rc=$?; } || :" lets us capture curl's exit code and
   288      # give a helpful diagnostic if it fails.
   289      { response=$(curl -s -X $method "${curl_args[@]}"            \
   290                      -H "Content-type: $content_type"             \
   291                      --dump-header $WORKDIR/curl.headers.out      \
   292                      --write-out '%{http_code}^%{content_type}^%{time_total}' \
   293                      -o $WORKDIR/curl.result.out "$url"); rc=$?; } || :
   294  
   295      # Any error from curl is instant bad news, from which we can't recover
   296      if [[ $rc -ne 0 ]]; then
   297          die "curl failure ($rc) on $url - cannot continue"
   298      fi
   299  
   300      # Show returned headers (without trailing ^M or empty lines) in log file.
   301      # Sometimes -- I can't remember why! -- we don't get headers.
   302      if [[ -e $WORKDIR/curl.headers.out ]]; then
   303          tr -d '\015' < $WORKDIR/curl.headers.out | egrep '.' >>$LOG
   304      fi
   305  
   306      IFS='^' read actual_code content_type time_total <<<"$response"
   307      printf "X-Response-Time: ${time_total}s\n\n" >>$LOG
   308  
   309      # Log results, if text. If JSON, filter through jq for readability.
   310      if [[ $content_type =~ /octet ]]; then
   311          output="[$(file --brief $WORKDIR/curl.result.out)]"
   312          echo "$output" >>$LOG
   313      elif [[ -e $WORKDIR/curl.result.out ]]; then
   314          # Output from /logs sometimes includes NULs. Strip them.
   315          output=$(tr -d '\0' < $WORKDIR/curl.result.out)
   316  
   317          if [[ $content_type =~ application/json ]] && [[ $method != "HEAD" ]]; then
   318              jq . <<<"$output" >>$LOG
   319          else
   320              echo "$output" >>$LOG
   321          fi
   322      else
   323          output=
   324          echo "[no output]" >>$LOG
   325      fi
   326  
   327      # Test return code
   328      is "$actual_code" "$expected_code" "$testname : status"
   329  
   330      # Special case: 204/304, by definition, MUST NOT return content (rfc2616)
   331      if [[ $expected_code = 204 || $expected_code = 304 ]]; then
   332          if [ -n "$*" ]; then
   333              die "Internal error: ${expected_code} status returns no output; fix your test."
   334          fi
   335          if [ -n "$output" ]; then
   336              _show_ok 0 "$testname: ${expected_code} status returns no output" "''" "$output"
   337          fi
   338          return
   339      fi
   340  
   341      local i
   342  
   343      # Special case: if response code does not match, dump the response body
   344      # and skip all further subtests.
   345      if [[ $actual_code != $expected_code ]]; then
   346          echo -e "#  response: $output"
   347          for i; do
   348              _show_ok skip "$testname: $i # skip - wrong return code"
   349          done
   350          return
   351      fi
   352  
   353      for i; do
   354          if expr "$i" : "[^=~]\+=.*" >/dev/null; then
   355              # Exact match on json field
   356              json_field=$(expr "$i" : "\([^=]*\)=")
   357              expect=$(expr "$i" : '[^=]*=\(.*\)')
   358              actual=$(jq -r "$json_field" <<<"$output")
   359              is "$actual" "$expect" "$testname : $json_field"
   360          elif expr "$i" : "[^=~]\+~.*" >/dev/null; then
   361              # regex match on json field
   362              json_field=$(expr "$i" : "\([^~]*\)~")
   363              expect=$(expr "$i" : '[^~]*~\(.*\)')
   364              actual=$(jq -r "$json_field" <<<"$output")
   365              like "$actual" "$expect" "$testname : $json_field"
   366          else
   367              # Direct string comparison
   368              is "$output" "$i" "$testname : output"
   369          fi
   370      done
   371  }
   372  
   373  ###################
   374  #  start_service  #  Run the socket listener
   375  ###################
   376  service_pid=
   377  function start_service() {
   378      # If there's a listener on the port, nothing for us to do
   379      { exec 3<> /dev/tcp/$HOST/$PORT; } &>/dev/null && return
   380  
   381      test -x $PODMAN_BIN || die "Not found: $PODMAN_BIN"
   382  
   383      if [ "$HOST" != "localhost" ]; then
   384          die "Cannot start service on non-localhost ($HOST)"
   385      fi
   386  
   387      echo "rootdir: "$WORKDIR
   388      # Some tests use shortnames; force registry override to work around
   389      # docker.io throttling.
   390  #    FIXME esm revisit pulling expected images re: shortnames caused tests to fail
   391  #    env CONTAINERS_REGISTRIES_CONF=$TESTS_DIR/../registries.conf
   392      $PODMAN_BIN \
   393          --root $WORKDIR/server_root --syslog=true \
   394          system service \
   395          --time 0 \
   396          tcp:127.0.0.1:$PORT \
   397          &> $WORKDIR/server.log &
   398      service_pid=$!
   399  
   400      wait_for_port $HOST $PORT
   401  }
   402  
   403  function stop_service() {
   404      # Stop the server
   405      if [[ -n $service_pid ]]; then
   406          kill $service_pid || :
   407          wait $service_pid || :
   408      fi
   409  }
   410  
   411  ####################
   412  #  start_registry  #  Run a local registry
   413  ####################
   414  REGISTRY_PORT=
   415  REGISTRY_USERNAME=
   416  REGISTRY_PASSWORD=
   417  REGISTRY_CONFIG_HEADER=
   418  function start_registry() {
   419      # We can be invoked multiple times, e.g. from different subtests, but
   420      # let's assume that once started we only kill it at the end of tests.
   421      if [[ -n "$REGISTRY_PORT" ]]; then
   422          return
   423      fi
   424  
   425      REGISTRY_PORT=$(random_port)
   426      REGISTRY_USERNAME=u$(random_string 7)
   427      REGISTRY_PASSWORD=p$(random_string 7)
   428      REGISTRY_CONFIG_HEADER=$(echo "{\"localhost:${REGISTRY_PORT}\":{\"username\":\"${REGISTRY_USERNAME}\",\"password\":\"${REGISTRY_PASSWORD}\"}}" | base64 --wrap=0)
   429  
   430      local REGDIR=$WORKDIR/registry
   431      local AUTHDIR=$REGDIR/auth
   432      mkdir -p $AUTHDIR
   433  
   434      mkdir -p ${REGDIR}/{root,runroot}
   435      local PODMAN_REGISTRY_ARGS="--root ${REGDIR}/root --runroot ${REGDIR}/runroot"
   436  
   437      # Give it three tries, to compensate for network flakes
   438      podman ${PODMAN_REGISTRY_ARGS} pull $REGISTRY_IMAGE ||
   439          podman ${PODMAN_REGISTRY_ARGS} pull $REGISTRY_IMAGE ||
   440          podman ${PODMAN_REGISTRY_ARGS} pull $REGISTRY_IMAGE
   441  
   442      # Create a local cert and credentials
   443      # FIXME: is there a hidden "--quiet" flag? This is too noisy.
   444      openssl req -newkey rsa:4096 -nodes -sha256 \
   445              -keyout $AUTHDIR/domain.key -x509 -days 2 \
   446              -out $AUTHDIR/domain.crt \
   447              -subj "/C=US/ST=Foo/L=Bar/O=Red Hat, Inc./CN=registry host certificate" \
   448              -addext subjectAltName=DNS:localhost
   449      htpasswd -Bbn ${REGISTRY_USERNAME} ${REGISTRY_PASSWORD} \
   450               > $AUTHDIR/htpasswd
   451  
   452      # Run the registry, and wait for it to come up
   453      podman ${PODMAN_REGISTRY_ARGS} run -d \
   454             -p ${REGISTRY_PORT}:5000 \
   455             --name registry \
   456             -v $AUTHDIR:/auth:Z \
   457             -e "REGISTRY_AUTH=htpasswd" \
   458             -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
   459             -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
   460             -e REGISTRY_HTTP_TLS_CERTIFICATE=/auth/domain.crt \
   461             -e REGISTRY_HTTP_TLS_KEY=/auth/domain.key \
   462             ${REGISTRY_IMAGE}
   463  
   464      wait_for_port localhost $REGISTRY_PORT 10
   465  }
   466  
   467  function stop_registry() {
   468      local REGDIR=${WORKDIR}/registry
   469      if [[ -d $REGDIR ]]; then
   470          local OPTS="--root ${REGDIR}/root --runroot ${REGDIR}/runroot"
   471          podman $OPTS stop -f -t 0 -a
   472  
   473          # rm/rmi are important when running rootless: without them we
   474          # get EPERMS in tmpdir cleanup because files are owned by subuids.
   475          podman $OPTS rm -f -a
   476          podman $OPTS rmi -f -a
   477      fi
   478  }
   479  
   480  #################
   481  #  random_port  #  Random open port; arg is range (min-max), default 5000-5999
   482  #################
   483  function random_port() {
   484      local range=${1:-5000-5999}
   485  
   486      local port
   487      for port in $(shuf -i ${range}); do
   488          if ! { exec 5<> /dev/tcp/127.0.0.1/$port; } &>/dev/null; then
   489              echo $port
   490              return
   491          fi
   492      done
   493  
   494      die "Could not find open port in range $range"
   495  }
   496  
   497  ###################
   498  #  random_string  #  Pseudorandom alphanumeric string of given length
   499  ###################
   500  function random_string() {
   501      local length=${1:-10}
   502      head /dev/urandom | tr -dc a-zA-Z0-9 | head -c$length
   503  }
   504  
   505  ###################
   506  #  wait_for_port  #  Returns once port is available on host
   507  ###################
   508  function wait_for_port() {
   509      local host=$1                      # Probably "localhost"
   510      local port=$2                      # Numeric port
   511      local _timeout=${3:-5}             # Optional; default to 5 seconds
   512  
   513      local path=/dev/tcp/$host/$port
   514  
   515      # Wait
   516      local i=$_timeout
   517      while [ $i -gt 0 ]; do
   518          { exec 3<> /dev/tcp/$host/$port; } &>/dev/null && return
   519          sleep 1
   520          i=$(( $i - 1 ))
   521      done
   522      die "Timed out (${_timeout}s) waiting for service ($path)"
   523  }
   524  
   525  ############
   526  #  podman  #  Needed by some test scripts to invoke the actual podman binary
   527  ############
   528  function podman() {
   529      echo "\$ $PODMAN_BIN $*"                           >>$WORKDIR/output.log
   530  #    env CONTAINERS_REGISTRIES_CONF=$TESTS_DIR/../registries.conf \
   531          $PODMAN_BIN --root $WORKDIR/server_root "$@"   >>$WORKDIR/output.log 2>&1
   532  }
   533  
   534  ####################
   535  #  root, rootless  #  Is server rootless?
   536  ####################
   537  ROOTLESS=
   538  function root() {
   539      ! rootless
   540  }
   541  
   542  function rootless() {
   543      if [[ -z $ROOTLESS ]]; then
   544          ROOTLESS=$(curl -s http://$HOST:$PORT/v1.40/info | jq .Rootless)
   545      fi
   546      test "$ROOTLESS" = "true"
   547  }
   548  
   549  # True if cgroups v2 are enabled
   550  function have_cgroupsv2() {
   551      cgroup_type=$(stat -f -c %T /sys/fs/cgroup)
   552      test "$cgroup_type" = "cgroup2fs"
   553  }
   554  
   555  # END   infrastructure code
   556  ###############################################################################
   557  # BEGIN sanity checks
   558  
   559  for tool in curl jq podman; do
   560      type $tool &>/dev/null || die "$ME: Required tool '$tool' not found"
   561  done
   562  
   563  # END   sanity checks
   564  ###############################################################################
   565  # BEGIN entry handler (subtest invoker)
   566  
   567  echo '============================= test session starts =============================='
   568  echo "podman client -- $(curl --version)"
   569  
   570  # Identify the tests to run. If called with args, use those as globs.
   571  tests_to_run=()
   572  if [ -n "$*" ]; then
   573      shopt -s nullglob
   574      for i; do
   575          match=(${TESTS_DIR}/*${i}*.at)
   576          if [ ${#match} -eq 0 ]; then
   577              die "No match for $TESTS_DIR/*$i*.at"
   578          fi
   579          tests_to_run+=("${match[@]}")
   580      done
   581      shopt -u nullglob
   582  else
   583      tests_to_run=($TESTS_DIR/*.at)
   584  fi
   585  echo -e "collected ${#tests_to_run[@]} items\n"
   586  
   587  start_service
   588  
   589  for i in ${tests_to_run[@]}; do
   590      TEST_CONTEXT="[$(basename $i .at)]"
   591  
   592      # Clear output from 'podman' helper
   593      >| $WORKDIR/output.log
   594  
   595      source $i
   596  done
   597  
   598  # END   entry handler
   599  ###############################################################################
   600  
   601  clean_up_server
   602  
   603  test_count=$(<$testcounter_file)
   604  failure_count=$(<$failures_file)
   605  
   606  if [ -z "$PODMAN_TESTS_KEEP_WORKDIR" ]; then
   607      rm -rf $WORKDIR
   608  fi
   609  
   610  echo "1..${test_count}"
   611  
   612  exit $failure_count