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 }