Skip to content

Commit

Permalink
Merge pull request #112 from warjiang/feature/cronjob-api
Browse files Browse the repository at this point in the history
feat: add api for cronjob resources
  • Loading branch information
karmada-bot authored Sep 9, 2024
2 parents a172afe + 090f4cf commit 705986f
Show file tree
Hide file tree
Showing 8 changed files with 455 additions and 0 deletions.
1 change: 1 addition & 0 deletions cmd/api/app/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
_ "github.com/karmada-io/dashboard/cmd/api/app/routes/cluster"
_ "github.com/karmada-io/dashboard/cmd/api/app/routes/config"
_ "github.com/karmada-io/dashboard/cmd/api/app/routes/configmap"
_ "github.com/karmada-io/dashboard/cmd/api/app/routes/cronjob"
_ "github.com/karmada-io/dashboard/cmd/api/app/routes/daemonset"
_ "github.com/karmada-io/dashboard/cmd/api/app/routes/deployment"
_ "github.com/karmada-io/dashboard/cmd/api/app/routes/job"
Expand Down
54 changes: 54 additions & 0 deletions cmd/api/app/routes/cronjob/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package deployment

import (
"github.com/gin-gonic/gin"
"github.com/karmada-io/dashboard/cmd/api/app/router"
"github.com/karmada-io/dashboard/cmd/api/app/types/common"
"github.com/karmada-io/dashboard/pkg/client"
"github.com/karmada-io/dashboard/pkg/resource/cronjob"
"github.com/karmada-io/dashboard/pkg/resource/event"
)

func handleGetCronJob(c *gin.Context) {
namespace := common.ParseNamespacePathParameter(c)
dataSelect := common.ParseDataSelectPathParameter(c)
k8sClient := client.InClusterClientForKarmadaApiServer()
result, err := cronjob.GetCronJobList(k8sClient, namespace, dataSelect)
if err != nil {
common.Fail(c, err)
return
}
common.Success(c, result)
}

func handleGetCronJobDetail(c *gin.Context) {
namespace := c.Param("namespace")
name := c.Param("statefulset")
k8sClient := client.InClusterClientForKarmadaApiServer()
result, err := cronjob.GetCronJobDetail(k8sClient, namespace, name)
if err != nil {
common.Fail(c, err)
return
}
common.Success(c, result)
}

func handleGetCronJobEvents(c *gin.Context) {
namespace := c.Param("namespace")
name := c.Param("statefulset")
k8sClient := client.InClusterClientForKarmadaApiServer()
dataSelect := common.ParseDataSelectPathParameter(c)
result, err := event.GetResourceEvents(k8sClient, dataSelect, namespace, name)
if err != nil {
common.Fail(c, err)
return
}
common.Success(c, result)
}
func init() {
r := router.V1()
r.GET("/cronjob", handleGetCronJob)
r.GET("/cronjob/:namespace", handleGetCronJob)
r.GET("/cronjob/:namespace/:statefulset", handleGetCronJobDetail)
r.GET("/cronjob/:namespace/:statefulset/event", handleGetCronJobEvents)
}
25 changes: 25 additions & 0 deletions pkg/resource/common/resourcechannels.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,31 @@ type CronJobListChannel struct {
Error chan error
}

// GetCronJobListChannel returns a pair of channels to a Cron Job list and errors that both must be read numReads times.
func GetCronJobListChannel(client client.Interface, nsQuery *NamespaceQuery, numReads int) CronJobListChannel {
channel := CronJobListChannel{
List: make(chan *batch.CronJobList, numReads),
Error: make(chan error, numReads),
}

go func() {
list, err := client.BatchV1().CronJobs(nsQuery.ToRequestParam()).List(context.TODO(), helpers.ListEverything)
var filteredItems []batch.CronJob
for _, item := range list.Items {
if nsQuery.Matches(item.ObjectMeta.Namespace) {
filteredItems = append(filteredItems, item)
}
}
list.Items = filteredItems
for i := 0; i < numReads; i++ {
channel.List <- list
channel.Error <- err
}
}()

return channel
}

