pkg/client/jenkins/jenkins.go (345 lines of code) (raw):
package jenkins
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"time"
"github.com/bndr/gojenkins"
"gopkg.in/resty.v1"
ctrl "sigs.k8s.io/controller-runtime"
jenkinsApi "github.com/epam/edp-jenkins-operator/v2/pkg/apis/v2/v1"
"github.com/epam/edp-jenkins-operator/v2/pkg/controller/helper"
"github.com/epam/edp-jenkins-operator/v2/pkg/service/platform"
platformHelper "github.com/epam/edp-jenkins-operator/v2/pkg/service/platform/helper"
)
const (
defaultTechScriptsDirectory = "tech-scripts"
defaultGetSlavesScript = "get-slaves"
defaultJobProvisionsFolder = "job-provisions"
jenkinsCrumbKey = "Jenkins-Crumb"
usernameKey = "username"
logNameKey = "name"
numOfAttempts = 3
numOfRedirects = 10
sleepTime = 5 * time.Second
)
var log = ctrl.Log.WithName("jenkins_client")
// JenkinsClient abstraction fo Jenkins client.
type JenkinsClient struct {
instance *jenkinsApi.Jenkins
PlatformService platform.PlatformService
resty *resty.Client
GoJenkins *gojenkins.Jenkins
}
// InitJenkinsClient performs initialization of Jenkins connection.
func InitJenkinsClient(instance *jenkinsApi.Jenkins, platformService platform.PlatformService) (*JenkinsClient, error) {
apiUrl := instance.Spec.RestAPIUrl
if apiUrl == "" {
h, s, p, err := platformService.GetExternalEndpoint(instance.Namespace, instance.Name)
if err != nil {
return nil, fmt.Errorf("unable to get route for %s, err: %w", instance.Name, err)
}
apiUrl = fmt.Sprintf("%v://%v%v", s, h, p)
}
if instance.Status.AdminSecretName == "" {
log.V(1).Info("Admin secret is not created yet")
return nil, nil
}
adminSecret, err := platformService.GetSecretData(instance.Namespace, instance.Status.AdminSecretName)
if err != nil {
return nil, fmt.Errorf("failed to get admin secret for %v: %w", instance.Name, err)
}
jc := &JenkinsClient{
instance: instance,
PlatformService: platformService,
resty: resty.SetHostURL(apiUrl).SetBasicAuth(string(adminSecret[usernameKey]), string(adminSecret["password"])),
}
return jc, nil
}
func InitGoJenkinsClient(instance *jenkinsApi.Jenkins, platformService platform.PlatformService) (*JenkinsClient, error) {
url := instance.Spec.RestAPIUrl
if url == "" {
h, shm, p, err := platformService.GetExternalEndpoint(instance.Namespace, instance.Name)
if err != nil {
return nil, fmt.Errorf("unable to get route for %s, err: %w", instance.Name, err)
}
url = fmt.Sprintf("%v://%v%v", shm, h, p)
}
s, err := platformService.GetSecretData(instance.Namespace, instance.Status.AdminSecretName)
if err != nil {
return nil, fmt.Errorf("failed to get admin secret for %v: %w", instance.Name, err)
}
log.V(2).Info("initializing new Jenkins client", "url", url, usernameKey, string(s[usernameKey]))
jenkins, err := gojenkins.CreateJenkins(http.DefaultClient, url, string(s[usernameKey]), string(s["password"])).Init()
if err != nil {
return nil, fmt.Errorf("failed to create jenkins: %w", err)
}
log.Info("Jenkins client is initialized", "url", url)
return &JenkinsClient{
GoJenkins: jenkins,
PlatformService: platformService,
resty: resty.SetHostURL(url).SetBasicAuth(string(s[usernameKey]), string(s["password"])),
}, nil
}
func (jc JenkinsClient) GetCrumb() (string, error) {
resp, err := jc.resty.R().Get("/crumbIssuer/api/json")
if err != nil {
return "", fmt.Errorf("failed to send request for Crumb: %w", err)
}
if resp.StatusCode() == http.StatusNotFound {
log.V(1).Info("Jenkins Crumb is not found")
return "", nil
}
if resp.IsError() {
return "", fmt.Errorf("failed to get crumb: response code: %v, response body: %s", resp.StatusCode(), resp.Body())
}
var responseData map[string]string
if err = json.Unmarshal(resp.Body(), &responseData); err != nil {
return "", fmt.Errorf("failed to unmarshal response output: %w", err)
}
return responseData["crumb"], nil
}
// RunScript performs initialization of Jenkins connection.
func (jc JenkinsClient) RunScript(context string) error {
crumb, err := jc.GetCrumb()
if err != nil {
return err
}
headers := make(map[string]string)
if crumb != "" {
headers[jenkinsCrumbKey] = crumb
}
params := map[string]string{"script": context}
resp, err := jc.resty.R().
SetFormData(params).
SetHeaders(headers).
Post("/scriptText")
if err != nil {
return fmt.Errorf("failed to perform request to Jenkins script API: %w", err)
}
if resp.IsError() {
return fmt.Errorf("failed to run script in Jenkins: status: %s", resp.Status())
}
return nil
}
// GetSlaves returns a list of slaves configured in Jenkins kubernetes plugin.
func (jc JenkinsClient) GetSlaves() ([]string, error) {
crumb, err := jc.GetCrumb()
if err != nil {
return nil, fmt.Errorf("failed to get crumb: %w", err)
}
headers := make(map[string]string)
if crumb != "" {
headers[jenkinsCrumbKey] = crumb
}
directory, err := platformHelper.CreatePathToTemplateDirectory(defaultTechScriptsDirectory)
if err != nil {
return nil, fmt.Errorf("failed to create path to template dir: %w", err)
}
path := fmt.Sprintf("%v/%v", directory, defaultGetSlavesScript)
cn, err := os.ReadFile(filepath.Clean(path))
if err != nil {
return nil, fmt.Errorf("failed to read File: %w", err)
}
pr := map[string]string{"script": string(cn)}
resp, err := jc.resty.R().
SetQueryParams(pr).
SetHeaders(headers).
Post("/scriptText")
if err != nil {
return nil, fmt.Errorf("failed to obtain Jenkins slaves list: %w", err)
}
if resp.IsError() {
return nil, fmt.Errorf("failed to run tech script %v: status: %s", defaultGetSlavesScript, resp.Status())
}
return helper.GetSlavesList(resp.String()), nil
}
// CreateUser creates new non-interactive user in Jenkins.
func (jc JenkinsClient) CreateUser(instance *jenkinsApi.JenkinsServiceAccount) error {
crumb, err := jc.GetCrumb()
if err != nil {
return fmt.Errorf("failed to get crumb: %w", err)
}
headers := make(map[string]string)
if crumb != "" {
headers[jenkinsCrumbKey] = crumb
headers["Content-Type"] = "application/x-www-form-urlencoded"
}
secretData, err := jc.PlatformService.GetSecretData(instance.Namespace, instance.Spec.Credentials)
if err != nil || secretData == nil {
return fmt.Errorf("failed to get info from secret %v", instance.Spec.Credentials)
}
credentials, err := helper.NewJenkinsUser(secretData, instance.Spec.Type, instance.Spec.Credentials)
if err != nil {
return fmt.Errorf("failed to create new jenkins user: %w", err)
}
requestParams := map[string]string{}
requestParams["json"], err = credentials.ToString()
if err != nil {
return fmt.Errorf("failed to parse credentials to string: %w", err)
}
resp, err := jc.resty.
SetRedirectPolicy(resty.FlexibleRedirectPolicy(numOfRedirects)).
R().
SetHeaders(headers).
SetFormData(requestParams).
Post("/credentials/store/system/domain/_/createCredentials")
if err != nil {
return fmt.Errorf("failed to send Jenkins user creation request: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return fmt.Errorf("failed to create user in Jenkins: response code: %v, response body: %s", resp.StatusCode(), resp.Body())
}
return nil
}
func (jc JenkinsClient) GetAdminToken() (*string, error) {
crumb, err := jc.GetCrumb()
if err != nil {
return nil, err
}
headers := make(map[string]string)
if crumb != "" {
headers[jenkinsCrumbKey] = crumb
}
params := map[string]string{"newTokenName": "admin"}
resp, err := jc.resty.R().
SetQueryParams(params).
SetHeaders(headers).
Post("/me/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken")
if err != nil {
return nil, fmt.Errorf("failed to perform POST request: %w", err)
}
if resp.IsError() {
return nil, fmt.Errorf("failed to process request: returns error: %v", resp.Status())
}
var parsedResponse map[string]interface{}
if err = json.Unmarshal(resp.Body(), &parsedResponse); err != nil {
return nil, fmt.Errorf("failed to unmarshal Jenkins response %v: %w", resp.Body(), err)
}
parsedData, valid := parsedResponse["data"].(map[string]interface{})
if valid {
token := fmt.Sprintf("%v", parsedData["tokenValue"])
return &token, nil
}
return nil, errors.New("failed to find token for admin")
}
// GetJobProvisions returns a list of Job provisions configured in Jenkins.
func (jc JenkinsClient) GetJobProvisions(jobPath string) ([]string, error) {
var provisionNames []string
raw, err := jc.obtainRawJobProvisions(jobPath)
if err != nil {
return nil, fmt.Errorf("failed to obtain raw JobProvisioners data: %w", err)
}
classValue, ok := raw["_class"].(string)
if !ok {
return nil, fmt.Errorf("failed to access \"_class\" field")
}
if classValue != "com.cloudbees.hudson.plugins.folder.Folder" {
return nil, fmt.Errorf("failed to get job provisions: %v is not a Jenkins folder", defaultJobProvisionsFolder)
}
jobValues, ok := raw["jobs"].([]interface{})
if !ok {
return nil, fmt.Errorf("failed to access \"jobs\" field")
}
for _, jobProvision := range jobValues {
provisionName, ok := jobProvision.(map[string]interface{})["name"].(string)
if !ok {
return nil, fmt.Errorf("failed to access \"name\" field of a job")
}
provisionNames = append(provisionNames, provisionName)
}
return provisionNames, nil
}
func (jc JenkinsClient) obtainRawJobProvisions(jobPath string) (map[string]interface{}, error) {
rawJobProvisioners := make(map[string]interface{})
crumb, err := jc.GetCrumb()
if err != nil {
return nil, fmt.Errorf("failed to get crumb: %w", err)
}
headers := make(map[string]string)
if crumb != "" {
headers[jenkinsCrumbKey] = crumb
}
resp, err := jc.resty.
R().
SetHeaders(headers).
Post(fmt.Sprintf("%v/api/json?pretty=true", jobPath))
if err != nil {
return nil, fmt.Errorf("failed to obtain Job Provisioners list: %w", err)
}
if resp.IsError() {
return nil, fmt.Errorf("failed to run tech script %v: status: %s", defaultGetSlavesScript, resp.Status())
}
if err = json.Unmarshal([]byte(resp.String()), &rawJobProvisioners); err != nil {
return nil, fmt.Errorf("failed to unmarshal JobProvisioners %s: %w", resp.String(), err)
}
return rawJobProvisioners, nil
}
func (jc JenkinsClient) BuildJob(jobName string, parameters map[string]string) (*int64, error) {
log.V(2).Info("start triggering job provision", logNameKey, jobName, "codebase name", parameters["NAME"])
qn, err := jc.GoJenkins.BuildJob(jobName, parameters)
if qn != 0 || err != nil {
log.V(2).Info("end triggering job provision", logNameKey, jobName, "codebase name", parameters["NAME"])
return jc.getBuildNumber(qn)
}
return nil, fmt.Errorf("failed to finish triggering job provision for %v codebase", parameters["NAME"])
}
func (jc JenkinsClient) getBuildNumber(queueNumber int64) (*int64, error) {
log.V(2).Info("start getting build number", "queueNumber", queueNumber)
for i := 0; i < numOfAttempts; i++ {
t, err := jc.GoJenkins.GetQueueItem(queueNumber)
if err != nil {
return nil, fmt.Errorf("failed to get queue item: %w", err)
}
n := t.Raw.Executable.Number
if n != 0 {
log.Info("build number has been received", "number", n)
return &n, nil
}
time.Sleep(sleepTime)
}
return nil, fmt.Errorf("failed to get build number by queue number %v", queueNumber)
}
func (jc JenkinsClient) CreateFolder(name string) error {
log.V(2).Info("start creating jenkins folder", logNameKey, name)
names, err := jc.GoJenkins.GetAllJobNames()
if err != nil {
return fmt.Errorf("failed to GetAllJobNames: %w", err)
}
for _, n := range names {
if n.Name == name {
log.V(2).Info("Jenkins folder already exists", logNameKey, name)
return nil
}
}
if _, err := jc.GoJenkins.CreateFolder(name); err != nil {
return fmt.Errorf("failed to CreateFolder: %w", err)
}
log.V(2).Info("end creating jenkins folder", logNameKey, name)
return nil
}
func (jc JenkinsClient) GetJobByName(jobName string) (*gojenkins.Job, error) {
log.V(2).Info("start getting jenkins job", "jobName", jobName)
job, err := jc.GoJenkins.GetJob(jobName)
if err != nil {
return nil, fmt.Errorf("failed to GetJob: %w", err)
}
log.V(2).Info("end getting jenkins job", "jobName", jobName)
return job, nil
}
func (jc JenkinsClient) TriggerJob(job string, parameters map[string]string) error {
vLog := log.WithValues(logNameKey, job)
vLog.Info("triggering jenkins job")
if _, err := jc.GoJenkins.BuildJob(job, parameters); err != nil {
return fmt.Errorf("failed to BuildJob: %w", err)
}
vLog.Info("jenkins job has been triggered")
return nil
}
func (JenkinsClient) GetLastBuild(job *gojenkins.Job) (*gojenkins.Build, error) {
build, err := job.GetLastBuild()
if err != nil {
return nil, fmt.Errorf("failed to GetLastBuild form the job: %w", err)
}
return build, nil
}
func (JenkinsClient) BuildIsRunning(build *gojenkins.Build) bool {
return build.IsRunning()
}