feat: scaffold job execution API
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
executionv1alpha1 "git.ddupan.top/panxiao81/ayatori/api/execution/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type JobState string
|
||||
|
||||
const (
|
||||
JobStateResolving JobState = "Resolving"
|
||||
JobStateScheduling JobState = "Scheduling"
|
||||
JobStateStarting JobState = "Starting"
|
||||
JobStateRunning JobState = "Running"
|
||||
JobStateCancelling JobState = "Cancelling"
|
||||
JobStateResultUnknown JobState = "ResultUnknown"
|
||||
JobStateSucceeded JobState = "Succeeded"
|
||||
JobStateFailed JobState = "Failed"
|
||||
JobStateCancelled JobState = "Cancelled"
|
||||
JobStateDeleting JobState = "Deleting"
|
||||
)
|
||||
|
||||
// StateOf derives a presentation state from durable API facts. The derived
|
||||
// state is deliberately not persisted as a second source of truth.
|
||||
func StateOf(job *executionv1alpha1.Job) JobState {
|
||||
if !job.DeletionTimestamp.IsZero() {
|
||||
return JobStateDeleting
|
||||
}
|
||||
|
||||
succeeded := condition(job.Status.Conditions, executionv1alpha1.JobConditionSucceeded)
|
||||
if succeeded != nil {
|
||||
switch succeeded.Status {
|
||||
case metav1.ConditionTrue:
|
||||
return JobStateSucceeded
|
||||
case metav1.ConditionFalse:
|
||||
if succeeded.Reason == "Cancelled" {
|
||||
return JobStateCancelled
|
||||
}
|
||||
return JobStateFailed
|
||||
}
|
||||
}
|
||||
|
||||
if job.Spec.DesiredState == executionv1alpha1.JobDesiredStateCancelled {
|
||||
return JobStateCancelling
|
||||
}
|
||||
if succeeded != nil && succeeded.Status == metav1.ConditionUnknown && succeeded.Reason == "ResultUnknown" {
|
||||
return JobStateResultUnknown
|
||||
}
|
||||
if job.Status.StartTime != nil {
|
||||
return JobStateRunning
|
||||
}
|
||||
if conditionTrue(job.Status.Conditions, executionv1alpha1.JobConditionScheduled) {
|
||||
return JobStateStarting
|
||||
}
|
||||
if conditionTrue(job.Status.Conditions, executionv1alpha1.JobConditionAccepted) {
|
||||
return JobStateScheduling
|
||||
}
|
||||
return JobStateResolving
|
||||
}
|
||||
|
||||
// ValidateStatus checks invariants that every adapter must preserve.
|
||||
func ValidateStatus(job *executionv1alpha1.Job) error {
|
||||
succeeded := condition(job.Status.Conditions, executionv1alpha1.JobConditionSucceeded)
|
||||
terminal := succeeded != nil && (succeeded.Status == metav1.ConditionTrue || succeeded.Status == metav1.ConditionFalse)
|
||||
if terminal && job.Status.CompletionTime == nil {
|
||||
return fmt.Errorf("terminal job must have completionTime")
|
||||
}
|
||||
if job.Status.CompletionTime != nil && !terminal {
|
||||
return fmt.Errorf("completionTime requires a terminal Succeeded condition")
|
||||
}
|
||||
if job.Status.StartTime != nil && !conditionTrue(job.Status.Conditions, executionv1alpha1.JobConditionScheduled) {
|
||||
return fmt.Errorf("startTime requires Scheduled=True")
|
||||
}
|
||||
if job.Status.StartTime != nil && job.Status.CompletionTime != nil && job.Status.CompletionTime.Before(job.Status.StartTime) {
|
||||
return fmt.Errorf("completionTime must not precede startTime")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func condition(conditions []metav1.Condition, conditionType string) *metav1.Condition {
|
||||
for i := range conditions {
|
||||
if conditions[i].Type == conditionType {
|
||||
return &conditions[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func conditionTrue(conditions []metav1.Condition, conditionType string) bool {
|
||||
current := condition(conditions, conditionType)
|
||||
return current != nil && current.Status == metav1.ConditionTrue
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
executionv1alpha1 "git.ddupan.top/panxiao81/ayatori/api/execution/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestStateOf(t *testing.T) {
|
||||
now := metav1.NewTime(time.Now())
|
||||
tests := []struct {
|
||||
name string
|
||||
job executionv1alpha1.Job
|
||||
want JobState
|
||||
}{
|
||||
{name: "resolving", want: JobStateResolving},
|
||||
{name: "scheduling", job: jobWithConditions(newCondition(executionv1alpha1.JobConditionAccepted, metav1.ConditionTrue, "Valid")), want: JobStateScheduling},
|
||||
{name: "starting", job: jobWithConditions(newCondition(executionv1alpha1.JobConditionScheduled, metav1.ConditionTrue, "BackendCreated")), want: JobStateStarting},
|
||||
{name: "running", job: executionv1alpha1.Job{Status: executionv1alpha1.JobStatus{StartTime: &now}}, want: JobStateRunning},
|
||||
{name: "result unknown", job: jobWithConditions(newCondition(executionv1alpha1.JobConditionSucceeded, metav1.ConditionUnknown, "ResultUnknown")), want: JobStateResultUnknown},
|
||||
{name: "cancelling", job: executionv1alpha1.Job{Spec: executionv1alpha1.JobSpec{DesiredState: executionv1alpha1.JobDesiredStateCancelled}}, want: JobStateCancelling},
|
||||
{name: "succeeded", job: jobWithConditions(newCondition(executionv1alpha1.JobConditionSucceeded, metav1.ConditionTrue, "Completed")), want: JobStateSucceeded},
|
||||
{name: "failed", job: jobWithConditions(newCondition(executionv1alpha1.JobConditionSucceeded, metav1.ConditionFalse, "ExitCode")), want: JobStateFailed},
|
||||
{name: "cancelled", job: jobWithConditions(newCondition(executionv1alpha1.JobConditionSucceeded, metav1.ConditionFalse, "Cancelled")), want: JobStateCancelled},
|
||||
{name: "terminal beats desired cancellation", job: func() executionv1alpha1.Job {
|
||||
j := jobWithConditions(newCondition(executionv1alpha1.JobConditionSucceeded, metav1.ConditionTrue, "Completed"))
|
||||
j.Spec.DesiredState = executionv1alpha1.JobDesiredStateCancelled
|
||||
return j
|
||||
}(), want: JobStateSucceeded},
|
||||
{name: "deleting beats terminal", job: func() executionv1alpha1.Job {
|
||||
j := jobWithConditions(newCondition(executionv1alpha1.JobConditionSucceeded, metav1.ConditionTrue, "Completed"))
|
||||
j.DeletionTimestamp = &now
|
||||
return j
|
||||
}(), want: JobStateDeleting},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := StateOf(&tt.job); got != tt.want {
|
||||
t.Fatalf("StateOf() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateStatus(t *testing.T) {
|
||||
start := metav1.NewTime(time.Unix(100, 0))
|
||||
finish := metav1.NewTime(time.Unix(200, 0))
|
||||
earlier := metav1.NewTime(time.Unix(50, 0))
|
||||
scheduled := newCondition(executionv1alpha1.JobConditionScheduled, metav1.ConditionTrue, "BackendCreated")
|
||||
succeeded := newCondition(executionv1alpha1.JobConditionSucceeded, metav1.ConditionTrue, "Completed")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
status executionv1alpha1.JobStatus
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "empty status"},
|
||||
{name: "running", status: executionv1alpha1.JobStatus{Conditions: []metav1.Condition{scheduled}, StartTime: &start}},
|
||||
{name: "completed", status: executionv1alpha1.JobStatus{Conditions: []metav1.Condition{scheduled, succeeded}, StartTime: &start, CompletionTime: &finish}},
|
||||
{name: "terminal without completion time", status: executionv1alpha1.JobStatus{Conditions: []metav1.Condition{succeeded}}, wantErr: true},
|
||||
{name: "completion without terminal", status: executionv1alpha1.JobStatus{CompletionTime: &finish}, wantErr: true},
|
||||
{name: "start without scheduling", status: executionv1alpha1.JobStatus{StartTime: &start}, wantErr: true},
|
||||
{name: "completion before start", status: executionv1alpha1.JobStatus{Conditions: []metav1.Condition{scheduled, succeeded}, StartTime: &start, CompletionTime: &earlier}, wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateStatus(&executionv1alpha1.Job{Status: tt.status})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("ValidateStatus() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newCondition(conditionType string, status metav1.ConditionStatus, reason string) metav1.Condition {
|
||||
return metav1.Condition{Type: conditionType, Status: status, Reason: reason}
|
||||
}
|
||||
|
||||
func jobWithConditions(conditions ...metav1.Condition) executionv1alpha1.Job {
|
||||
return executionv1alpha1.Job{Status: executionv1alpha1.JobStatus{Conditions: conditions}}
|
||||
}
|
||||
Reference in New Issue
Block a user