Semaphore/services/tasks/LocalJob.go
2024-10-04 15:08:59 -04:00

600 lines
14 KiB
Go

package tasks
import (
"encoding/json"
"fmt"
"maps"
"os"
"path"
"strconv"
"github.com/ansible-semaphore/semaphore/db"
"github.com/ansible-semaphore/semaphore/db_lib"
"github.com/ansible-semaphore/semaphore/pkg/task_logger"
"github.com/ansible-semaphore/semaphore/util"
)
type LocalJob struct {
// Received constant fields
Task db.Task
Template db.Template
Inventory db.Inventory
Repository db.Repository
Environment db.Environment
Secret string
Logger task_logger.Logger
App db_lib.LocalApp
// Internal field
Process *os.Process
sshKeyInstallation db.AccessKeyInstallation
becomeKeyInstallation db.AccessKeyInstallation
vaultFileInstallations map[string]db.AccessKeyInstallation
}
func (t *LocalJob) Kill() {
if t.Process == nil {
return
}
err := t.Process.Kill()
if err != nil {
t.Log(err.Error())
}
}
func (t *LocalJob) Log(msg string) {
t.Logger.Log(msg)
}
func (t *LocalJob) SetStatus(status task_logger.TaskStatus) {
t.Logger.SetStatus(status)
}
func (t *LocalJob) getEnvironmentExtraVars(username string, incomingVersion *string) (extraVars map[string]interface{}, err error) {
extraVars = make(map[string]interface{})
if t.Environment.JSON != "" {
err = json.Unmarshal([]byte(t.Environment.JSON), &extraVars)
if err != nil {
return
}
}
taskDetails := make(map[string]interface{})
taskDetails["id"] = t.Task.ID
if t.Task.Message != "" {
taskDetails["message"] = t.Task.Message
}
taskDetails["username"] = username
taskDetails["url"] = t.Task.GetUrl()
if t.Template.Type != db.TemplateTask {
taskDetails["type"] = t.Template.Type
if incomingVersion != nil {
taskDetails["incoming_version"] = incomingVersion
}
if t.Template.Type == db.TemplateBuild {
taskDetails["target_version"] = t.Task.Version
}
}
vars := make(map[string]interface{})
vars["task_details"] = taskDetails
extraVars["semaphore_vars"] = vars
return
}
func (t *LocalJob) getEnvironmentExtraVarsJSON(username string, incomingVersion *string) (str string, err error) {
extraVars := make(map[string]interface{})
extraSecretVars := make(map[string]interface{})
if t.Environment.JSON != "" {
err = json.Unmarshal([]byte(t.Environment.JSON), &extraVars)
if err != nil {
return
}
}
if t.Secret != "" {
err = json.Unmarshal([]byte(t.Secret), &extraSecretVars)
if err != nil {
return
}
}
t.Secret = "{}"
maps.Copy(extraVars, extraSecretVars)
taskDetails := make(map[string]interface{})
taskDetails["id"] = t.Task.ID
if t.Task.Message != "" {
taskDetails["message"] = t.Task.Message
}
taskDetails["username"] = username
taskDetails["url"] = t.Task.GetUrl()
if t.Template.Type != db.TemplateTask {
taskDetails["type"] = t.Template.Type
if incomingVersion != nil {
taskDetails["incoming_version"] = incomingVersion
}
if t.Template.Type == db.TemplateBuild {
taskDetails["target_version"] = t.Task.Version
}
}
vars := make(map[string]interface{})
vars["task_details"] = taskDetails
extraVars["semaphore_vars"] = vars
ev, err := json.Marshal(extraVars)
if err != nil {
return
}
str = string(ev)
return
}
func (t *LocalJob) getEnvironmentENV() (res []string, err error) {
environmentVars := make(map[string]string)
if t.Environment.ENV != nil {
err = json.Unmarshal([]byte(*t.Environment.ENV), &environmentVars)
if err != nil {
return
}
}
for key, val := range environmentVars {
res = append(res, fmt.Sprintf("%s=%s", key, val))
}
for _, secret := range t.Environment.Secrets {
if secret.Type != db.EnvironmentSecretEnv {
continue
}
res = append(res, fmt.Sprintf("%s=%s", secret.Name, secret.Secret))
}
return
}
// nolint: gocyclo
func (t *LocalJob) getShellArgs(username string, incomingVersion *string) (args []string, err error) {
extraVars, err := t.getEnvironmentExtraVars(username, incomingVersion)
if err != nil {
t.Log(err.Error())
t.Log("Error getting environment extra vars")
return
}
var templateExtraArgs []string
if t.Template.Arguments != nil {
err = json.Unmarshal([]byte(*t.Template.Arguments), &templateExtraArgs)
if err != nil {
t.Log("Invalid format of the template extra arguments, must be valid JSON")
return
}
}
var taskExtraArgs []string
if t.Template.AllowOverrideArgsInTask && t.Task.Arguments != nil {
err = json.Unmarshal([]byte(*t.Task.Arguments), &taskExtraArgs)
if err != nil {
t.Log("Invalid format of the TaskRunner extra arguments, must be valid JSON")
return
}
}
// Script to run
args = append(args, t.Template.Playbook)
// Include Environment Secret Vars
for _, secret := range t.Environment.Secrets {
if secret.Type == db.EnvironmentSecretVar {
args = append(args, fmt.Sprintf("%s=%s", secret.Name, secret.Secret))
}
}
// Include extra args from template
args = append(args, templateExtraArgs...)
// Include ExtraVars and Survey Vars
for name, value := range extraVars {
if name != "semaphore_vars" {
args = append(args, fmt.Sprintf("%s=%s", name, value))
}
}
// Include extra args from task
args = append(args, taskExtraArgs...)
return
}
// nolint: gocyclo
func (t *LocalJob) getTerraformArgs(username string, incomingVersion *string) (args []string, err error) {
args = []string{}
extraVars, err := t.getEnvironmentExtraVars(username, incomingVersion)
if err != nil {
t.Log(err.Error())
t.Log("Could not remove command environment, if existent it will be passed to --extra-vars. This is not fatal but be aware of side effects")
return
}
for name, value := range extraVars {
if name == "semaphore_vars" {
continue
}
args = append(args, "-var", fmt.Sprintf("%s=%s", name, value))
}
for _, secret := range t.Environment.Secrets {
if secret.Type != db.EnvironmentSecretVar {
continue
}
args = append(args, "-var", fmt.Sprintf("%s=%s", secret.Name, secret.Secret))
}
return
}
// nolint: gocyclo
func (t *LocalJob) getPlaybookArgs(username string, incomingVersion *string) (args []string, inputs map[string]string, err error) {
inputMap := make(map[db.AccessKeyRole]string)
inputs = make(map[string]string)
playbookName := t.Task.Playbook
if playbookName == "" {
playbookName = t.Template.Playbook
}
var inventoryFilename string
switch t.Inventory.Type {
case db.InventoryFile:
if t.Inventory.RepositoryID == nil {
inventoryFilename = t.Inventory.GetFilename()
} else {
inventoryFilename = path.Join(t.tmpInventoryFullPath(), t.Inventory.GetFilename())
}
case db.InventoryStatic, db.InventoryStaticYaml:
inventoryFilename = t.tmpInventoryFullPath()
default:
err = fmt.Errorf("invalid inventory type")
return
}
args = []string{
"-i", inventoryFilename,
}
if t.Inventory.SSHKeyID != nil {
switch t.Inventory.SSHKey.Type {
case db.AccessKeySSH:
if t.sshKeyInstallation.Login != "" {
args = append(args, "--user", t.sshKeyInstallation.Login)
}
case db.AccessKeyLoginPassword:
if t.sshKeyInstallation.Login != "" {
args = append(args, "--user", t.sshKeyInstallation.Login)
}
if t.sshKeyInstallation.Password != "" {
args = append(args, "--ask-pass")
inputMap[db.AccessKeyRoleAnsibleUser] = t.sshKeyInstallation.Password
}
case db.AccessKeyNone:
default:
err = fmt.Errorf("access key does not suite for inventory's user credentials")
return
}
}
if t.Inventory.BecomeKeyID != nil {
switch t.Inventory.BecomeKey.Type {
case db.AccessKeyLoginPassword:
if t.becomeKeyInstallation.Login != "" {
args = append(args, "--become-user", t.becomeKeyInstallation.Login)
}
if t.becomeKeyInstallation.Password != "" {
args = append(args, "--ask-become-pass")
inputMap[db.AccessKeyRoleAnsibleBecomeUser] = t.becomeKeyInstallation.Password
}
case db.AccessKeyNone:
default:
err = fmt.Errorf("access key does not suite for inventory's sudo user credentials")
return
}
}
if t.Task.Debug {
args = append(args, "-vvvv")
}
if t.Task.Diff {
args = append(args, "--diff")
}
if t.Task.DryRun {
args = append(args, "--check")
}
for name, install := range t.vaultFileInstallations {
if install.Password != "" {
args = append(args, fmt.Sprintf("--vault-id=%s@prompt", name))
inputs[fmt.Sprintf("Vault password (%s):", name)] = install.Password
}
}
extraVars, err := t.getEnvironmentExtraVarsJSON(username, incomingVersion)
if err != nil {
t.Log(err.Error())
t.Log("Could not remove command environment, if existent it will be passed to --extra-vars. This is not fatal but be aware of side effects")
} else if extraVars != "" {
args = append(args, "--extra-vars", extraVars)
}
for _, secret := range t.Environment.Secrets {
if secret.Type != db.EnvironmentSecretVar {
continue
}
args = append(args, "--extra-vars", fmt.Sprintf("%s=%s", secret.Name, secret.Secret))
}
var templateExtraArgs []string
if t.Template.Arguments != nil {
err = json.Unmarshal([]byte(*t.Template.Arguments), &templateExtraArgs)
if err != nil {
t.Log("Invalid format of the template extra arguments, must be valid JSON")
return
}
}
var taskExtraArgs []string
if t.Template.AllowOverrideArgsInTask && t.Task.Arguments != nil {
err = json.Unmarshal([]byte(*t.Task.Arguments), &taskExtraArgs)
if err != nil {
t.Log("Invalid format of the TaskRunner extra arguments, must be valid JSON")
return
}
}
if t.Task.Limit != "" {
t.Log("--limit=" + t.Task.Limit)
taskExtraArgs = append(taskExtraArgs, "--limit="+t.Task.Limit)
}
args = append(args, templateExtraArgs...)
args = append(args, taskExtraArgs...)
args = append(args, playbookName)
if line, ok := inputMap[db.AccessKeyRoleAnsibleUser]; ok {
inputs["SSH password:"] = line
}
if line, ok := inputMap[db.AccessKeyRoleAnsibleBecomeUser]; ok {
inputs["BECOME password"] = line
}
return
}
func (t *LocalJob) Run(username string, incomingVersion *string) (err error) {
t.SetStatus(task_logger.TaskRunningStatus) // It is required for local mode. Don't delete
err = t.prepareRun()
if err != nil {
return err
}
defer func() {
t.destroyKeys()
t.destroyInventoryFile()
}()
var args []string
var inputs map[string]string
switch t.Template.App {
case db.AppAnsible:
args, inputs, err = t.getPlaybookArgs(username, incomingVersion)
case db.AppTerraform, db.AppTofu:
args, err = t.getTerraformArgs(username, incomingVersion)
default:
args, err = t.getShellArgs(username, incomingVersion)
}
if err != nil {
return
}
environmentVariables, err := t.getEnvironmentENV()
if err != nil {
return
}
if t.Inventory.SSHKey.Type == db.AccessKeySSH && t.Inventory.SSHKeyID != nil {
environmentVariables = append(environmentVariables, fmt.Sprintf("SSH_AUTH_SOCK=%s", t.sshKeyInstallation.SSHAgent.SocketFile))
}
if t.Template.Type != db.TemplateTask {
environmentVariables = append(environmentVariables, fmt.Sprintf("SEMAPHORE_TASK_TYPE=%s", t.Template.Type))
if incomingVersion != nil {
environmentVariables = append(
environmentVariables,
fmt.Sprintf("SEMAPHORE_TASK_INCOMING_VERSION=%s", *incomingVersion))
}
if t.Template.Type == db.TemplateBuild && t.Task.Version != nil {
environmentVariables = append(
environmentVariables,
fmt.Sprintf("SEMAPHORE_TASK_TARGET_VERSION=%s", *t.Task.Version))
}
}
return t.App.Run(args, &environmentVariables, inputs, func(p *os.Process) {
t.Process = p
})
}
func (t *LocalJob) prepareRun() error {
t.Log("Preparing: " + strconv.Itoa(t.Task.ID))
if err := checkTmpDir(util.Config.TmpPath); err != nil {
t.Log("Creating tmp dir failed: " + err.Error())
return err
}
if t.Repository.GetType() == db.RepositoryLocal {
if _, err := os.Stat(t.Repository.GitURL); err != nil {
t.Log("Failed in finding static repository at " + t.Repository.GitURL + ": " + err.Error())
return err
}
} else {
if err := t.updateRepository(); err != nil {
t.Log("Failed updating repository: " + err.Error())
return err
}
if err := t.checkoutRepository(); err != nil {
t.Log("Failed to checkout repository to required commit: " + err.Error())
return err
}
}
if err := t.installInventory(); err != nil {
t.Log("Failed to install inventory: " + err.Error())
return err
}
if err := t.App.InstallRequirements(); err != nil {
t.Log("Running galaxy failed: " + err.Error())
return err
}
if err := t.installVaultKeyFiles(); err != nil {
t.Log("Failed to install vault password files: " + err.Error())
return err
}
return nil
}
func (t *LocalJob) updateRepository() error {
repo := db_lib.GitRepository{
Logger: t.Logger,
TemplateID: t.Template.ID,
Repository: t.Repository,
Client: db_lib.CreateDefaultGitClient(),
}
err := repo.ValidateRepo()
if err != nil {
if !os.IsNotExist(err) {
err = os.RemoveAll(repo.GetFullPath())
if err != nil {
return err
}
}
return repo.Clone()
}
if repo.CanBePulled() {
err = repo.Pull()
if err == nil {
return nil
}
}
err = os.RemoveAll(repo.GetFullPath())
if err != nil {
return err
}
return repo.Clone()
}
func (t *LocalJob) checkoutRepository() error {
repo := db_lib.GitRepository{
Logger: t.Logger,
TemplateID: t.Template.ID,
Repository: t.Repository,
Client: db_lib.CreateDefaultGitClient(),
}
err := repo.ValidateRepo()
if err != nil {
return err
}
if t.Task.CommitHash != nil {
// checkout to commit if it is provided for TaskRunner
return repo.Checkout(*t.Task.CommitHash)
}
// store commit to TaskRunner table
//commitHash, err := repo.GetLastCommitHash()
//
//if err != nil {
// return err
//}
//
//commitMessage, _ := repo.GetLastCommitMessage()
//
//t.task.CommitHash = &commitHash
//t.task.CommitMessage = commitMessage
//
//return t.pool.store.UpdateTask(t.task)
return nil
}
func (t *LocalJob) installVaultKeyFiles() (err error) {
t.vaultFileInstallations = make(map[string]db.AccessKeyInstallation)
if t.Template.Vaults == nil || len(t.Template.Vaults) == 0 {
return nil
}
for _, vault := range t.Template.Vaults {
var name string
if vault.Name != nil {
name = *vault.Name
} else {
name = "default"
}
var install db.AccessKeyInstallation
install, err = vault.Vault.Install(db.AccessKeyRoleAnsiblePasswordVault, t.Logger)
if err != nil {
return
}
t.vaultFileInstallations[name] = install
}
return
}