// ServiceListChannel is a list and error channels to Services.
type ServiceListChannel struct {
List chan *v1.ServiceList
Expand Down
69 changes: 69 additions & 0 deletions pkg/resource/cronjob/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package cronjob

import (
"github.com/karmada-io/dashboard/pkg/dataselect"
"github.com/karmada-io/dashboard/pkg/resource/common"
batch "k8s.io/api/batch/v1"
)

// The code below allows to perform complex data section on []batch.CronJob

type CronJobCell batch.CronJob

func (self CronJobCell) GetProperty(name dataselect.PropertyName) dataselect.ComparableValue {
switch name {
case dataselect.NameProperty:
return dataselect.StdComparableString(self.ObjectMeta.Name)
case dataselect.CreationTimestampProperty:
return dataselect.StdComparableTime(self.ObjectMeta.CreationTimestamp.Time)
case dataselect.NamespaceProperty:
return dataselect.StdComparableString(self.ObjectMeta.Namespace)
default:
// if name is not supported then just return a constant dummy value, sort will have no effect.
return nil
}
}

func ToCells(std []batch.CronJob) []dataselect.DataCell {
cells := make([]dataselect.DataCell, len(std))
for i := range std {
cells[i] = CronJobCell(std[i])
}
return cells
}

func FromCells(cells []dataselect.DataCell) []batch.CronJob {
std := make([]batch.CronJob, len(cells))
for i := range std {
std[i] = batch.CronJob(cells[i].(CronJobCell))
}
return std
}

func getStatus(list *batch.CronJobList) common.ResourceStatus {
info := common.ResourceStatus{}
if list == nil {
return info
}

for _, cronJob := range list.Items {
if cronJob.Spec.Suspend != nil && !(*cronJob.Spec.Suspend) {
info.Running++
} else {
info.Failed++
}
}

return info
}

func getContainerImages(cronJob *batch.CronJob) []string {
podSpec := cronJob.Spec.JobTemplate.Spec.Template.Spec
result := make([]string, 0)

for _, container := range podSpec.Containers {
result = append(result, container.Image)
}

return result
}
41 changes: 41 additions & 0 deletions pkg/resource/cronjob/detail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package cronjob

import (
"context"

batch "k8s.io/api/batch/v1"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sClient "k8s.io/client-go/kubernetes"
)

// CronJobDetail contains Cron Job details.
type CronJobDetail struct {
// Extends list item structure.
CronJob `json:",inline"`

ConcurrencyPolicy string `json:"concurrencyPolicy"`
StartingDeadLineSeconds *int64 `json:"startingDeadlineSeconds"`

// List of non-critical errors, that occurred during resource retrieval.
Errors []error `json:"errors"`
}

// GetCronJobDetail gets Cron Job details.
func GetCronJobDetail(client k8sClient.Interface, namespace, name string) (*CronJobDetail, error) {

rawObject, err := client.BatchV1().CronJobs(namespace).Get(context.TODO(), name, metaV1.GetOptions{})
if err != nil {
return nil, err
}

cj := toCronJobDetail(rawObject)
return &cj, nil
}

func toCronJobDetail(cj *batch.CronJob) CronJobDetail {
return CronJobDetail{
CronJob: toCronJob(cj),
ConcurrencyPolicy: string(cj.Spec.ConcurrencyPolicy),
StartingDeadLineSeconds: cj.Spec.StartingDeadlineSeconds,
}
}
21 changes: 21 additions & 0 deletions pkg/resource/cronjob/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package cronjob

import (
"github.com/karmada-io/dashboard/pkg/dataselect"
"github.com/karmada-io/dashboard/pkg/resource/common"
"github.com/karmada-io/dashboard/pkg/resource/event"
client "k8s.io/client-go/kubernetes"
)

// GetCronJobEvents gets events associated to cron job.
func GetCronJobEvents(client client.Interface, dsQuery *dataselect.DataSelectQuery, namespace, name string) (
*common.EventList, error) {

raw, err := event.GetEvents(client, namespace, name)
if err != nil {
return event.EmptyEventList, err
}

events := event.CreateEventList(raw, dsQuery)
return &events, nil
}
147 changes: 147 additions & 0 deletions pkg/resource/cronjob/jobs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package cronjob

import (
"context"
"github.com/karmada-io/dashboard/pkg/common/errors"
"github.com/karmada-io/dashboard/pkg/common/types"
"github.com/karmada-io/dashboard/pkg/dataselect"
"github.com/karmada-io/dashboard/pkg/resource/common"
"github.com/karmada-io/dashboard/pkg/resource/job"

batch "k8s.io/api/batch/v1"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
apimachinery "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
client "k8s.io/client-go/kubernetes"
)

const (
CronJobAPIVersion = "v1"
CronJobKindName = "cronjob"
)

var emptyJobList = &job.JobList{
Jobs: make([]job.Job, 0),
Errors: make([]error, 0),
ListMeta: types.ListMeta{
TotalItems: 0,
},
}

// GetCronJobJobs returns list of jobs owned by cron job.
func GetCronJobJobs(client client.Interface,
dsQuery *dataselect.DataSelectQuery, namespace, name string, active bool) (*job.JobList, error) {

cronJob, err := client.BatchV1().CronJobs(namespace).Get(context.TODO(), name, meta.GetOptions{})
if err != nil {
return emptyJobList, err
}

channels := &common.ResourceChannels{
JobList: common.GetJobListChannel(client, common.NewSameNamespaceQuery(namespace), 1),
PodList: common.GetPodListChannel(client, common.NewSameNamespaceQuery(namespace), 1),
EventList: common.GetEventListChannel(client, common.NewSameNamespaceQuery(namespace), 1),
}

jobs := <-channels.JobList.List
err = <-channels.JobList.Error
nonCriticalErrors, criticalError := errors.ExtractErrors(err)
if criticalError != nil {
return emptyJobList, nil
}

pods := <-channels.PodList.List
err = <-channels.PodList.Error
nonCriticalErrors, criticalError = errors.AppendError(err, nonCriticalErrors)
if criticalError != nil {
return emptyJobList, criticalError
}

events := <-channels.EventList.List
err = <-channels.EventList.Error
nonCriticalErrors, criticalError = errors.AppendError(err, nonCriticalErrors)
if criticalError != nil {
return emptyJobList, criticalError
}

jobs.Items = filterJobsByOwnerUID(cronJob.UID, jobs.Items)
jobs.Items = filterJobsByState(active, jobs.Items)

return job.ToJobList(jobs.Items, pods.Items, events.Items, nonCriticalErrors, dsQuery), nil
}

// TriggerCronJob manually triggers a cron job and creates a new job.
func TriggerCronJob(client client.Interface,
namespace, name string) error {

cronJob, err := client.BatchV1().CronJobs(namespace).Get(context.TODO(), name, meta.GetOptions{})

if err != nil {
return err
}

annotations := make(map[string]string)
annotations["cronjob.kubernetes.io/instantiate"] = "manual"

labels := make(map[string]string)
for k, v := range cronJob.Spec.JobTemplate.Labels {
labels[k] = v
}

//job name cannot exceed DNS1053LabelMaxLength (52 characters)
var newJobName string
if len(cronJob.Name) < 42 {
newJobName = cronJob.Name + "-manual-" + rand.String(3)
} else {
newJobName = cronJob.Name[0:41] + "-manual-" + rand.String(3)
}

jobToCreate := &batch.Job{
ObjectMeta: meta.ObjectMeta{
Name: newJobName,
Namespace: namespace,
Annotations: annotations,
Labels: labels,
OwnerReferences: []meta.OwnerReference{{
APIVersion: CronJobAPIVersion,
Kind: CronJobKindName,
Name: cronJob.Name,
UID: cronJob.UID,
}},
},
Spec: cronJob.Spec.JobTemplate.Spec,
}

_, err = client.BatchV1().Jobs(namespace).Create(context.TODO(), jobToCreate, meta.CreateOptions{})

if err != nil {
return err
}

return nil
}

func filterJobsByOwnerUID(UID apimachinery.UID, jobs []batch.Job) (matchingJobs []batch.Job) {
for _, j := range jobs {
for _, i := range j.OwnerReferences {
if i.UID == UID {
matchingJobs = append(matchingJobs, j)
break
}
}
}
return
}

func filterJobsByState(active bool, jobs []batch.Job) (matchingJobs []batch.Job) {
for _, j := range jobs {
if active && j.Status.Active > 0 {
matchingJobs = append(matchingJobs, j)
} else if !active && j.Status.Active == 0 {
matchingJobs = append(matchingJobs, j)
} else {
//sup
}
}
return
}
Loading

0 comments on commit 705986f

Please sign in to comment.