Skip to content
Merged
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
165 changes: 165 additions & 0 deletions vars/ansible.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Ansible operations for Jenkins pipelines (Docker-based)

// Get the Docker image to use for ansible commands
def _getImage() {
return 'rancher-infra-tools:latest'
}

// Run an Ansible playbook
// [ dir: string, inventory: string, playbook: string, extraVars?: Map, tags?: string, limit?: string, verbose?: bool ]
def runPlaybook(Map config) {
if (!(config.dir && config.inventory && config.playbook)) {
error 'Directory, inventory, and playbook must be provided.'
}

steps.echo "Running Ansible playbook: ${config.playbook}"

def ansibleArgs = [
"ansible-playbook",
"-i ${config.inventory}",
config.playbook
]

// Add extra variables if provided
if (config.extraVars) {
def extraVarsStr = config.extraVars.collect { k, v ->
"${k}=${v}"
}.join(' ')
ansibleArgs.add("--extra-vars \"${extraVarsStr}\"")
}

// Add tags if provided
if (config.tags) {
ansibleArgs.add("--tags ${config.tags}")
}

// Add limit if provided
if (config.limit) {
ansibleArgs.add("--limit ${config.limit}")
}

// Add verbosity if requested
if (config.verbose) {
ansibleArgs.add("-vvv")
}

// Properly escape ansible arguments to prevent command injection
def escapedAnsibleArgs = ansibleArgs.collect { arg ->
// Escape any double quotes and backslashes in the argument
arg.replace('\\', '\\\\').replace('"', '\\"')
}

def ansibleCommand = "cd ${config.dir} && ${escapedAnsibleArgs.join(' ')}"

def workspace = steps.pwd()
def globalConfig = new config()
def platform = globalConfig.getDockerPlatform()
def dockerCommand = "docker run --rm --platform ${platform} -v ${workspace}:/workspace -v ${workspace}/.ssh:/root/.ssh:ro -w /workspace ${_getImage()} sh -c \"${ansibleCommand}\""

def status = steps.sh(script: dockerCommand, returnStatus: true)

if (status != 0) {
error "Ansible playbook execution failed with status ${status}"
}

steps.echo "Ansible playbook completed successfully"
}

// Write variables to Ansible inventory group_vars
// [ path: string, content: string ]
def writeInventoryVars(Map config) {
if (!(config.path && config.content)) {
error 'Path and content must be provided.'
}

steps.echo "Writing Ansible variables to ${config.path}"

try {
steps.writeFile file: config.path, text: config.content
steps.echo "Ansible variables written successfully"
} catch (e) {
error "Failed to write Ansible variables: ${e.message}"
}
}

// Validate Ansible inventory
// [ dir: string, inventory: string ]
def validateInventory(Map config) {
if (!(config.dir && config.inventory)) {
error 'Directory and inventory must be provided.'
}

steps.echo "Validating Ansible inventory: ${config.inventory}"

// Properly escape inventory path to prevent command injection
def escapedDir = config.dir.replace('\\', '\\\\').replace('"', '\\"')
def escapedInventory = config.inventory.replace('\\', '\\\\').replace('"', '\\"')

def validateCommand = "cd ${escapedDir} && ansible-inventory -i ${escapedInventory} --list > /dev/null"

def status = steps.sh(script: validateCommand, returnStatus: true)

if (status != 0) {
steps.echo "Warning: Ansible inventory validation failed"
return false
}

steps.echo "Ansible inventory validated successfully"
return true
}

// Run ansible-playbook in a container
// [ container: string, dir: string, inventory: string, playbook: string, extraVars?: Map, tags?: string, envVars?: Map ]
def runPlaybookInContainer(Map config) {
if (!(config.container && config.dir && config.inventory && config.playbook)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another nit, if config.container is just the container name, maybe we could generate one and return it or just name it config.container_name. Looks good anyway if you don't want to change this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not understanding how this makes it better.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it doesn't, I guess this well within the subjective realm so this is only a suggestion.

My thought was that you could prefer an alternative that has less arguments.

error 'Container, directory, inventory, and playbook must be provided.'
}

steps.echo "Running Ansible playbook in container: ${config.container}"

def ansibleArgs = [
"ansible-playbook",
"-i ${config.inventory}",
config.playbook
]

if (config.extraVars) {
def extraVarsStr = config.extraVars.collect { k, v ->
"${k}=${v}"
}.join(' ')
ansibleArgs.add("--extra-vars \"${extraVarsStr}\"")
}

if (config.tags) {
ansibleArgs.add("--tags ${config.tags}")
}

def envArgs = []
if (config.envVars) {
envArgs = config.envVars.collect { k, v -> "-e ${k}=${v}" }
}

// Properly escape ansible arguments to prevent command injection
def escapedAnsibleArgs = ansibleArgs.collect { arg ->
// Escape any double quotes and backslashes in the argument
arg.replace('\\', '\\\\').replace('"', '\\"')
}

def dockerCommand = [
"docker run --rm",
envArgs.join(' '),
"-v ${config.dir}:/workspace",
"-w /workspace",
config.container,
"-c",
"\"${escapedAnsibleArgs.join(' ')}\""
].join(' ')

def status = steps.sh(script: dockerCommand, returnStatus: true)

if (status != 0) {
error "Ansible playbook in container failed with status ${status}"
}

steps.echo "Ansible playbook in container completed successfully"
}
136 changes: 136 additions & 0 deletions vars/config.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Centralized configuration management for Jenkins shared library

