-
Notifications
You must be signed in to change notification settings - Fork 706
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add custom per-repo interval in AppRepo #5599
Add custom per-repo interval in AppRepo #5599
Conversation
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
✅ Deploy Preview for kubeapps-dev ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify site settings. |
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Antonio, nice clean change :)
One caveat is that the upgrade process: since we are creating a CronJob and passing the crontab value on creation via the apprepository controller, from the Repos API we don't have a clear way to edit the cronjob once generated.
* In this PR we've addressed it by just disabling the field when upgrading
I think this should be explained in a bit more detail in the code, or in an issue, that can be referenced in the code. I'd currently have to dig a bit to understand why we can't delete and re-create the CronJob
when updating the AppRepository
CRD.
cronTime, err = intervalToCron(apprepo.Spec.Interval) | ||
} | ||
// If the interval is invalid, use the default global crontab | ||
if err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm - I know we don't yet have any way to report this back to the user (other than failing the operation, which isn't great either), so I reckon it'd be useful to at least log this error so that we can help debug if there's an issue?
// Set to replace as short-circuit in k8s <1.12 | ||
// TODO re-evaluate ConcurrentPolicy when 1.12+ is mainstream (i.e 1.14) | ||
// https://github.com/kubernetes/kubernetes/issues/54870 | ||
ConcurrencyPolicy: "Replace", | ||
ConcurrencyPolicy: batchv1.ReplaceConcurrent, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dunno, but does this change to the concurrency policy mean we can remove the above comment? EDIT: hah, or it's not a change at all, just switch to constant :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly, I just replaced the Replace
string with the constant coming from the k8s package. Below the different options:
const (
// AllowConcurrent allows CronJobs to run concurrently.
AllowConcurrent ConcurrencyPolicy = "Allow"
// ForbidConcurrent forbids concurrent runs, skipping next run if previous
// hasn't finished yet.
ForbidConcurrent ConcurrencyPolicy = "Forbid"
// ReplaceConcurrent cancels currently running job and replaces it with a new one.
ReplaceConcurrent ConcurrencyPolicy = "Replace"
)
{ | ||
name: "good interval, every two hours", | ||
interval: "2h", | ||
expectedCron: "*/120 * * * *", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that actually valid? I thought Cron was stateless (and so effectively reset at the turn of each hour for a minutes range here). I'm not 100%, but worth checking (see some notes on the wikipedia article - not a source of truth obviously).
Could be worth using https://pkg.go.dev/github.com/robfig/cron#Every , not sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➕ I don't know either if that's even valid, but besides that, I think that if we allow values higher than 60 min
, the intervalToCron
function should turn this kind of value to * */2 * * *
, which is a more readable cron expression.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your comments and pointers, I've had to revisit my knowledge about how cron
works internally.
The expression I was building is syntactically valid, that's why I thought it was valid (=meaningful). However, it is semantically pointless given how the cron works.
Kubernetes is using the vixie-cron
implementation, the POSIX-compliant implementation developed for BSD (back in 1988).
NOTE: There are some other implementations, like dcron
, fcron
, bcron
and cronie
. The latter, is a vixie-cron
fork maintained by Fedora where, for instance, does validate the step
field and therefore makes the discussed expression invalid syntactically.
Well, so coming back to the vixie implementation, the step can be any number:
https://github.com/vixie/cron/blob/2e298fc87bf6f9220d09e60162ebf909a607c4fb/entry.c#L518-L521
But, it is going to, silently, drop the invalid values:
https://github.com/vixie/cron/blob/2e298fc87bf6f9220d09e60162ebf909a607c4fb/entry.c#L580-L584
In short, as you pointed out, in an interval 0-59
, the only possible values for step are [1-59]
.
Unfortunately, this is something that k8s is NOT validating:
k get cronjob
NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE
kubernetes-cron-job */61 * * * * False 0 <none> 20m
Note the "last schedule" equals to "none"
The library you pointed out seems to be a scheduler itself, as it does not generate the crontab thing, but actually runs a timer to calculate the next executions.
So, it seems we might need to make the function a bit smarter :S and limit the values + perform the proper conversions between time units.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The library you pointed out seems to be a scheduler itself, as it does not generate the crontab thing, but actually runs a timer to calculate the next executions.
Yes, sorry - I realised the library was for a scheduler, but had thought that function I linked to was for converting a duration into a crontab format, but seems it is not.
Type: "helm", | ||
Url: "http://example.com", | ||
NamespaceScoped: true, | ||
Interval: "99m", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, not sure that's valid. We could even simplify things by just using a drop-down selection in the UX (and validating valid values of 1, 2, ..., 15, 20, 30 etc.
requestCustomizer: func(request *corev1.UpdatePackageRepositoryRequest) *corev1.UpdatePackageRepositoryRequest { | ||
request.Interval = "1s" | ||
request.Interval = "99m" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, maybe use 15m as a valid interval.
func intervalToCron(duration string) (string, error) { | ||
if duration == "" { | ||
return "", fmt.Errorf("duration cannot be empty") | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The else
branch here is redundant. IMHO the code would be easier to follow if you get rid of it.
} else { | ||
if d, err := time.ParseDuration(duration); err != nil { | ||
return "", err | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same here.
{ | ||
name: "good interval, every two hours", | ||
interval: "2h", | ||
expectedCron: "*/120 * * * *", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➕ I don't know either if that's even valid, but besides that, I think that if we allow values higher than 60 min
, the intervalToCron
function should turn this kind of value to * */2 * * *
, which is a more readable cron expression.
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
After re-reading it, you're right, there's something wrong. The CR controller should be smart enough to detect a change in the definition to recreate the cronjob... the problem is in how we consider two AppRepos are equals. Below the excerpt of the code: if oldApp.Spec.URL != newApp.Spec.URL || oldApp.Spec.ResyncRequests != newApp.Spec.ResyncRequests {
controller.enqueueAppRepo(newApp)
} That's why the "update" branch of the controller wasn't being triggered. Fixing this in the next commit. Thansk for the heads-up! |
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
if oldApp.Spec.URL != newApp.Spec.URL || oldApp.Spec.ResyncRequests != newApp.Spec.ResyncRequests { | ||
if !reflect.DeepEqual(oldApp.Spec, newApp.Spec) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@absoludity, do you know if there's any reason for checking the equality manually instead of just using reflect.DeepEqual
as I'm proposing herein? I guess that a change in the AppRepo spec should trigger an update in the controller, doesn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. I can't see anything on that AppRepo Spec struct that, if changed, should not trigger an update. Agree 100%.
if cronMins < 60 { | ||
// https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#cron-schedule-syntax | ||
// minute(0-59) hour(0-23) dayOfMonth(1-31) month(1-12) dayOfWeek(0-6) | ||
return fmt.Sprintf("*/%v * * * *", cronMins), nil // every cronMins minutes | ||
} | ||
|
||
cronHours := math.Ceil(d.Hours()) // round up to nearest hour | ||
if cronHours < 24 { | ||
return fmt.Sprintf("0 */%v * * *", cronHours), nil // every cronHours hours | ||
} | ||
|
||
cronDays := math.Ceil(cronHours / 24) // get the days | ||
if cronDays < 32 { | ||
return fmt.Sprintf("0 0 */%v * *", cronDays), nil // every cronHoursDays days | ||
} | ||
|
||
cronMonths := math.Ceil(cronDays / 31) // get the months | ||
if cronMonths < 13 { | ||
return fmt.Sprintf("0 0 1 */%v *", cronMonths), nil // every cronHoursMonths months | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New logic for roughly estimating the cron line from a duration
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice - thanks for the update. Just note, small typo in the inline comments on lines 451 and 456 (s/Hours//)
if gron.IsValid(apprepo.Spec.Interval) { | ||
cronTime = apprepo.Spec.Interval | ||
} else { | ||
// otherwise, convert it | ||
cronTime, err = intervalToCron(apprepo.Spec.Interval) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since people might want to specify their own cron lines, this Helm plugin is accepting a valid cron expression and, if invalid, it will try to pasre it as a go duration. This brings more flexibility, but it differs from the API spec a little bit. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, handy I think. I'd be ok with that (and updating the API spec to match, if we want - still backwards compatible so a nice addition, imo). Nice one!
if oldApp.Spec.URL != newApp.Spec.URL || oldApp.Spec.ResyncRequests != newApp.Spec.ResyncRequests { | ||
if !reflect.DeepEqual(oldApp.Spec, newApp.Spec) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. I can't see anything on that AppRepo Spec struct that, if changed, should not trigger an update. Agree 100%.
if cronMins < 60 { | ||
// https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#cron-schedule-syntax | ||
// minute(0-59) hour(0-23) dayOfMonth(1-31) month(1-12) dayOfWeek(0-6) | ||
return fmt.Sprintf("*/%v * * * *", cronMins), nil // every cronMins minutes | ||
} | ||
|
||
cronHours := math.Ceil(d.Hours()) // round up to nearest hour | ||
if cronHours < 24 { | ||
return fmt.Sprintf("0 */%v * * *", cronHours), nil // every cronHours hours | ||
} | ||
|
||
cronDays := math.Ceil(cronHours / 24) // get the days | ||
if cronDays < 32 { | ||
return fmt.Sprintf("0 0 */%v * *", cronDays), nil // every cronHoursDays days | ||
} | ||
|
||
cronMonths := math.Ceil(cronDays / 31) // get the months | ||
if cronMonths < 13 { | ||
return fmt.Sprintf("0 0 1 */%v *", cronMonths), nil // every cronHoursMonths months | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice - thanks for the update. Just note, small typo in the inline comments on lines 451 and 456 (s/Hours//)
if gron.IsValid(apprepo.Spec.Interval) { | ||
cronTime = apprepo.Spec.Interval | ||
} else { | ||
// otherwise, convert it | ||
cronTime, err = intervalToCron(apprepo.Spec.Interval) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, handy I think. I'd be ok with that (and updating the API spec to match, if we want - still backwards compatible so a nice addition, imo). Nice one!
Signed-off-by: Antonio Gamez Diaz <agamez@vmware.com>
// Some plugins may, addionally, support other formats, for instance, | ||
// a cron expression. | ||
// e.g., "*/10 * * * *" will be equivalent to "10m" | ||
// Optional. Defaults to 10m if not specified | ||
string interval = 7; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added this note to document the fact that other plugins might support other formats while also supporting the "time+unit" one.
Description of the change
This PR is adding a new field (
Interval
) in theAppRepository
CRD. This field will override the global defaultcrontab
used for every repository, which is aligned with the recent Repositories API we defined.The PR includes every required change across the components, namely:
Interval
field in the CR definition.apprepository-controller
and use it; otherwise, use the default one.Benefits
Helm repos can have customizable intervals now.
Possible drawbacks
IRL test is still pending. I don't know if adding a field in the CRD is breaking something, though we already added some fields in the past with no impact, but want to re-check just in case.Applicable issues
Additional information
After the IRL test, some notes: