Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist/
build/
__pycache__/
*.pyc
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
__pycache__/
*.py[oc]
build/
dist/
# dist/ - Keep this for binary artifacts
wheels/
*.egg-info

# Virtual environments
.venv

### Intellij+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360

.idea/
version_info.txt
48 changes: 48 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# ----------------------------
# Stage 1: Builder
# ----------------------------
FROM python:3.12-slim AS builder

WORKDIR /app

# Install system dependencies needed for PyInstaller
RUN apt-get update && apt-get install -y --no-install-recommends \
binutils \
gcc \
libc6-dev \
make \
&& rm -rf /var/lib/apt/lists/*

# Copy pyproject.toml and source code
COPY pyproject.toml .
COPY uv.lock .
COPY sv_mcp/ ./sv_mcp

# Install your project and its dependencies
RUN pip install --no-cache-dir . \
&& pip install --no-cache-dir pyinstaller tomli pdm

RUN pdm install --prod --no-self

# Run build.py
WORKDIR /app/sv_mcp
RUN python build.py
# ----------------------------
# Stage 2: Final
# ----------------------------
FROM python:3.12-slim AS runtime

ENV MCP_DOCKER=true

WORKDIR /app

# Copy the statically named binary
COPY --from=builder /app/sv_mcp/dist/bzm-mcp-linux /usr/local/bin/bzm-mcp
RUN chmod +x /usr/local/bin/bzm-mcp

# Run as non-root user
RUN groupadd -r bzm-mcp && useradd -r -g bzm-mcp bzm-mcp
USER bzm-mcp

ENTRYPOINT ["bzm-mcp"]
CMD []
177 changes: 177 additions & 0 deletions Jenkinsfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
clearWorkspaceAsRoot()

@Library('jenkins_library') _

pipeline {
agent {
kubernetes {
yaml agentYaml()
defaultContainer 'jenkins-docker-agent'
}
}

parameters {
booleanParam(name: 'PERFORM_PRISMA_SCAN', defaultValue: true, description: 'Perform a Prisma scan for the Docker image')
booleanParam(name: 'PERFORM_WHITESOURCE_SCAN', defaultValue: true, description: 'Perform a WhiteSource scan for the code')
}

options {
buildDiscarder(logRotator(numToKeepStr: '100', daysToKeepStr: '45'))
ansiColor('xterm')
timestamps()
disableConcurrentBuilds()
}

stages {
stage('Setup') {
steps {
script {
currentBuild.displayName = "#${env.BUILD_NUMBER}"
}
}
}

stage('Build') {
steps {
script {
sh "pip install build --break-system-packages"
sh "python -m build --sdist"
sh "pip install . --break-system-packages"
}
}
}

stage('Test') {
steps {
sh "pip install . --break-system-packages"
sh "PYTHONPATH=. pytest --junitxml=reports/junit-report.xml"
junit allowEmptyResults: true, testResults: 'reports/junit-report.xml', skipPublishingChecks: true, skipMarkingBuildUnstable: true
}
}

stage('Build Docker Image') {
steps {
script {
// Set image repository and name
env.IMAGE_REPO = "us-docker.pkg.dev/verdant-bulwark-278/sv-mcp"
env.IMAGE_NAME = "sv-mcp"

def sanitisedBranch = env.BRANCH_NAME.replaceAll("/", "-").replaceAll("[^a-zA-Z0-9\\-_]+", "")
env.IMAGE_TAG = "${sanitisedBranch}-${env.BUILD_NUMBER}"

// Authenticate with GCR/Artifact Registry
dockerLoginGCR('GoogleCredForJenkins2')
sh "gcloud auth configure-docker us-docker.pkg.dev --quiet"

// Generate tags
def tags = [
"${env.IMAGE_REPO}/${env.IMAGE_NAME}:${env.IMAGE_TAG}",
"${env.IMAGE_REPO}/${env.IMAGE_NAME}:latest-${sanitisedBranch}"
]
if (env.BRANCH_NAME == 'master') {
tags.add("${env.IMAGE_REPO}/${env.IMAGE_NAME}:latest-master")
} else if (env.BRANCH_NAME.contains('release')) {
tags.add("${env.IMAGE_REPO}/${env.IMAGE_NAME}:latest-release")
}

// Build Docker image using standard Docker
def tagArgs = tags.collect { "-t ${it}" }.join(' ')
sh """
docker build ${tagArgs} \
--build-arg BUILD_NUMBER=${env.BUILD_NUMBER} \
--build-arg BRANCH_NAME=${env.BRANCH_NAME} \
--build-arg BUILD_TIME=${currentBuild.startTimeInMillis} \
--build-arg COMMIT_HASH=${env.GIT_COMMIT} \
-f Dockerfile .
"""

// Push all tags
tags.each { tag ->
sh "docker push ${tag}"
}

// Store image details for scans
env.DOCKER_IMAGE = "${env.IMAGE_REPO}/${env.IMAGE_NAME}:${env.IMAGE_TAG}"
}
}
}

stage('WhiteSource Scan') {
when { expression { params.PERFORM_WHITESOURCE_SCAN } }
steps {
script {
whiteSourceScan("Virtual-Services-MCP", env.BRANCH_NAME)
}
}
}

stage('PrismaCloud Scan') {
when { expression { params.PERFORM_PRISMA_SCAN } }
steps {
script {
prismaCloudScanImage(
dockerAddress: 'unix:///var/run/docker.sock',
image: "${env.DOCKER_IMAGE}",
logLevel: 'info',
resultsFile: 'prisma-cloud-scan-results.json',
ignoreImageBuildTime: true
)

sh '''
if [ -f prisma-cloud-scan-results.json ]; then
chmod 644 prisma-cloud-scan-results.json
ls -lah prisma-cloud-scan-results.json
else
echo "Results file not found"
fi
'''
prismaCloudPublish(resultsFilePattern: 'prisma-cloud-scan-results.json')
}
}
}
}

post {
always {
script {
// Clean up Docker images
sh """
docker rmi ${env.DOCKER_IMAGE} || true
docker system prune -f || true
"""
if (params.PERFORM_PRISMA_SCAN && fileExists('prisma-cloud-scan-results.json')) {
try {
// Wait a moment to ensure file is fully written
sleep(time: 2, unit: 'SECONDS')
archiveArtifacts artifacts: 'prisma-cloud-scan-results.json', allowEmptyArchive: true
echo "Prisma Cloud scan results archived successfully"
} catch (Exception e) {
echo "Failed to archive Prisma scan results: ${e.message}"
}
} else {
echo "Prisma Cloud scan not performed or results file not found"
}
}
cleanWs()
}
success {
script {
echo "Build succeeded"
// Send Slack notification on success
slackSend(channel: "@" + getBuildUserSlackIdMB(), message: "SUCCESS <${BUILD_URL} | *${JOB_NAME}*>.", color: "#00ff00")
slackSend(channel: "#bm-notifications-jenkins", message: "SUCCESS <${BUILD_URL} | *${JOB_NAME}*>.", color: "#00ff00")
}
}
failure {
script {
// Send Slack notification if the pipeline fails
def errorMessage = currentBuild.description ?: "Unknown error"
slackSend(channel: "@" + getBuildUserSlackIdMB(), message: "FAILED <${BUILD_URL} | *${JOB_NAME}*>. Error: ${errorMessage}", color: "#ff0000")
slackSend(channel: "#bm-alerts-blazemeter", message: "FAILED <${BUILD_URL} | *${JOB_NAME}*>. Error: ${errorMessage}", color: "#ff0000")

// Send email notification
notifyJobFailureEmailToAuthor(sender: 'jenkins@blazemeter.com')
}
}
}
}
Loading