diff --git a/api/jobs/job_controller.go b/api/jobs/job_controller.go index 180ed1f9..64a6d8b8 100644 --- a/api/jobs/job_controller.go +++ b/api/jobs/job_controller.go @@ -41,6 +41,11 @@ func (jc *jobController) GetRoutes() models.Routes { Method: "POST", HandlerFunc: StopApplicationJob, }, + models.Route{ + Path: rootPath + "/jobs/{jobName}/rerun", + Method: "POST", + HandlerFunc: RerunApplicationJob, + }, models.Route{ Path: rootPath + "/jobs/{jobName}/pipelineruns", Method: "GET", @@ -228,6 +233,54 @@ func StopApplicationJob(accounts models.Accounts, w http.ResponseWriter, r *http w.WriteHeader(http.StatusNoContent) } +// RerunApplicationJob Reruns the pipeline job +func RerunApplicationJob(accounts models.Accounts, w http.ResponseWriter, r *http.Request) { + // swagger:operation POST /applications/{appName}/jobs/{jobName}/restart pipeline-job rerunApplicationJob + // --- + // summary: Reruns the pipeline job + // parameters: + // - name: appName + // in: path + // description: name of application + // type: string + // required: true + // - name: jobName + // in: path + // description: name of job + // type: string + // required: true + // - name: Impersonate-User + // in: header + // description: Works only with custom setup of cluster. Allow impersonation of test users (Required if Impersonate-Group is set) + // type: string + // required: false + // - name: Impersonate-Group + // in: header + // description: Works only with custom setup of cluster. Allow impersonation of test group (Required if Impersonate-User is set) + // type: array + // items: + // type: string + // required: false + // responses: + // "204": + // description: "Job rerun ok" + // "401": + // description: "Unauthorized" + // "404": + // description: "Not found" + appName := mux.Vars(r)["appName"] + jobName := mux.Vars(r)["jobName"] + handler := Init(accounts, deployments.Init(accounts)) + err := handler.RerunJob(r.Context(), appName, jobName) + + if err != nil { + radixhttp.ErrorResponse(w, r, err) + return + } + + w.WriteHeader(http.StatusNoContent) +} + // GetTektonPipelineRuns Get the Tekton pipeline runs overview func GetTektonPipelineRuns(accounts models.Accounts, w http.ResponseWriter, r *http.Request) { // swagger:operation GET /applications/{appName}/jobs/{jobName}/pipelineruns pipeline-job getTektonPipelineRuns diff --git a/api/jobs/job_handler_test.go b/api/jobs/job_handler_test.go index 0dc08c1e..f1ce09f1 100644 --- a/api/jobs/job_handler_test.go +++ b/api/jobs/job_handler_test.go @@ -5,15 +5,13 @@ import ( "testing" "time" - secretproviderfake "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned/fake" - deployMock "github.com/equinor/radix-api/api/deployments/mock" deploymentModels "github.com/equinor/radix-api/api/deployments/models" jobModels "github.com/equinor/radix-api/api/jobs/models" "github.com/equinor/radix-api/models" radixmodels "github.com/equinor/radix-common/models" radixutils "github.com/equinor/radix-common/utils" - v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" + radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" "github.com/equinor/radix-operator/pkg/apis/utils" "github.com/equinor/radix-operator/pkg/apis/utils/slice" radixfake "github.com/equinor/radix-operator/pkg/client/clientset/versioned/fake" @@ -23,6 +21,7 @@ import ( k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kubefake "k8s.io/client-go/kubernetes/fake" + secretproviderfake "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned/fake" ) type JobHandlerTestSuite struct { @@ -47,50 +46,75 @@ type jobCreatedScenario struct { type jobStatusScenario struct { scenarioName string jobName string - condition v1.RadixJobCondition + condition radixv1.RadixJobCondition stop bool expectedStatus string } +type jobProperties struct { + name string + condition radixv1.RadixJobCondition + stop bool +} + +type jobRerunScenario struct { + scenarioName string + existingJob *jobProperties + jobNameToRerun string + expectedError error +} + func TestRunJobHandlerTestSuite(t *testing.T) { suite.Run(t, new(JobHandlerTestSuite)) } func (s *JobHandlerTestSuite) SetupTest() { + s.setupTest() +} + +func (s *JobHandlerTestSuite) setupTest() { s.inKubeClient, s.inRadixClient, s.outKubeClient, s.outRadixClient, s.inSecretProviderClient, s.outSecretProviderClient = s.getUtils() accounts := models.NewAccounts(s.inKubeClient, s.inRadixClient, s.inSecretProviderClient, nil, s.outKubeClient, s.outRadixClient, s.outSecretProviderClient, nil, "", radixmodels.Impersonation{}) s.accounts = accounts } +func (s *JobHandlerTestSuite) getUtils() (inKubeClient *kubefake.Clientset, inRadixClient *radixfake.Clientset, outKubeClient *kubefake.Clientset, outRadixClient *radixfake.Clientset, inSecretProviderClient *secretproviderfake.Clientset, outSecretProviderClient *secretproviderfake.Clientset) { + inKubeClient, outKubeClient = kubefake.NewSimpleClientset(), kubefake.NewSimpleClientset() + inRadixClient, outRadixClient = radixfake.NewSimpleClientset(), radixfake.NewSimpleClientset() + inSecretProviderClient, outSecretProviderClient = secretproviderfake.NewSimpleClientset(), secretproviderfake.NewSimpleClientset() + return +} + func (s *JobHandlerTestSuite) Test_GetApplicationJob() { - jobName, appName, branch, commitId, pipeline, triggeredBy := "a_job", "an_app", "a_branch", "a_commitid", v1.BuildDeploy, "a_user" + jobName, appName, branch, commitId, pipeline, triggeredBy := "a_job", "an_app", "a_branch", "a_commitid", radixv1.BuildDeploy, "a_user" started, ended := metav1.NewTime(time.Date(2020, 1, 1, 0, 0, 0, 0, time.Local)), metav1.NewTime(time.Date(2020, 1, 2, 0, 0, 0, 0, time.Local)) - step1Name, step1Pod, step1Condition, step1Started, step1Ended, step1Components := "step1_name", "step1_pod", v1.JobRunning, metav1.Now(), metav1.NewTime(time.Now().Add(1*time.Hour)), []string{"step1_comp1", "step1_comp2"} + step1Name, step1Pod, step1Condition, step1Started, step1Ended, step1Components := "step1_name", "step1_pod", radixv1.JobRunning, metav1.Now(), metav1.NewTime(time.Now().Add(1*time.Hour)), []string{"step1_comp1", "step1_comp2"} step2Name := "step2_name" - rj := &v1.RadixJob{ + rj := &radixv1.RadixJob{ ObjectMeta: metav1.ObjectMeta{ Name: jobName, Namespace: utils.GetAppNamespace(appName), }, - Spec: v1.RadixJobSpec{ - Build: v1.RadixBuildSpec{ + Spec: radixv1.RadixJobSpec{ + Build: radixv1.RadixBuildSpec{ Branch: branch, CommitID: commitId, }, PipeLineType: pipeline, TriggeredBy: triggeredBy, }, - Status: v1.RadixJobStatus{ + Status: radixv1.RadixJobStatus{ Started: &started, Ended: &ended, - Steps: []v1.RadixJobStep{ + Steps: []radixv1.RadixJobStep{ {Name: step1Name, PodName: step1Pod, Condition: step1Condition, Started: &step1Started, Ended: &step1Ended, Components: step1Components}, {Name: step2Name}, }, }, } - s.outRadixClient.RadixV1().RadixJobs(rj.Namespace).Create(context.Background(), rj, metav1.CreateOptions{}) + _, err := s.outRadixClient.RadixV1().RadixJobs(rj.Namespace).Create(context.Background(), rj, metav1.CreateOptions{}) + s.NoError(err) deploymentName := "a_deployment" comp1Name, comp1Type, comp1Image := "comp1", "type1", "image1" @@ -183,7 +207,7 @@ func (s *JobHandlerTestSuite) Test_GetApplicationJob_Created() { dh := deployMock.NewMockDeployHandler(ctrl) dh.EXPECT().GetDeploymentsForPipelineJob(context.Background(), gomock.Any(), gomock.Any()).Return(nil, nil).Times(1) h := Init(s.accounts, dh) - rj := v1.RadixJob{ObjectMeta: metav1.ObjectMeta{Name: scenario.jobName, Namespace: utils.GetAppNamespace(appName), CreationTimestamp: scenario.creationTimestamp}} + rj := radixv1.RadixJob{ObjectMeta: metav1.ObjectMeta{Name: scenario.jobName, Namespace: utils.GetAppNamespace(appName), CreationTimestamp: scenario.creationTimestamp}} if scenario.jobStatusCreated != emptyTime { rj.Status.Created = &scenario.jobStatusCreated } @@ -199,10 +223,10 @@ func (s *JobHandlerTestSuite) Test_GetApplicationJob_Created() { func (s *JobHandlerTestSuite) Test_GetApplicationJob_Status() { appName := "any_app" scenarios := []jobStatusScenario{ - {scenarioName: "status is set to condition when stop is false", jobName: "job1", condition: v1.JobFailed, stop: false, expectedStatus: jobModels.Failed.String()}, - {scenarioName: "status is Stopping when stop is true and condition is not Stopped", jobName: "job2", condition: v1.JobRunning, stop: true, expectedStatus: jobModels.Stopping.String()}, - {scenarioName: "status is Stopped when stop is true and condition is Stopped", jobName: "job3", condition: v1.JobStopped, stop: true, expectedStatus: jobModels.Stopped.String()}, - {scenarioName: "status is JobStoppedNoChanges when there is no changes", jobName: "job4", condition: v1.JobStoppedNoChanges, stop: false, expectedStatus: jobModels.StoppedNoChanges.String()}, + {scenarioName: "status is set to condition when stop is false", jobName: "job1", condition: radixv1.JobFailed, stop: false, expectedStatus: jobModels.Failed.String()}, + {scenarioName: "status is Stopping when stop is true and condition is not Stopped", jobName: "job2", condition: radixv1.JobRunning, stop: true, expectedStatus: jobModels.Stopping.String()}, + {scenarioName: "status is Stopped when stop is true and condition is Stopped", jobName: "job3", condition: radixv1.JobStopped, stop: true, expectedStatus: jobModels.Stopped.String()}, + {scenarioName: "status is JobStoppedNoChanges when there is no changes", jobName: "job4", condition: radixv1.JobStoppedNoChanges, stop: false, expectedStatus: jobModels.StoppedNoChanges.String()}, {scenarioName: "status is Waiting when condition is empty", jobName: "job5", expectedStatus: jobModels.Waiting.String()}, } @@ -214,10 +238,10 @@ func (s *JobHandlerTestSuite) Test_GetApplicationJob_Status() { dh := deployMock.NewMockDeployHandler(ctrl) dh.EXPECT().GetDeploymentsForPipelineJob(context.Background(), gomock.Any(), gomock.Any()).Return(nil, nil).Times(1) h := Init(s.accounts, dh) - rj := v1.RadixJob{ + rj := radixv1.RadixJob{ ObjectMeta: metav1.ObjectMeta{Name: scenario.jobName, Namespace: utils.GetAppNamespace(appName)}, - Spec: v1.RadixJobSpec{Stop: scenario.stop}, - Status: v1.RadixJobStatus{Condition: scenario.condition}, + Spec: radixv1.RadixJobSpec{Stop: scenario.stop}, + Status: radixv1.RadixJobStatus{Condition: scenario.condition}, } _, err := s.outRadixClient.RadixV1().RadixJobs(rj.Namespace).Create(context.Background(), &rj, metav1.CreateOptions{}) @@ -229,9 +253,82 @@ func (s *JobHandlerTestSuite) Test_GetApplicationJob_Status() { } } -func (s *JobHandlerTestSuite) getUtils() (inKubeClient *kubefake.Clientset, inRadixClient *radixfake.Clientset, outKubeClient *kubefake.Clientset, outRadixClient *radixfake.Clientset, inSecretProviderClient *secretproviderfake.Clientset, outSecretProviderClient *secretproviderfake.Clientset) { - inKubeClient, outKubeClient = kubefake.NewSimpleClientset(), kubefake.NewSimpleClientset() - inRadixClient, outRadixClient = radixfake.NewSimpleClientset(), radixfake.NewSimpleClientset() - inSecretProviderClient, outSecretProviderClient = secretproviderfake.NewSimpleClientset(), secretproviderfake.NewSimpleClientset() - return +func (s *JobHandlerTestSuite) TestJobHandler_RerunJob() { + appName := "anyApp" + namespace := utils.GetAppNamespace(appName) + tests := []jobRerunScenario{ + {scenarioName: "existing failed job", existingJob: &jobProperties{name: "job1", condition: radixv1.JobFailed}, jobNameToRerun: "job1", expectedError: nil}, + {scenarioName: "existing stopped job", existingJob: &jobProperties{name: "job1", condition: radixv1.JobStopped, stop: true}, jobNameToRerun: "job1", expectedError: nil}, + {scenarioName: "existing running job", existingJob: &jobProperties{name: "job1", condition: radixv1.JobRunning}, jobNameToRerun: "job1", expectedError: jobModels.JobHasInvalidConditionToRerunError(appName, "job1", radixv1.JobRunning)}, + {scenarioName: "existing stopped-no-changes job", existingJob: &jobProperties{name: "job1", condition: radixv1.JobStoppedNoChanges}, jobNameToRerun: "job1", expectedError: jobModels.JobHasInvalidConditionToRerunError(appName, "job1", radixv1.JobStoppedNoChanges)}, + {scenarioName: "existing queued job", existingJob: &jobProperties{name: "job1", condition: radixv1.JobQueued}, jobNameToRerun: "job1", expectedError: jobModels.JobHasInvalidConditionToRerunError(appName, "job1", radixv1.JobQueued)}, + {scenarioName: "existing succeeded job", existingJob: &jobProperties{name: "job1", condition: radixv1.JobSucceeded}, jobNameToRerun: "job1", expectedError: jobModels.JobHasInvalidConditionToRerunError(appName, "job1", radixv1.JobSucceeded)}, + {scenarioName: "existing waiting job", existingJob: &jobProperties{name: "job1", condition: radixv1.JobWaiting}, jobNameToRerun: "job1", expectedError: jobModels.JobHasInvalidConditionToRerunError(appName, "job1", radixv1.JobWaiting)}, + {scenarioName: "not existing job", existingJob: nil, jobNameToRerun: "job1", expectedError: jobModels.PipelineNotFoundError(appName, "job1")}, + } + for _, tt := range tests { + s.T().Run(tt.scenarioName, func(t *testing.T) { + s.setupTest() + ctrl := gomock.NewController(s.T()) + defer ctrl.Finish() + dh := deployMock.NewMockDeployHandler(ctrl) + jh := s.getJobHandler(dh) + if tt.existingJob != nil { + _, err := s.accounts.UserAccount.RadixClient.RadixV1().RadixJobs(namespace).Create(context.Background(), &radixv1.RadixJob{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: tt.existingJob.name}, + Spec: radixv1.RadixJobSpec{Stop: tt.existingJob.stop}, + Status: radixv1.RadixJobStatus{Condition: tt.existingJob.condition}, + }, metav1.CreateOptions{}) + s.NoError(err) + } + + err := jh.RerunJob(context.Background(), appName, tt.jobNameToRerun) + s.Equal(tt.expectedError, err) + }) + } +} + +func (s *JobHandlerTestSuite) TestJobHandler_StopJob() { + appName := "anyApp" + namespace := utils.GetAppNamespace(appName) + tests := []jobRerunScenario{ + {scenarioName: "existing failed job", existingJob: &jobProperties{name: "job1", condition: radixv1.JobFailed}, jobNameToRerun: "job1", expectedError: jobModels.JobHasInvalidConditionToStopError(appName, "job1", radixv1.JobFailed)}, + {scenarioName: "existing stopped job", existingJob: &jobProperties{name: "job1", condition: radixv1.JobStopped, stop: true}, jobNameToRerun: "job1", expectedError: jobModels.JobAlreadyRequestedToStopError(appName, "job1")}, + {scenarioName: "existing running job with stop in spec", existingJob: &jobProperties{name: "job1", condition: radixv1.JobRunning, stop: true}, jobNameToRerun: "job1", expectedError: jobModels.JobAlreadyRequestedToStopError(appName, "job1")}, + {scenarioName: "existing running job", existingJob: &jobProperties{name: "job1", condition: radixv1.JobRunning}, jobNameToRerun: "job1", expectedError: nil}, + {scenarioName: "existing stopped-no-changes job", existingJob: &jobProperties{name: "job1", condition: radixv1.JobStoppedNoChanges}, jobNameToRerun: "job1", expectedError: jobModels.JobHasInvalidConditionToStopError(appName, "job1", radixv1.JobStoppedNoChanges)}, + {scenarioName: "existing queued job", existingJob: &jobProperties{name: "job1", condition: radixv1.JobQueued}, jobNameToRerun: "job1", expectedError: nil}, + {scenarioName: "existing succeeded job", existingJob: &jobProperties{name: "job1", condition: radixv1.JobSucceeded}, jobNameToRerun: "job1", expectedError: nil}, + {scenarioName: "existing waiting job", existingJob: &jobProperties{name: "job1", condition: radixv1.JobWaiting}, jobNameToRerun: "job1", expectedError: nil}, + {scenarioName: "not existing job", existingJob: nil, jobNameToRerun: "job1", expectedError: jobModels.PipelineNotFoundError(appName, "job1")}, + } + for _, tt := range tests { + s.T().Run(tt.scenarioName, func(t *testing.T) { + s.setupTest() + ctrl := gomock.NewController(s.T()) + defer ctrl.Finish() + dh := deployMock.NewMockDeployHandler(ctrl) + jh := s.getJobHandler(dh) + if tt.existingJob != nil { + _, err := s.accounts.UserAccount.RadixClient.RadixV1().RadixJobs(namespace).Create(context.Background(), &radixv1.RadixJob{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: tt.existingJob.name}, + Spec: radixv1.RadixJobSpec{Stop: tt.existingJob.stop}, + Status: radixv1.RadixJobStatus{Condition: tt.existingJob.condition}, + }, metav1.CreateOptions{}) + s.NoError(err) + } + + err := jh.StopJob(context.Background(), appName, tt.jobNameToRerun) + s.Equal(tt.expectedError, err) + }) + } +} + +func (s *JobHandlerTestSuite) getJobHandler(dh *deployMock.MockDeployHandler) JobHandler { + return JobHandler{ + accounts: s.accounts, + userAccount: s.accounts.UserAccount, + serviceAccount: s.accounts.ServiceAccount, + deploy: dh, + } } diff --git a/api/jobs/manage_job_handler.go b/api/jobs/manage_job_handler.go new file mode 100644 index 00000000..4aa2be58 --- /dev/null +++ b/api/jobs/manage_job_handler.go @@ -0,0 +1,99 @@ +package jobs + +import ( + "context" + "fmt" + + jobModels "github.com/equinor/radix-api/api/jobs/models" + "github.com/equinor/radix-api/api/kubequery" + "github.com/equinor/radix-common/utils/slice" + radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" + log "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ( + jobConditionsNotValidForJobStop = []radixv1.RadixJobCondition{radixv1.JobFailed, radixv1.JobStopped, radixv1.JobStoppedNoChanges} + jobConditionsValidForJobRerun = []radixv1.RadixJobCondition{radixv1.JobFailed, radixv1.JobStopped} +) + +// StopJob Stops an application job +func (jh JobHandler) StopJob(ctx context.Context, appName, jobName string) error { + log.Infof("Stopping the job: %s, %s", jobName, appName) + radixJob, err := jh.getPipelineJobByName(ctx, appName, jobName) + if err != nil { + return err + } + if radixJob.Spec.Stop { + return jobModels.JobAlreadyRequestedToStopError(appName, jobName) + } + if slice.Any(jobConditionsNotValidForJobStop, func(condition radixv1.RadixJobCondition) bool { return condition == radixJob.Status.Condition }) { + return jobModels.JobHasInvalidConditionToStopError(appName, jobName, radixJob.Status.Condition) + } + + radixJob.Spec.Stop = true + + _, err = jh.userAccount.RadixClient.RadixV1().RadixJobs(radixJob.GetNamespace()).Update(ctx, radixJob, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to patch job object: %v", err) + } + return nil +} + +// RerunJob Reruns the pipeline job as a copy +func (jh JobHandler) RerunJob(ctx context.Context, appName, jobName string) error { + log.Infof("Rerunning the job %s in the application %s", jobName, appName) + radixJob, err := jh.getPipelineJobByName(ctx, appName, jobName) + if err != nil { + return err + } + if !slice.Any(jobConditionsValidForJobRerun, func(condition radixv1.RadixJobCondition) bool { return condition == radixJob.Status.Condition }) { + return jobModels.JobHasInvalidConditionToRerunError(appName, jobName, radixJob.Status.Condition) + } + + copiedRadixJob := jh.buildPipelineJobToRerunFrom(radixJob) + _, err = jh.createPipelineJob(ctx, appName, copiedRadixJob) + if err != nil { + return fmt.Errorf("failed to create a job %s to rerun: %v", radixJob.GetName(), err) + } + + return nil +} + +func (jh JobHandler) buildPipelineJobToRerunFrom(radixJob *radixv1.RadixJob) *radixv1.RadixJob { + rerunJobName, imageTag := getUniqueJobName(workerImage) + rerunRadixJob := radixv1.RadixJob{ + ObjectMeta: metav1.ObjectMeta{ + Name: rerunJobName, + Labels: radixJob.Labels, + Annotations: radixJob.Annotations, + }, + Spec: radixJob.Spec, + } + if rerunRadixJob.ObjectMeta.Annotations == nil { + rerunRadixJob.ObjectMeta.Annotations = make(map[string]string) + } + rerunRadixJob.ObjectMeta.Annotations[jobModels.RadixPipelineJobRerunAnnotation] = radixJob.GetName() + if len(rerunRadixJob.Spec.Build.ImageTag) > 0 { + rerunRadixJob.Spec.Build.ImageTag = imageTag + } + rerunRadixJob.Spec.Stop = false + triggeredBy, err := jh.getTriggeredBy("") + if err != nil { + log.Warnf("failed to get triggeredBy: %v", err) + } + rerunRadixJob.Spec.TriggeredBy = triggeredBy + return &rerunRadixJob +} + +func (jh JobHandler) getPipelineJobByName(ctx context.Context, appName string, jobName string) (*radixv1.RadixJob, error) { + radixJob, err := kubequery.GetRadixJob(ctx, jh.userAccount.RadixClient, appName, jobName) + if err != nil { + if errors.IsNotFound(err) { + err = jobModels.PipelineNotFoundError(appName, jobName) + } + return nil, err + } + return radixJob, nil +} diff --git a/api/jobs/models/job.go b/api/jobs/models/job.go index 77468236..446a2c86 100644 --- a/api/jobs/models/job.go +++ b/api/jobs/models/job.go @@ -6,6 +6,10 @@ import ( v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" ) +const ( + RadixPipelineJobRerunAnnotation = "radix.equinor.com/rerun-pipeline-job-from" +) + // Job holds general information about job // swagger:model Job type Job struct { @@ -39,6 +43,12 @@ type Job struct { // example: a_user@equinor.com TriggeredBy string `json:"triggeredBy"` + // RerunFromJob The source name of the job if this job was restarted from it + // + // required: false + // example: radix-pipeline-20231011104617-urynf + RerunFromJob string `json:"rerunFromJob"` + // Started timestamp // // required: false @@ -127,18 +137,19 @@ func GetJobFromRadixJob(job *v1.RadixJob, jobDeployments []*deploymentModels.Dep } jobModel := Job{ - Name: job.GetName(), - Branch: job.Spec.Build.Branch, - CommitID: job.Spec.Build.CommitID, - Created: created, - Started: radixutils.FormatTime(job.Status.Started), - Ended: radixutils.FormatTime(job.Status.Ended), - Status: GetStatusFromRadixJobStatus(job.Status, job.Spec.Stop), - Pipeline: string(job.Spec.PipeLineType), - Steps: steps, - Deployments: jobDeployments, - Components: jobComponents, - TriggeredBy: job.Spec.TriggeredBy, + Name: job.GetName(), + Branch: job.Spec.Build.Branch, + CommitID: job.Spec.Build.CommitID, + Created: created, + Started: radixutils.FormatTime(job.Status.Started), + Ended: radixutils.FormatTime(job.Status.Ended), + Status: GetStatusFromRadixJobStatus(job.Status, job.Spec.Stop), + Pipeline: string(job.Spec.PipeLineType), + Steps: steps, + Deployments: jobDeployments, + Components: jobComponents, + TriggeredBy: job.Spec.TriggeredBy, + RerunFromJob: job.Annotations[RadixPipelineJobRerunAnnotation], } if job.Spec.PipeLineType == v1.Promote { jobModel.PromotedFromEnvironment = job.Spec.Promote.FromEnvironment diff --git a/api/jobs/models/job_errors.go b/api/jobs/models/job_errors.go index 4e98a812..b30760fa 100644 --- a/api/jobs/models/job_errors.go +++ b/api/jobs/models/job_errors.go @@ -4,6 +4,7 @@ import ( "fmt" radixhttp "github.com/equinor/radix-common/net/http" + radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" ) // PipelineNotFoundError Pipeline job not found @@ -16,7 +17,17 @@ func PipelineStepNotFoundError(appName, jobName, stepName string) error { return radixhttp.TypeMissingError(fmt.Sprintf("step %s for the job %s not found for the app %s", stepName, jobName, appName), nil) } -// PipelineRunNotFoundError Tekton PipelineRun not found for the pipeline job -func PipelineRunNotFoundError(appName, jobName, pipelineRunName string) error { - return radixhttp.TypeMissingError(fmt.Sprintf("pipeline run %s not found for the app %s and the pipeline job %s", pipelineRunName, appName, jobName), nil) +// JobHasInvalidConditionToRerunError Pipeline job cannot be rerun due to invalid condition +func JobHasInvalidConditionToRerunError(appName, jobName string, jobCondition radixv1.RadixJobCondition) error { + return radixhttp.ValidationError("Radix Application Pipeline", fmt.Sprintf("only pipeline jobs that have the status Failed or Stopped can be rerun, but the job %s for the app %s has status %s", appName, jobName, jobCondition)) +} + +// JobAlreadyRequestedToStopError Pipeline job was already requested to stop +func JobAlreadyRequestedToStopError(appName, jobName string) error { + return radixhttp.ValidationError("Radix Application Pipeline", fmt.Sprintf("job %s for the app %s has already been requested to be stopped", appName, jobName)) +} + +// JobHasInvalidConditionToStopError Pipeline job cannot be stopped due to invalid condition +func JobHasInvalidConditionToStopError(appName, jobName string, jobCondition radixv1.RadixJobCondition) error { + return radixhttp.ValidationError("Radix Application Pipeline", fmt.Sprintf("only pipeline jobs that doesn't have the status Failed or Stopped can be stopped, but the job %s for the app %s has status %s", appName, jobName, jobCondition)) } diff --git a/api/jobs/start_job_handler.go b/api/jobs/start_job_handler.go index 98042e33..83610afc 100644 --- a/api/jobs/start_job_handler.go +++ b/api/jobs/start_job_handler.go @@ -34,16 +34,19 @@ func (jh JobHandler) HandleStartPipelineJob(ctx context.Context, appName string, return nil, err } - job := jh.createPipelineJob(appName, radixRegistration.Spec.CloneURL, radixConfigFullName, pipeline, jobSpec) + job := jh.buildPipelineJob(appName, radixRegistration.Spec.CloneURL, radixConfigFullName, pipeline, jobSpec) + return jh.createPipelineJob(ctx, appName, job) +} +func (jh JobHandler) createPipelineJob(ctx context.Context, appName string, job *v1.RadixJob) (*jobModels.JobSummary, error) { log.Infof("Starting job: %s, %s", job.GetName(), workerImage) appNamespace := k8sObjectUtils.GetAppNamespace(appName) - job, err = jh.userAccount.RadixClient.RadixV1().RadixJobs(appNamespace).Create(ctx, job, metav1.CreateOptions{}) + job, err := jh.userAccount.RadixClient.RadixV1().RadixJobs(appNamespace).Create(ctx, job, metav1.CreateOptions{}) if err != nil { return nil, err } - metrics.AddJobTriggered(appName, string(pipeline.Type)) + metrics.AddJobTriggered(appName, string(job.Spec.PipeLineType)) log.Infof("Started job: %s, %s", job.GetName(), workerImage) return jobModels.GetSummaryFromRadixJob(job), nil @@ -59,7 +62,7 @@ func getRadixConfigFullName(radixRegistration *v1.RadixRegistration) (string, er return radixRegistration.Spec.RadixConfigFullName, nil } -func (jh JobHandler) createPipelineJob(appName, cloneURL, radixConfigFullName string, pipeline *pipelineJob.Definition, jobSpec *jobModels.JobParameters) *v1.RadixJob { +func (jh JobHandler) buildPipelineJob(appName, cloneURL, radixConfigFullName string, pipeline *pipelineJob.Definition, jobSpec *jobModels.JobParameters) *v1.RadixJob { jobName, imageTag := getUniqueJobName(workerImage) if len(jobSpec.ImageTag) > 0 { imageTag = jobSpec.ImageTag @@ -69,7 +72,7 @@ func (jh JobHandler) createPipelineJob(appName, cloneURL, radixConfigFullName st var promoteSpec v1.RadixPromoteSpec var deploySpec v1.RadixDeploySpec - triggeredBy, err := jh.getTriggeredBy(jobSpec) + triggeredBy, err := jh.getTriggeredBy(jobSpec.TriggeredBy) if err != nil { log.Warnf("failed to get triggeredBy: %v", err) } @@ -121,8 +124,7 @@ func (jh JobHandler) createPipelineJob(appName, cloneURL, radixConfigFullName st return &job } -func (jh JobHandler) getTriggeredBy(jobSpec *jobModels.JobParameters) (string, error) { - triggeredBy := jobSpec.TriggeredBy +func (jh JobHandler) getTriggeredBy(triggeredBy string) (string, error) { if triggeredBy != "" && triggeredBy != "" { return triggeredBy, nil } diff --git a/api/jobs/stop_job_handler.go b/api/jobs/stop_job_handler.go deleted file mode 100644 index 6e8b6a71..00000000 --- a/api/jobs/stop_job_handler.go +++ /dev/null @@ -1,35 +0,0 @@ -package jobs - -import ( - "context" - "fmt" - - jobModels "github.com/equinor/radix-api/api/jobs/models" - crdUtils "github.com/equinor/radix-operator/pkg/apis/utils" - log "github.com/sirupsen/logrus" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// StopJob Stops an application job -func (jh JobHandler) StopJob(ctx context.Context, appName, jobName string) error { - log.Infof("Stopping job: %s, %s", jobName, appName) - appNamespace := crdUtils.GetAppNamespace(appName) - job, err := jh.serviceAccount.RadixClient.RadixV1().RadixJobs(appNamespace).Get(ctx, jobName, metav1.GetOptions{}) - - if errors.IsNotFound(err) { - return jobModels.PipelineNotFoundError(appName, jobName) - } - if err != nil { - return err - } - - job.Spec.Stop = true - - _, err = jh.userAccount.RadixClient.RadixV1().RadixJobs(appNamespace).Update(ctx, job, metav1.UpdateOptions{}) - if err != nil { - return fmt.Errorf("failed to patch job object: %v", err) - } - - return nil -} diff --git a/swaggerui_src/swagger.json b/swaggerui_src/swagger.json index 155eb3e8..12926e2a 100644 --- a/swaggerui_src/swagger.json +++ b/swaggerui_src/swagger.json @@ -4364,6 +4364,57 @@ } } }, + "/applications/{appName}/jobs/{jobName}/restart": { + "post": { + "tags": [ + "pipeline-job" + ], + "summary": "Reruns the pipeline job", + "operationId": "rerunApplicationJob", + "parameters": [ + { + "type": "string", + "description": "name of application", + "name": "appName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of job", + "name": "jobName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Works only with custom setup of cluster. Allow impersonation of test users (Required if Impersonate-Group is set)", + "name": "Impersonate-User", + "in": "header" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "description": "Works only with custom setup of cluster. Allow impersonation of test group (Required if Impersonate-User is set)", + "name": "Impersonate-Group", + "in": "header" + } + ], + "responses": { + "204": { + "description": "Job rerun ok" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Not found" + } + } + } + }, "/applications/{appName}/jobs/{jobName}/stop": { "post": { "tags": [ @@ -6238,6 +6289,12 @@ "x-go-name": "PromotedToEnvironment", "example": "qa" }, + "rerunFromJob": { + "description": "RerunFromJob The source name of the job if this job was restarted from it", + "type": "string", + "x-go-name": "RerunFromJob", + "example": "radix-pipeline-20231011104617-urynf" + }, "started": { "description": "Started timestamp", "type": "string",