How do I solve the 'pytest not found command' error in Jenkins

I have a Jenkins pipeline for a FastAPI and when I run the pipeline using agent any I am getting an error : pytest: not found.

This is the Jenkinsfile :

pipeline {
 agent any  
triggers {
    githubPush()
}

stages {
    
    stage('Setup'){
       steps{
        dir('.'){
            sh 'python3.8 -m venv ./venv'
        }            
        }
     }    


stage('Unit Tests'){ 
           steps{
             dir('.') {
                 sh '. ./venv/bin/activate'
                 sh 'pip install -r requirements.txt'
                 sh 'pytest -v --junitxml=docs/unit-tests/htmlcoverage/coverage.xml --cov-report xml --cov app.main'
             }           
            }                    
         }                
       stage('Publish Test Report'){ 
           steps{
              cobertura autoUpdateHealth: false, autoUpdateStability: false, coberturaReportFile: 'coverage*.xml', conditionalCoverageTargets: '70, 0, 0', failUnhealthy: false, failUnstable: false, lineCoverageTargets: '80, 0, 0', maxNumberOfBuilds: 0, methodCoverageTargets: '80, 0, 0', onlyStable: false, sourceEncoding: 'ASCII', zoomCoverageChart: false
              archiveArtifacts artifacts: 'docs/unit-tests/htmlcoverage/*.*'
            }                    
         }        

      }
}
 

However when I run the pipeline using python docker agent as below pytest executes successfully :

pipeline {
  agent {
  docker {
    image 'python:3.9-slim'
    args '-u root --privileged'
  }
} 
triggers {
    githubPush()
}

stages {     
    stage('Setup'){
       steps{
           sh 'pip install -r requirements.txt'  
        }
     }
      


stage('Unit Tests'){ 
           steps{
               sh 'pytest -v --junitxml=docs/unit-tests/htmlcoverage/coverage.xml --cov-report xml --cov app.main'
            }                    
         }                              
       ...
       ...  
      }
}
 

I have switched from using python docker agent to agent any because I cant find a solution yet of running the docker agent as non-root (the agent throws a bunch of errors when running as non-root).

What am I missing ?

I expected that pytest should execute as I have successfully setup the virtual environment.

The Jenkins console log also shows that pytest was indeed installed :