// Default configuration values
def getDefaultConfig() {
return [
// Docker configuration
docker: [
images: [
infraTools: env.RANCHER_INFRA_TOOLS_IMAGE ?: 'rancher-infra-tools:latest'
],
platform: env.DOCKER_PLATFORM ?: 'linux/amd64',
defaultEnvFile: env.DOCKER_DEFAULT_ENV_FILE ?: '.env'
],

// Testing configuration
testing: [
defaultTags: env.TEST_DEFAULT_TAGS ?: 'validation',
defaultTimeout: env.TEST_DEFAULT_TIMEOUT ?: '60m',
defaultResultsXML: env.TEST_DEFAULT_RESULTS_XML ?: 'results.xml',
defaultResultsJSON: env.TEST_DEFAULT_RESULTS_JSON ?: 'results.json'
],

// Naming conventions
naming: [
containerSuffix: env.CONTAINER_SUFFIX ?: 'test',
imagePrefix: env.IMAGE_PREFIX ?: 'rancher-validation-'
],

// UI/Display configuration
ui: [
colorMapName: env.COLOR_MAP_NAME ?: 'XTerm',
defaultFg: env.DEFAULT_FG_COLOR ? env.DEFAULT_FG_COLOR.toInteger() : 2,
defaultBg: env.DEFAULT_BG_COLOR ? env.DEFAULT_BG_COLOR.toInteger() : 1
],

// Path configuration
paths: [
defaultDir: env.DEFAULT_DIR ?: '.',
sshDir: env.SSH_DIR ?: '.ssh',
validationDir: env.VALIDATION_DIR ?: 'validation'
]
]
}

// Get specific configuration section
def getConfig(String section) {
def config = getDefaultConfig()
return config[section] ?: [:]
}

// Get specific configuration value with fallback
def getConfigValue(String section, String key, def defaultValue = null) {
def config = getDefaultConfig()
def sectionConfig = config[section] ?: [:]
return sectionConfig[key] ?: defaultValue
}

// Merge user configuration with defaults
def mergeConfig(Map userConfig = [:]) {
def defaultConfig = getDefaultConfig()

// Deep merge configuration
return deepMerge(defaultConfig, userConfig)
}

// Deep merge two maps recursively
def deepMerge(Map target, Map source) {
source.each { key, value ->
if (value instanceof Map && target[key] instanceof Map) {
target[key] = deepMerge(target[key], value)
} else {
target[key] = value
}
}
return target
}

// Validate configuration
def validateConfig(Map config) {
def errors = []

// Validate required Docker configuration
if (!config.docker?.images?.infraTools) {
errors.add("Docker infra tools image is required")
}

if (!config.docker?.platform) {
errors.add("Docker platform is required")
}

// Validate required testing configuration
if (!config.testing?.defaultTags) {
errors.add("Default test tags are required")
}

if (!config.testing?.defaultTimeout) {
errors.add("Default test timeout is required")
}

if (errors) {
error "Configuration validation failed: ${errors.join(', ')}"
}

return true
}

// Get Docker image name
def getDockerImage(String imageType = 'infraTools') {
def dockerConfig = getConfig('docker')
return dockerConfig.images[imageType]
}

// Get Docker platform
def getDockerPlatform() {
return getConfigValue('docker', 'platform')
}

// Get default test configuration
def getTestConfig() {
return getConfig('testing')
}

// Get naming configuration
def getNamingConfig() {
return getConfig('naming')
}

// Get UI configuration
def getUIConfig() {
return getConfig('ui')
}

// Get path configuration
def getPathConfig() {
return getConfig('paths')
}
20 changes: 15 additions & 5 deletions vars/container.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ def _containerCommand(Map container) {
}

if ( !(container?.envFile) ) {
container.envFile = '.env'
def globalConfig = new config()
def dockerConfig = globalConfig.getConfig('docker')
container.envFile = dockerConfig.defaultEnvFile
}

args.addAll(['--env-file', container.envFile])
Expand Down Expand Up @@ -232,29 +234,37 @@ def _goTestCommand(Map test) {
args.add("--packages=${test.packages}")

if (!(test?.resultsXML)) {
test.resultsXML = 'results.xml'
def globalConfig = new config()
def testConfig = globalConfig.getConfig('testing')
test.resultsXML = testConfig.defaultResultsXML
}

args.addAll(['--junitfile', test.resultsXML])

if (!(test?.resultsJSON)) {
test.resultsJSON = 'results.json'
def globalConfig = new config()
def testConfig = globalConfig.getConfig('testing')
test.resultsJSON = testConfig.defaultResultsJSON
}

args.addAll(['--jsonfile', test.resultsJSON])

args.add('--')

if (!(test?.tags)) {
test.tags = 'validation'
def globalConfig = new config()
def testConfig = globalConfig.getConfig('testing')
test.tags = testConfig.defaultTags
}

args.add("-tags=${test.tags}")

args.add(test.cases)

if (!(test?.timeout)) {
test.timeout = '60m'
def globalConfig = new config()
def testConfig = globalConfig.getConfig('testing')
test.timeout = testConfig.defaultTimeout
}

args.add("-timeout=${test.timeout}")
Expand Down
17 changes: 3 additions & 14 deletions vars/generate.groovy
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
// suffix string
def names(String suffix = 'test') {
def jobName = env.JOB_NAME
def buildNumber = env.BUILD_NUMBER

if (jobName.contains('/')) {
def jobNames = jobName.split('/')

jobName = jobNames[jobNames.size() - 1]
}

def containerName = "${jobName}${buildNumber}_test"
def imageName = "rancher-validation-${jobName}${buildNumber}"

return [container: containerName, image: imageName]
def naming = new naming()
return naming.generateNames([suffix: suffix])
}

//Below enables name generation with Integer count param
//requires docker's build.sh changes for name, right now it's not an ENV
Expand All @@ -33,4 +23,3 @@ def names(String suffix = 'test') {
// }
//
// return resourceNames
}
Loading