|
| 1 | +package cron |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/json" |
| 6 | + "fmt" |
| 7 | + "math" |
| 8 | + "os" |
| 9 | + "time" |
| 10 | + |
| 11 | + "github.com/argoproj/argo-workflows/v3/workflow/util" |
| 12 | + |
| 13 | + cron "github.com/robfig/cron/v3" |
| 14 | + "github.com/spf13/cobra" |
| 15 | + "sigs.k8s.io/yaml" |
| 16 | + |
| 17 | + "github.com/argoproj/pkg/rand" |
| 18 | + |
| 19 | + "github.com/argoproj/argo-workflows/v3/cmd/argo/commands/client" |
| 20 | + "github.com/argoproj/argo-workflows/v3/pkg/apiclient/cronworkflow" |
| 21 | + "github.com/argoproj/argo-workflows/v3/pkg/apiclient/workflow" |
| 22 | + "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" |
| 23 | + "github.com/argoproj/argo-workflows/v3/workflow/common" |
| 24 | +) |
| 25 | + |
| 26 | +type backfillOpts struct { |
| 27 | + cronWfName string |
| 28 | + name string |
| 29 | + startDate string |
| 30 | + endDate string |
| 31 | + parallel bool |
| 32 | + argName string |
| 33 | + dateFormat string |
| 34 | + maxWorkflowCount int |
| 35 | +} |
| 36 | + |
| 37 | +func NewBackfillCommand() *cobra.Command { |
| 38 | + var ( |
| 39 | + cliOps backfillOpts |
| 40 | + ) |
| 41 | + var command = &cobra.Command{ |
| 42 | + Use: "backfill cronwf", |
| 43 | + Short: "create a cron backfill(new alpha feature)", |
| 44 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 45 | + if len(args) == 0 { |
| 46 | + cmd.HelpFunc()(cmd, args) |
| 47 | + os.Exit(0) |
| 48 | + } |
| 49 | + if cliOps.name == "" { |
| 50 | + name, err := rand.RandString(5) |
| 51 | + if err != nil { |
| 52 | + return err |
| 53 | + } |
| 54 | + cliOps.name = name |
| 55 | + } |
| 56 | + |
| 57 | + cliOps.cronWfName = args[0] |
| 58 | + return backfillCronWorkflow(cmd.Context(), args[0], cliOps) |
| 59 | + }, |
| 60 | + } |
| 61 | + command.Flags().StringVar(&cliOps.name, "name", "", "Backfill name") |
| 62 | + command.Flags().StringVar(&cliOps.startDate, "start", "", "Start date") |
| 63 | + command.Flags().StringVar(&cliOps.endDate, "end", "", "End Date") |
| 64 | + command.Flags().BoolVar(&cliOps.parallel, "parallel", false, "Enabled all backfile workflows run parallel") |
| 65 | + command.Flags().StringVar(&cliOps.argName, "argname", "cronScheduleTime", "Schedule time argument name for workflow") |
| 66 | + command.Flags().StringVar(&cliOps.dateFormat, "format", time.RFC1123, "Date format for Schedule time value") |
| 67 | + command.Flags().IntVar(&cliOps.maxWorkflowCount, "maxworkflowcount", 1000, "Maximum number of generated backfill workflows") |
| 68 | + |
| 69 | + return command |
| 70 | +} |
| 71 | + |
| 72 | +func backfillCronWorkflow(ctx context.Context, cronWFName string, cliOps backfillOpts) error { |
| 73 | + if cliOps.startDate == "" { |
| 74 | + return fmt.Errorf("Start Date should not be empty") |
| 75 | + } |
| 76 | + startTime, err := time.Parse(cliOps.dateFormat, cliOps.startDate) |
| 77 | + if err != nil { |
| 78 | + return err |
| 79 | + } |
| 80 | + var endTime time.Time |
| 81 | + if cliOps.endDate != "" { |
| 82 | + endTime, err = time.Parse(cliOps.dateFormat, cliOps.endDate) |
| 83 | + if err != nil { |
| 84 | + return err |
| 85 | + } |
| 86 | + } else { |
| 87 | + endTime = time.Now() |
| 88 | + cliOps.endDate = endTime.Format(time.RFC1123) |
| 89 | + } |
| 90 | + |
| 91 | + ctx, apiClient, err := client.NewAPIClient(ctx) |
| 92 | + if err != nil { |
| 93 | + return err |
| 94 | + } |
| 95 | + cronClient, err := apiClient.NewCronWorkflowServiceClient() |
| 96 | + if err != nil { |
| 97 | + return err |
| 98 | + } |
| 99 | + wfClient := apiClient.NewWorkflowServiceClient() |
| 100 | + req := cronworkflow.GetCronWorkflowRequest{ |
| 101 | + Name: cronWFName, |
| 102 | + Namespace: client.Namespace(), |
| 103 | + } |
| 104 | + cronWF, err := cronClient.GetCronWorkflow(ctx, &req) |
| 105 | + if err != nil { |
| 106 | + return err |
| 107 | + } |
| 108 | + cronTab, err := cron.ParseStandard(cronWF.Spec.Schedule) |
| 109 | + if err != nil { |
| 110 | + return err |
| 111 | + } |
| 112 | + scheTime := startTime |
| 113 | + priority := int32(math.MaxInt32) |
| 114 | + var scheList []string |
| 115 | + wf := common.ConvertCronWorkflowToWorkflow(cronWF) |
| 116 | + paramArg := `{{inputs.parameters.backfillscheduletime}}` |
| 117 | + wf.GenerateName = util.GenerateBackfillWorkflowPrefix(cronWF.Name, cliOps.name) + "-" |
| 118 | + param := v1alpha1.Parameter{ |
| 119 | + Name: cliOps.argName, |
| 120 | + Value: v1alpha1.AnyStringPtr(paramArg), |
| 121 | + } |
| 122 | + if !cliOps.parallel { |
| 123 | + wf.Spec.Priority = &priority |
| 124 | + wf.Spec.Synchronization = &v1alpha1.Synchronization{ |
| 125 | + Mutex: &v1alpha1.Mutex{Name: cliOps.name}, |
| 126 | + } |
| 127 | + } |
| 128 | + wf.Spec.Arguments.Parameters = append(wf.Spec.Arguments.Parameters, param) |
| 129 | + for { |
| 130 | + scheTime = cronTab.Next(scheTime) |
| 131 | + if endTime.Before(scheTime) { |
| 132 | + break |
| 133 | + } |
| 134 | + timeStr := scheTime.String() |
| 135 | + scheList = append(scheList, timeStr) |
| 136 | + } |
| 137 | + wfJsonByte, err := json.Marshal(wf) |
| 138 | + if err != nil { |
| 139 | + return err |
| 140 | + } |
| 141 | + yamlbyte, err := yaml.JSONToYAML(wfJsonByte) |
| 142 | + if err != nil { |
| 143 | + return err |
| 144 | + } |
| 145 | + wfYamlStr := "apiVersion: argoproj.io/v1alpha1 \n" + string(yamlbyte) |
| 146 | + if len(scheList) > 0 { |
| 147 | + return CreateMonitorWf(ctx, wfYamlStr, client.Namespace(), cronWFName, scheList, wfClient, cliOps) |
| 148 | + } else { |
| 149 | + fmt.Print("There is no suitable scheduling time.") |
| 150 | + } |
| 151 | + return nil |
| 152 | +} |
| 153 | + |
| 154 | +const backfillWf = `{ |
| 155 | + "apiVersion": "argoproj.io/v1alpha1", |
| 156 | + "kind": "Workflow", |
| 157 | + "metadata": { |
| 158 | + "generateName": "backfill-wf-" |
| 159 | + }, |
| 160 | + "spec": { |
| 161 | + "entrypoint": "main", |
| 162 | + "templates": [ |
| 163 | + { |
| 164 | + "name": "main", |
| 165 | + "steps": [ |
| 166 | + [ |
| 167 | + { |
| 168 | + "name": "create-workflow", |
| 169 | + "template": "create-workflow", |
| 170 | + "arguments": { |
| 171 | + "parameters": [ |
| 172 | + { |
| 173 | + "name": "backfillscheduletime", |
| 174 | + "value": "{{item}}" |
| 175 | + } |
| 176 | + ], |
| 177 | + "withParam": "{{workflows.parameters.cronscheduletime}}" |
| 178 | + } |
| 179 | + } |
| 180 | + ] |
| 181 | + ] |
| 182 | + }, |
| 183 | + { |
| 184 | + "name": "create-workflow", |
| 185 | + "inputs": { |
| 186 | + "parameters": [ |
| 187 | + { |
| 188 | + "name": "backfillscheduletime" |
| 189 | + } |
| 190 | + ] |
| 191 | + }, |
| 192 | + "resource": { |
| 193 | + "successCondition": "status.phase == Succeeded", |
| 194 | + "action": "create" |
| 195 | + } |
| 196 | + } |
| 197 | + ] |
| 198 | + } |
| 199 | +} |
| 200 | +` |
| 201 | + |
| 202 | +func CreateMonitorWf(ctx context.Context, wf, namespace, cronWFName string, scheTime []string, wfClient workflow.WorkflowServiceClient, cliOps backfillOpts) error { |
| 203 | + var monitorWfObj v1alpha1.Workflow |
| 204 | + err := json.Unmarshal([]byte(backfillWf), &monitorWfObj) |
| 205 | + if monitorWfObj.ObjectMeta.Labels == nil { |
| 206 | + monitorWfObj.ObjectMeta.Labels = make(map[string]string) |
| 207 | + } |
| 208 | + monitorWfObj.ObjectMeta.Labels[common.LabelKeyCronWorkflowBackfill] = cronWFName |
| 209 | + if err != nil { |
| 210 | + return err |
| 211 | + } |
| 212 | + TotalScheCount := len(scheTime) |
| 213 | + iterCount := int(float64(len(scheTime)/cliOps.maxWorkflowCount)) + 1 |
| 214 | + startIdx := 0 |
| 215 | + var endIdx int |
| 216 | + var wfNames []string |
| 217 | + for i := 0; i < iterCount; i++ { |
| 218 | + tmpl := monitorWfObj.GetTemplateByName("create-workflow") |
| 219 | + if (TotalScheCount - i*cliOps.maxWorkflowCount) < cliOps.maxWorkflowCount { |
| 220 | + endIdx = TotalScheCount |
| 221 | + } else { |
| 222 | + endIdx = startIdx + cliOps.maxWorkflowCount |
| 223 | + } |
| 224 | + scheTimeByte, err := json.Marshal(scheTime[startIdx:endIdx]) |
| 225 | + startIdx = endIdx |
| 226 | + if err != nil { |
| 227 | + return err |
| 228 | + } |
| 229 | + tmpl.Resource.Manifest = fmt.Sprint(wf) |
| 230 | + stepTmpl := monitorWfObj.GetTemplateByName("main") |
| 231 | + stepTmpl.Steps[0].Steps[0].WithParam = string(scheTimeByte) |
| 232 | + c, err := wfClient.CreateWorkflow(ctx, &workflow.WorkflowCreateRequest{Namespace: namespace, Workflow: &monitorWfObj}) |
| 233 | + if err != nil { |
| 234 | + return err |
| 235 | + } |
| 236 | + wfNames = append(wfNames, c.Name) |
| 237 | + } |
| 238 | + printBackFillOutput(wfNames, len(scheTime), cliOps) |
| 239 | + return nil |
| 240 | +} |
| 241 | + |
| 242 | +func printBackFillOutput(wfNames []string, totalSches int, cliOps backfillOpts) { |
| 243 | + fmt.Printf("Created %s Backfill task for Cronworkflow %s \n", cliOps.name, cliOps.cronWfName) |
| 244 | + fmt.Printf("==================================================\n") |
| 245 | + fmt.Printf("Backfill Period :\n") |
| 246 | + fmt.Printf("Start Time : %s \n", cliOps.startDate) |
| 247 | + fmt.Printf(" End Time : %s \n", cliOps.endDate) |
| 248 | + fmt.Printf("Total Backfill Schedule: %d \n", totalSches) |
| 249 | + fmt.Printf("==================================================\n") |
| 250 | + fmt.Printf("Backfill Workflows: \n") |
| 251 | + fmt.Printf(" NAMESPACE\t WORKFLOW: \n") |
| 252 | + namespace := client.Namespace() |
| 253 | + for idx, wfName := range wfNames { |
| 254 | + fmt.Printf("%d. %s \t %s \n", idx+1, namespace, wfName) |
| 255 | + } |
| 256 | +} |
0 commit comments