github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/controller/pipeline/pipelinerunner_controller.go (about)

     1  package pipeline
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  
     8  	"github.com/olli-ai/jx/v2/pkg/cmd/clients"
     9  	"github.com/olli-ai/jx/v2/pkg/cmd/opts"
    10  	"github.com/olli-ai/jx/v2/pkg/tekton"
    11  	"github.com/olli-ai/jx/v2/pkg/tekton/metapipeline"
    12  
    13  	"io/ioutil"
    14  	"net/http"
    15  	"os"
    16  	"os/signal"
    17  	"strconv"
    18  	"strings"
    19  	"sync"
    20  	"syscall"
    21  	"time"
    22  
    23  	"github.com/olli-ai/jx/v2/pkg/prow"
    24  	"github.com/olli-ai/jx/v2/pkg/util"
    25  	"github.com/sirupsen/logrus"
    26  
    27  	"github.com/olli-ai/jx/v2/pkg/cmd/step/create"
    28  
    29  	"github.com/jenkins-x/jx-logging/pkg/log"
    30  	"github.com/olli-ai/jx/v2/pkg/jenkinsfile"
    31  
    32  	jxclient "github.com/jenkins-x/jx-api/pkg/client/clientset/versioned"
    33  	"github.com/olli-ai/jx/v2/pkg/kube"
    34  	"github.com/pkg/errors"
    35  
    36  	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	prowapi "k8s.io/test-infra/prow/apis/prowjobs/v1"
    38  	"k8s.io/test-infra/prow/pod-utils/downwardapi"
    39  )
    40  
    41  const (
    42  	// healthPath is the URL path for the HTTP endpoint that returns health status.
    43  	healthPath = "/health"
    44  	// readyPath URL path for the HTTP endpoint that returns ready status.
    45  	readyPath = "/ready"
    46  
    47  	// jobLabel is the label name used to identify the Prow job within PipelineRunRequest.Labels
    48  	jobLabel = "prowJobName"
    49  
    50  	shutdownTimeout = 5
    51  )
    52  
    53  var (
    54  	logger = log.Logger().WithFields(logrus.Fields{"component": "pipelinerunner"})
    55  )
    56  
    57  // PipelineRunRequest the request to trigger a pipeline run
    58  type PipelineRunRequest struct {
    59  	Labels      map[string]string   `json:"labels,omitempty"`
    60  	ProwJobSpec prowapi.ProwJobSpec `json:"prowJobSpec,omitempty"`
    61  }
    62  
    63  // PipelineRunResponse the results of triggering a pipeline run
    64  type PipelineRunResponse struct {
    65  	Resources []kube.ObjectReference `json:"resources,omitempty"`
    66  }
    67  
    68  // ObjectReference represents a reference to a k8s resource
    69  type ObjectReference struct {
    70  	APIVersion string `json:"apiVersion" protobuf:"bytes,5,opt,name=apiVersion"`
    71  	// Kind of the referent.
    72  	// More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds
    73  	Kind string `json:"kind" protobuf:"bytes,1,opt,name=kind"`
    74  	// Name of the referent.
    75  	// More info: http://kubernetes.io/docs/user-guide/identifiers#names
    76  	Name string `json:"name" protobuf:"bytes,3,opt,name=name"`
    77  }
    78  
    79  type controller struct {
    80  	bindAddress        string
    81  	path               string
    82  	port               int
    83  	useMetaPipeline    bool
    84  	metaPipelineImage  string
    85  	semanticRelease    bool
    86  	serviceAccount     string
    87  	ns                 string
    88  	jxClient           jxclient.Interface
    89  	metaPipelineClient metapipeline.Client
    90  }
    91  
    92  func (c *controller) Start() {
    93  	var wg sync.WaitGroup
    94  	ctx, cancel := context.WithCancel(context.Background())
    95  	defer cancel()
    96  	c.startWorkers(ctx, &wg, cancel)
    97  	c.setupSignalChannel(cancel)
    98  	wg.Wait()
    99  }
   100  
   101  func (c *controller) startWorkers(ctx context.Context, wg *sync.WaitGroup, cancel context.CancelFunc) {
   102  	wg.Add(1)
   103  	go func() {
   104  		defer wg.Done()
   105  		mux := http.NewServeMux()
   106  		mux.Handle(c.path, http.HandlerFunc(c.pipeline))
   107  		mux.Handle(healthPath, http.HandlerFunc(c.health))
   108  		mux.Handle(readyPath, http.HandlerFunc(c.ready))
   109  		srv := &http.Server{
   110  			Addr:    fmt.Sprintf("%s:%d", c.bindAddress, c.port),
   111  			Handler: mux,
   112  		}
   113  
   114  		go func() {
   115  			logger.Infof("starting HTTP server on %s port %d", c.bindAddress, c.port)
   116  			logger.Infof("using meta pipeline mode: %t", c.useMetaPipeline)
   117  			if c.metaPipelineImage != "" {
   118  				logger.Infof("using custom pipeline image: %s", c.metaPipelineImage)
   119  			}
   120  			if err := srv.ListenAndServe(); err != nil {
   121  				if err == http.ErrServerClosed {
   122  					logger.Debugf("server closed")
   123  				} else {
   124  					logger.Errorf("unexpected error in HTTP server: %s", err.Error())
   125  				}
   126  				cancel()
   127  				return
   128  			}
   129  		}()
   130  
   131  		for {
   132  			select {
   133  			case <-ctx.Done():
   134  				logger.Infof("shutting down HTTP server on %s port %d", c.bindAddress, c.port)
   135  				ctx, cancel := context.WithTimeout(ctx, shutdownTimeout*time.Second)
   136  				_ = srv.Shutdown(ctx)
   137  				if c.metaPipelineClient != nil {
   138  					err := c.metaPipelineClient.Close()
   139  					logger.Error(errors.Wrap(err, "Error closing the meta pipeline client"))
   140  				}
   141  				cancel()
   142  				return
   143  			}
   144  		}
   145  	}()
   146  }
   147  
   148  // health returns either HTTP 204 if the service is healthy, otherwise nothing ('cos it's dead).
   149  func (c *controller) health(w http.ResponseWriter, r *http.Request) {
   150  	logger.Trace("health check")
   151  	w.WriteHeader(http.StatusNoContent)
   152  }
   153  
   154  // ready returns either HTTP 204 if the service is ready to serve requests, otherwise HTTP 503.
   155  func (c *controller) ready(w http.ResponseWriter, r *http.Request) {
   156  	logger.Trace("ready check")
   157  	w.WriteHeader(http.StatusNoContent)
   158  }
   159  
   160  // handle request for pipeline runs
   161  func (c *controller) pipeline(w http.ResponseWriter, r *http.Request) {
   162  	switch r.Method {
   163  	case http.MethodGet:
   164  		_, err := fmt.Fprintf(w, "please POST JSON to this endpoint!\n")
   165  		if err != nil {
   166  			logger.Errorf("unable to write response to GET request: %s", err.Error())
   167  		}
   168  	case http.MethodHead:
   169  		logger.Info("HEAD Todo...")
   170  	case http.MethodPost:
   171  		c.handlePostRequest(r, w)
   172  	default:
   173  		logger.Errorf("unsupported method %s for %s", r.Method, c.path)
   174  		http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
   175  	}
   176  }
   177  
   178  func (c *controller) handlePostRequest(r *http.Request, w http.ResponseWriter) {
   179  	requestParams, err := c.parseStartPipelineRequestParameters(r)
   180  	if err != nil {
   181  		c.returnStatusBadRequest(err, "could not read the JSON request body: "+err.Error(), w)
   182  		return
   183  	}
   184  
   185  	pipelineRunResponse, err := c.startPipeline(requestParams)
   186  	if err != nil {
   187  		c.returnStatusBadRequest(err, "could not start pipeline: "+err.Error(), w)
   188  		return
   189  	}
   190  
   191  	data, err := c.marshalPayload(pipelineRunResponse)
   192  	if err != nil {
   193  		c.returnStatusBadRequest(err, "failed to marshal payload", w)
   194  		return
   195  	}
   196  	_, err = w.Write(data)
   197  	if err != nil {
   198  		logger.Errorf("error writing PipelineRunResponse: %s", err.Error())
   199  	}
   200  }
   201  
   202  func (c *controller) parseStartPipelineRequestParameters(r *http.Request) (PipelineRunRequest, error) {
   203  	request := PipelineRunRequest{}
   204  	data, err := ioutil.ReadAll(r.Body)
   205  	if err != nil {
   206  		return request, errors.Wrapf(err, fmt.Sprintf("could not read the JSON request body: %s", err.Error()))
   207  	}
   208  	err = json.Unmarshal(data, &request)
   209  	if err != nil {
   210  		return request, errors.Wrapf(err, fmt.Sprintf("failed to unmarshal the JSON request body: %s", err.Error()))
   211  	}
   212  	logger.WithField("payload", util.PrettyPrint(request)).Debug("received PipelineRunRequest payload")
   213  	return request, nil
   214  }
   215  
   216  // startPipeline handles an incoming request to start a pipeline.
   217  func (c *controller) startPipeline(pipelineRun PipelineRunRequest) (PipelineRunResponse, error) {
   218  	response := PipelineRunResponse{}
   219  	var revision string
   220  	var prNumber string
   221  
   222  	prowJobSpec := pipelineRun.ProwJobSpec
   223  	if prowJobSpec.Refs == nil {
   224  		return response, errors.New(fmt.Sprintf("no prowJobSpec.refs passed: %s", util.PrettyPrint(pipelineRun)))
   225  	}
   226  
   227  	// Only if there is one Pull in Refs, it's a PR build so we are going to pass it
   228  	if len(prowJobSpec.Refs.Pulls) == 1 {
   229  		revision = prowJobSpec.Refs.Pulls[0].SHA
   230  		prNumber = strconv.Itoa(prowJobSpec.Refs.Pulls[0].Number)
   231  	} else {
   232  		//Otherwise it's a Master / Batch build, and we handle it later
   233  		revision = prowJobSpec.Refs.BaseSHA
   234  	}
   235  
   236  	envs, err := downwardapi.EnvForSpec(downwardapi.NewJobSpec(prowJobSpec, "", ""))
   237  	if err != nil {
   238  		return response, errors.Wrap(err, "failed to get env vars from prowjob")
   239  	}
   240  
   241  	sourceURL := c.getSourceURL(prowJobSpec.Refs.Org, prowJobSpec.Refs.Repo)
   242  	if sourceURL == "" {
   243  		// fallback to GutHub provider
   244  		sourceURL = fmt.Sprintf("https://github.com/%s/%s.git", prowJobSpec.Refs.Org, prowJobSpec.Refs.Repo)
   245  	}
   246  
   247  	if revision == "" {
   248  		revision = "master"
   249  	}
   250  
   251  	branch := c.getBranch(prowJobSpec)
   252  	if branch == "" {
   253  		branch = "master"
   254  	}
   255  
   256  	logger.WithFields(logrus.Fields{"sourceURL": sourceURL, "branch": branch, "revision": revision, "context": prowJobSpec.Context, "meta": c.useMetaPipeline}).Info("triggering pipeline")
   257  
   258  	results := PipelineRunResponse{}
   259  	if c.useMetaPipeline {
   260  		crds, err := c.triggerMetaPipeline(pipelineRun, prNumber, sourceURL, revision, branch, envs)
   261  		if err != nil {
   262  			return response, err
   263  		}
   264  
   265  		results.Resources = crds.ObjectReferences()
   266  	} else {
   267  		pipelineCreateOption := c.buildStepCreateTaskOption(prowJobSpec, prNumber, sourceURL, revision, branch, pipelineRun, envs)
   268  		err = pipelineCreateOption.Run()
   269  		if err != nil {
   270  			return response, errors.Wrap(err, "error triggering the pipeline run")
   271  		}
   272  		results.Resources = pipelineCreateOption.Results.ObjectReferences()
   273  	}
   274  
   275  	return results, nil
   276  }
   277  
   278  func (c *controller) buildStepCreateTaskOption(prowJobSpec prowapi.ProwJobSpec, prNumber string, sourceURL string, revision string, branch string, pipelineRun PipelineRunRequest, envs map[string]string) *create.StepCreateTaskOptions {
   279  	createTaskOption := &create.StepCreateTaskOptions{}
   280  	createTaskOption.CommonOptions = opts.NewCommonOptionsWithTerm(clients.NewFactory(), os.Stdin, os.Stdout, os.Stderr)
   281  	if prowJobSpec.Type == prowapi.PostsubmitJob {
   282  		createTaskOption.PipelineKind = jenkinsfile.PipelineKindRelease
   283  	} else {
   284  		createTaskOption.PipelineKind = jenkinsfile.PipelineKindPullRequest
   285  	}
   286  
   287  	// defaults
   288  	createTaskOption.SourceName = "source"
   289  	createTaskOption.Duration = time.Second * 20
   290  	createTaskOption.PullRequestNumber = prNumber
   291  	createTaskOption.CloneGitURL = sourceURL
   292  	createTaskOption.DeleteTempDir = true
   293  	createTaskOption.Context = prowJobSpec.Context
   294  	createTaskOption.Branch = branch
   295  	createTaskOption.Revision = revision
   296  	createTaskOption.ServiceAccount = c.serviceAccount
   297  	createTaskOption.SemanticRelease = c.semanticRelease
   298  	// turn map into string array with = separator to match type of custom labels which are CLI flags
   299  	for key, value := range pipelineRun.Labels {
   300  		createTaskOption.CustomLabels = append(createTaskOption.CustomLabels, fmt.Sprintf("%s=%s", key, value))
   301  	}
   302  	// turn map into string array with = separator to match type of custom env vars which are CLI flags
   303  	for key, value := range envs {
   304  		createTaskOption.CustomEnvs = append(createTaskOption.CustomEnvs, fmt.Sprintf("%s=%s", key, value))
   305  	}
   306  
   307  	return createTaskOption
   308  }
   309  
   310  func (c *controller) triggerMetaPipeline(pipelineRun PipelineRunRequest, prNumber string, sourceURL string, revision string, branch string, envs map[string]string) (*tekton.CRDWrapper, error) {
   311  	prowJobSpec := pipelineRun.ProwJobSpec
   312  	pullRefs := c.getPullRefs(prowJobSpec)
   313  
   314  	job := pipelineRun.Labels[jobLabel]
   315  	if job == "" {
   316  		return nil, errors.Errorf("unable to find prow job name in pipeline request: %s", util.PrettyPrint(pipelineRun))
   317  	}
   318  
   319  	pullRef := c.prowToMetaPipelinePullRef(sourceURL, &pullRefs)
   320  	pipelineKind := c.determinePipelineKind(pullRefs)
   321  
   322  	pipelineCreateParam := metapipeline.PipelineCreateParam{
   323  		PullRef:        pullRef,
   324  		PipelineKind:   pipelineKind,
   325  		Context:        prowJobSpec.Context,
   326  		EnvVariables:   envs,
   327  		Labels:         pipelineRun.Labels,
   328  		ServiceAccount: c.serviceAccount,
   329  		DefaultImage:   c.metaPipelineImage,
   330  	}
   331  
   332  	pipelineActivity, tektonCRDs, err := c.metaPipelineClient.Create(pipelineCreateParam)
   333  	if err != nil {
   334  		return nil, errors.Wrap(err, "unable to create Tekton CRDs")
   335  	}
   336  
   337  	logger.WithField("crds", tektonCRDs.String()).Tracef("generated crds for %s", pipelineActivity.Name)
   338  
   339  	err = c.metaPipelineClient.Apply(pipelineActivity, tektonCRDs)
   340  	if err != nil {
   341  		return nil, errors.Wrap(err, "unable to apply Tekton CRDs")
   342  	}
   343  
   344  	return &tektonCRDs, nil
   345  }
   346  
   347  func (c *controller) marshalPayload(payload interface{}) ([]byte, error) {
   348  	data, err := json.Marshal(payload)
   349  	if err != nil {
   350  		return nil, errors.Wrapf(err, "marshalling the JSON payload %#v", payload)
   351  	}
   352  	return data, nil
   353  }
   354  
   355  func (c *controller) returnStatusBadRequest(err error, message string, w http.ResponseWriter) {
   356  	logger.Infof("%v %s", err, message)
   357  	w.WriteHeader(http.StatusBadRequest)
   358  	_, err = w.Write([]byte(message))
   359  	if err != nil {
   360  		logger.Warnf("Error returning HTTP 400: %s", err)
   361  	}
   362  }
   363  
   364  func (c *controller) getBranch(spec prowapi.ProwJobSpec) string {
   365  	branch := spec.Refs.BaseRef
   366  	if spec.Type == prowapi.PostsubmitJob {
   367  		return branch
   368  	}
   369  	if spec.Type == prowapi.BatchJob {
   370  		return "batch"
   371  	}
   372  	if len(spec.Refs.Pulls) > 0 {
   373  		branch = fmt.Sprintf("PR-%v", spec.Refs.Pulls[0].Number)
   374  	}
   375  	return branch
   376  }
   377  
   378  func (c *controller) getPullRefs(spec prowapi.ProwJobSpec) prow.PullRefs {
   379  	toMerge := make(map[string]string)
   380  	for _, pull := range spec.Refs.Pulls {
   381  		toMerge[strconv.Itoa(pull.Number)] = pull.SHA
   382  	}
   383  
   384  	pullRef := prow.PullRefs{
   385  		BaseBranch: spec.Refs.BaseRef,
   386  		BaseSha:    spec.Refs.BaseSHA,
   387  		ToMerge:    toMerge,
   388  	}
   389  	return pullRef
   390  }
   391  
   392  // setupSignalChannel registers a listener for Unix signals for a ordered shutdown
   393  func (c *controller) setupSignalChannel(cancel context.CancelFunc) {
   394  	sigchan := make(chan os.Signal, 1)
   395  	signal.Notify(sigchan, syscall.SIGTERM)
   396  
   397  	go func() {
   398  		<-sigchan
   399  		logger.Info("Received SIGTERM signal. Initiating shutdown.")
   400  		cancel()
   401  	}()
   402  }
   403  
   404  func (c *controller) getSourceURL(org, repo string) string {
   405  	resourceInterface := c.jxClient.JenkinsV1().SourceRepositories(c.ns)
   406  
   407  	sourceRepos, err := resourceInterface.List(meta_v1.ListOptions{
   408  		LabelSelector: fmt.Sprintf("owner=%s,repository=%s", org, repo),
   409  	})
   410  
   411  	if err != nil || sourceRepos == nil || len(sourceRepos.Items) == 0 {
   412  		return ""
   413  	}
   414  
   415  	gitProviderURL := sourceRepos.Items[0].Spec.Provider
   416  	if gitProviderURL == "" {
   417  		return ""
   418  	}
   419  	if !strings.HasSuffix(gitProviderURL, "/") {
   420  		gitProviderURL = gitProviderURL + "/"
   421  	}
   422  
   423  	return fmt.Sprintf("%s%s/%s.git", gitProviderURL, org, repo)
   424  }
   425  
   426  func (c *controller) prowToMetaPipelinePullRef(sourceURL string, prowPullRef *prow.PullRefs) metapipeline.PullRef {
   427  	var pullRef metapipeline.PullRef
   428  	if len(prowPullRef.ToMerge) > 0 {
   429  		var prs []metapipeline.PullRequestRef
   430  		for prID, SHA := range prowPullRef.ToMerge {
   431  			prs = append(prs, metapipeline.PullRequestRef{ID: prID, MergeSHA: SHA})
   432  		}
   433  		pullRef = metapipeline.NewPullRefWithPullRequest(sourceURL, prowPullRef.BaseBranch, prowPullRef.BaseSha, prs...)
   434  	} else {
   435  		pullRef = metapipeline.NewPullRef(sourceURL, prowPullRef.BaseBranch, prowPullRef.BaseSha)
   436  	}
   437  	return pullRef
   438  }
   439  
   440  func (c *controller) determinePipelineKind(pullRef prow.PullRefs) metapipeline.PipelineKind {
   441  	var kind metapipeline.PipelineKind
   442  
   443  	prCount := len(pullRef.ToMerge)
   444  	if prCount > 0 {
   445  		kind = metapipeline.PullRequestPipeline
   446  	} else {
   447  		kind = metapipeline.ReleasePipeline
   448  	}
   449  	log.Logger().Debugf("pipeline kind for pull ref '%s' : '%s'", pullRef.String(), kind)
   450  	return kind
   451  }