Started by user gold
Obtained Jenkinsfile from git https://github.com/edtshuma/devsecops-labs.git
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /var/lib/jenkins/workspace/Python-DevSecOps
...
Running in /var/lib/jenkins/workspace/Python-DevSecOps
[Pipeline] {
[Pipeline] sh
+ python3.8 -m venv ./venv
...
Running in /var/lib/jenkins/workspace/Python-DevSecOps
[Pipeline] {
[Pipeline] sh
+ . ./venv/bin/activate
+ deactivate nondestructive
+ [ -n  ]
+ [ -n  ]
+ [ -n  -o -n  ]
+ [ -n  ]
+ unset VIRTUAL_ENV
+ [ ! nondestructive = nondestructive ]
+ VIRTUAL_ENV=/var/lib/jenkins/workspace/Python-DevSecOps/venv
+ export VIRTUAL_ENV
+ _OLD_VIRTUAL_PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
+ PATH=/var/lib/jenkins/workspace/Python-DevSecOps/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
+ export PATH
+ [ -n  ]
+ [ -z  ]
+ _OLD_VIRTUAL_PS1=$ 
+ [ x(venv)  != x ]
+ PS1=(venv) $ 
+ export PS1
+ [ -n  -o -n  ]
[Pipeline] sh
+ pip install -r requirements.txt
...
Requirement already satisfied: pydantic==1.10.4 in /var/lib/jenkins/.local/lib/python3.8/site-packages (from -r requirements.txt (line 26)) (1.10.4)
Requirement already satisfied: pytest==7.2.1 in /var/lib/jenkins/.local/lib/python3.8/site-packages (from -r requirements.txt (line 27)) (7.2.1)
Requirement already satisfied: pytest-cov==4.0.0 in /var/lib/jenkins/.local/lib/python3.8/site-packages (from -r requirements.txt (line 28)) (4.0.0)
...
[Pipeline] sh
+ pytest -v --junitxml=docs/unit-tests/htmlcoverage/coverage.xml --cov-report xml --cov app.main
/var/lib/jenkins/workspace/Python-DevSecOps@tmp/durable-cd259706/script.sh: 1: pytest: not found

UPDATE Using the full path is not working either :

stage('Unit Tests'){ 
       steps{
         dir('.') {
             sh '. ./venv/bin/activate'
             sh '/var/lib/jenkins/workspace/Python-DevSecOps/venv/bin pip install -r requirements.txt'
             sh '/var/lib/jenkins/workspace/Python-DevSecOps/venv/bin pytest -v --junitxml=docs/unit-tests/htmlcoverage/coverage.xml --cov-report xml --cov app.main'
         }           
        }                    
     }   

Gives error :

+ ../env/bin pytest -v --junitxml=docs/unit-tests/htmlcoverage/coverage.xml --cov-report xml --cov app.main

/var/lib/jenkins/workspace/Python-DevSecOps@tmp/durable-0925a56a/script.sh: 1: …/env/bin: not found

Hi @golide
try using this plugin Pyenv Pipeline

put the 3 separate sh steps into a single step

sh '''. ./venv/bin/activate
             /var/lib/jenkins/workspace/Python-DevSecOps/venv/bin pip install -r requirements.txt
             /var/lib/jenkins/workspace/Python-DevSecOps/venv/bin pytest -v --junitxml=docs/unit-tests/htmlcoverage/coverage.xml --cov-report xml --cov app.main
'''

When calling . ./venv/bin/activate this will basically just set some env variable which are lost when the sh step terminates. So they are not available in the next sh step

1 Like

I have updated the Jenkinsfile and installed PyEnv :

stages {
        
        stage('Setup'){
           steps{
           withPythonEnv('/usr/bin/python3.8') {
                sh 'echo "Job is starting" '
            }            
            }
         }    
 stage('Unit Tests'){ 
           steps{
               withPythonEnv('/usr/bin/python3.8') {
                 sh '''pip install -r requirements.txt
                       sudo chown -R jenkins:jenkins ./docs/unit-tests/htmlcoverage
                       pytest -v --junitxml=docs/unit-tests/htmlcoverage/coverage.xml --cov-report xml --cov app.main
                '''
             }           
            }                    
         }     

I have added the line sudo chown -R jenkins:jenkins ./docs/unit-tests/htmlcoverage because I am facing a permissions error to the coverage file :

INTERNALERROR> PermissionError: [Errno 13] Permission denied: 'coverage.xml'

I have also verified that coverage.xml is under root user and not the regular Jenkins (What even causes this ?) :

To fix this I amended the jenkins user :

jenkins ALL= NOPASSWD: ALL

and also did a restart of the Jenkins service.

I have run the pipeline again but it seems I cannot still sudo :

....
Requirement already satisfied: websockets==10.4 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 43)) (10.4)
+ sudo chown -R jenkins:jenkins ./docs/unit-tests/htmlcoverage
sudo: a terminal is required to read the password; either use the -S option to read from standard input or configure an askpass helper
....

Hi @golide
this is the one you tried?
echo “jenkins ALL=(ALL) NOPASSWD: ALL” >> /etc/sudoers

@Jana Same error still :

It seems something is still wrong with my permissions. I am attaching the full log here :

Started by user gold
Obtained Jenkinsfile from git https://github.com/edtshuma/devsecops-labs.git
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /var/lib/jenkins/workspace/Python-DevSecOps
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Declarative: Checkout SCM)
[Pipeline] checkout
Selected Git installation does not exist. Using Default
The recommended git tool is: NONE
using credential github-credentials
 > git rev-parse --resolve-git-dir /var/lib/jenkins/workspace/Python-DevSecOps/.git # timeout=10
Fetching changes from the remote Git repository
 > git config remote.origin.url https://github.com/edtshuma/devsecops-labs.git # timeout=10
Fetching upstream changes from https://github.com/edtshuma/devsecops-labs.git
 > git --version # timeout=10
 > git --version # 'git version 2.25.1'
using GIT_ASKPASS to set credentials Credentials for GitHub
 > git fetch --tags --force --progress -- https://github.com/edtshuma/devsecops-labs.git +refs/heads/*:refs/remotes/origin/* # timeout=10
 > git rev-parse refs/remotes/origin/python^{commit} # timeout=10
Checking out Revision 242b61e1ba550b656c407dfd66fb922b6255e523 (refs/remotes/origin/python)
 > git config core.sparsecheckout # timeout=10
 > git checkout -f 242b61e1ba550b656c407dfd66fb922b6255e523 # timeout=10
Commit message: "sudo chown"
 > git rev-list --no-walk 242b61e1ba550b656c407dfd66fb922b6255e523 # timeout=10
