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
97 changes: 96 additions & 1 deletion db_lib/AnsibleApp.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import (
"crypto/md5"
"fmt"
"io"
"net/url"
"os"
"path"
"strings"

"github.com/semaphoreui/semaphore/db"
"github.com/semaphoreui/semaphore/pkg/galaxy"
"github.com/semaphoreui/semaphore/pkg/ssh"
"github.com/semaphoreui/semaphore/pkg/task_logger"
)

Expand Down Expand Up @@ -54,6 +57,11 @@ type AnsibleApp struct {
Playbook *AnsiblePlaybook
Template db.Template
Repository db.Repository

// Set for the duration of InstallRequirements. The key is installed on the
// first galaxy run rather than up front, see galaxyGitEnvForRun.
galaxyInstaller AccessKeyInstaller
galaxyKey *ssh.AccessKeyInstallation
}

func (t *AnsibleApp) SetLogger(logger task_logger.Logger) task_logger.Logger {
Expand Down Expand Up @@ -90,13 +98,46 @@ func (t *AnsibleApp) InstallRequirements(args LocalAppInstallingArgs) error {
return err
}

t.galaxyInstaller = args.Installer
defer t.destroyGalaxyKey()

err = t.installCollectionsRequirements(args.EnvironmentVars, collectionArgs)
if err != nil {
return err
}
return t.installRolesRequirements(args.EnvironmentVars, roleArgs)
}

// galaxyGitEnvForRun returns the git credentials galaxy's clones need. The
// repository key is installed into an agent here rather than in
// InstallRequirements: most tasks have no requirements file to install, and
// installing up front would decrypt the key and start an agent — one more thing
// that can fail — for every task. The installation is reused across files.
func (t *AnsibleApp) galaxyGitEnvForRun() ([]string, error) {
env := galaxyGitEnv(t.Repository)

if t.galaxyInstaller == nil {
return env, nil
}

if t.galaxyKey == nil {
installation, err := t.galaxyInstaller.Install(t.Repository.SSHKey, db.AccessKeyRoleGit, t.Logger)
if err != nil {
return nil, err
}
t.galaxyKey = &installation
}

return append(env, t.galaxyKey.GetGitEnv()...), nil
}

func (t *AnsibleApp) destroyGalaxyKey() {
if t.galaxyKey != nil {
_ = t.galaxyKey.Destroy()
t.galaxyKey = nil
}
}

// skipGalaxyInstall reports whether the Galaxy install step must be skipped.
// The template-level flag provides the default; when the template allows
// overriding it, the task-level flag takes precedence.
Expand Down Expand Up @@ -218,7 +259,61 @@ func (t *AnsibleApp) installCollectionsRequirements(environmentVars, extraArgs [
}

func (t *AnsibleApp) runGalaxy(args []string, environmentVars []string) error {
return t.Playbook.RunGalaxy(args, environmentVars)
gitEnv, err := t.galaxyGitEnvForRun()
if err != nil {
return err
}

// Task variables come last so a manually configured GIT_* var still wins.
return t.Playbook.RunGalaxy(args, append(gitEnv, environmentVars...))
}

// sqQuote quotes s for GIT_CONFIG_PARAMETERS: the value is wrapped in single
// quotes, and any single quote inside it is escaped the way sh requires.
func sqQuote(s string) string {
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
}

// galaxyGitEnv lets ansible-galaxy authenticate to the repository's own git server.
//
// Galaxy shells out to `git clone` for `scm: git` requirements, and those clones
// inherit no credentials, so roles hosted next to the repository fail with
// "could not read Username" (GitHub #3677). Credentials go through
// GIT_CONFIG_PARAMETERS so they stay out of `ps` output and out of the task log,
// which only ever shows the pre-rewrite URL.
func galaxyGitEnv(repo db.Repository) (env []string) {
Comment thread
befika marked this conversation as resolved.
// Without this git prompts on /dev/tty and the task hangs instead of failing.
env = append(env, "GIT_TERMINAL_PROMPT=0")

if repo.GetType() != db.RepositoryHTTP || repo.SSHKey.Type != db.AccessKeyLoginPassword {
return
}

plain, err := url.Parse(repo.GitURL)
if err != nil || plain.Host == "" {
return
}

// Scoped to this exact scheme://host[:port] so no other server named in
// requirements.yml is ever offered the credential.
plain.Path, plain.RawQuery, plain.Fragment, plain.User = "/", "", "", nil

withAuth := *plain
if login := repo.SSHKey.LoginPassword.Login; login == "" {
withAuth.User = url.User(repo.SSHKey.LoginPassword.Password)
} else {
withAuth.User = url.UserPassword(login, repo.SSHKey.LoginPassword.Password)
}

// git splits each GIT_CONFIG_PARAMETERS entry at its first "=", and net/url
// leaves "=" unescaped in userinfo, so a credential containing one would cut
// the key short and abort the clone with "error: invalid key". git decodes
// the escape again when it authenticates. Only the credential can hold one:
// the path, query and fragment are cleared above.
authURL := strings.ReplaceAll(withAuth.String(), "=", "%3D")

return append(env, "GIT_CONFIG_PARAMETERS="+sqQuote(
"url."+authURL+".insteadOf="+plain.String()))
}

// galaxyExtraArgs returns the template-configured arguments for one galaxy
Expand Down
Loading
Loading