[Pipeline] }
[Pipeline] // stage
[Pipeline] withEnv
[Pipeline] {
[Pipeline] withEnv
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Setup)
[Pipeline] withPythonEnv
[Pipeline] {
[Pipeline] sh
+ echo Job is starting
Job is starting
[Pipeline] }
[Pipeline] // withPythonEnv
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Unit Tests)
[Pipeline] withPythonEnv
[Pipeline] {
[Pipeline] sh
+ pip install -r requirements.txt
Requirement already satisfied: anyio==3.6.2 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 1)) (3.6.2)
Requirement already satisfied: attrs==22.2.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 2)) (22.2.0)
Requirement already satisfied: certifi==2022.12.7 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 3)) (2022.12.7)
Requirement already satisfied: charset-normalizer==3.0.1 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 4)) (3.0.1)
Requirement already satisfied: click==8.1.3 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 5)) (8.1.3)
Requirement already satisfied: colorama==0.4.6 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 6)) (0.4.6)
Requirement already satisfied: coverage==7.1.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 7)) (7.1.0)
Requirement already satisfied: dnspython==2.3.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 8)) (2.3.0)
Requirement already satisfied: email-validator==1.3.1 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 9)) (1.3.1)
Requirement already satisfied: et-xmlfile==1.1.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 10)) (1.1.0)
Requirement already satisfied: exceptiongroup==1.1.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 11)) (1.1.0)
Requirement already satisfied: fastapi==0.92.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 12)) (0.92.0)
Requirement already satisfied: h11==0.14.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 13)) (0.14.0)
Requirement already satisfied: httpcore==0.16.3 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 14)) (0.16.3)
Requirement already satisfied: httptools==0.5.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 15)) (0.5.0)
Requirement already satisfied: httpx==0.23.3 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 16)) (0.23.3)
Requirement already satisfied: idna==3.4 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 17)) (3.4)
Requirement already satisfied: iniconfig==2.0.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 18)) (2.0.0)
Requirement already satisfied: itsdangerous==2.1.2 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 19)) (2.1.2)
Requirement already satisfied: Jinja2==3.1.2 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 20)) (3.1.2)
Requirement already satisfied: MarkupSafe==2.1.2 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 21)) (2.1.2)
Requirement already satisfied: openpyxl==3.0.10 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 22)) (3.0.10)
Requirement already satisfied: orjson==3.8.5 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 23)) (3.8.5)
Requirement already satisfied: packaging==23.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 24)) (23.0)
Requirement already satisfied: pluggy==1.0.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 25)) (1.0.0)
Requirement already satisfied: pydantic==1.10.4 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 26)) (1.10.4)
Requirement already satisfied: pytest==7.2.1 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 27)) (7.2.1)
Requirement already satisfied: pytest-cov==4.0.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 28)) (4.0.0)
Requirement already satisfied: python-dotenv==0.21.1 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 29)) (0.21.1)
Requirement already satisfied: python-multipart==0.0.5 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 30)) (0.0.5)
Requirement already satisfied: PyYAML==6.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 31)) (6.0)
Requirement already satisfied: requests==2.28.2 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 32)) (2.28.2)
Requirement already satisfied: rfc3986==1.5.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 33)) (1.5.0)
Requirement already satisfied: six==1.16.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 34)) (1.16.0)
Requirement already satisfied: sniffio==1.3.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 35)) (1.3.0)
Requirement already satisfied: starlette==0.25.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 36)) (0.25.0)
Requirement already satisfied: tomli==2.0.1 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 37)) (2.0.1)
Requirement already satisfied: typing-extensions==4.4.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 38)) (4.4.0)
Requirement already satisfied: ujson==5.7.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 39)) (5.7.0)
Requirement already satisfied: urllib3==1.26.14 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 40)) (1.26.14)
Requirement already satisfied: uvicorn==0.20.0 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 41)) (0.20.0)
Requirement already satisfied: watchfiles==0.18.1 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 42)) (0.18.1)
Requirement already satisfied: websockets==10.4 in ./.pyenv-usr-bin-python3.8/lib/python3.8/site-packages (from -r requirements.txt (line 43)) (10.4)
+ sudo chown -R jenkins:jenkins ./docs/unit-tests/htmlcoverage
sudo: a terminal is required to read the password; either use the -S option to read from standard input or configure an askpass helper
[Pipeline] }
[Pipeline] // withPythonEnv
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Publish Test Report)
Stage "Publish Test Report" skipped due to earlier failure(s)
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
ERROR: script returned exit code 1
Finished: FAILURE

And the sudoers file :

And the pipeline stage that is failing :

stage('Unit Tests'){ 
           steps{
               withPythonEnv('/usr/bin/python3.8') {
                 sh '''pip install -r requirements.txt
                       sudo chown -R jenkins:jenkins ./docs/unit-tests/htmlcoverage
                       pytest -v --junitxml=docs/unit-tests/htmlcoverage/coverage.xml --cov-report xml --cov app.main
                '''
             }           
            }                    
         }                

For anyone struggling with this - the problem was with rules precedence inside /etc/sudoers.

By default if there are multiple entries for a user inside of the /etc/sudoers file, sudo uses the last rule that applies

The change to jenkins user was being overridden because of the rule’s position in /etc/sudoers :

WRONG
image

It is wrong because jenkins user is already a member of sudo group, and thus the line jenkins ALL=(ALL) NOPASSWD: ALL will be overriden by the line

sudo ALL=(ALL:ALL) ALL

CORRECT