diff --git a/charts/fleet-crd/templates/crds.yaml b/charts/fleet-crd/templates/crds.yaml index b15a8037e8..862d82f42b 100644 --- a/charts/fleet-crd/templates/crds.yaml +++ b/charts/fleet-crd/templates/crds.yaml @@ -290,6 +290,12 @@ spec: nullable: true type: array type: object + schedule: + nullable: true + type: string + scheduleWindow: + nullable: true + type: string serviceAccount: nullable: true type: string @@ -556,6 +562,12 @@ spec: namespace: nullable: true type: string + schedule: + nullable: true + type: string + scheduleWindow: + nullable: true + type: string serviceAccount: nullable: true type: string @@ -1060,6 +1072,12 @@ spec: namespace: nullable: true type: string + schedule: + nullable: true + type: string + scheduleWindow: + nullable: true + type: string serviceAccount: nullable: true type: string @@ -1203,6 +1221,12 @@ spec: namespace: nullable: true type: string + schedule: + nullable: true + type: string + scheduleWindow: + nullable: true + type: string serviceAccount: nullable: true type: string @@ -1328,6 +1352,11 @@ spec: release: nullable: true type: string + scheduled: + type: boolean + scheduledAt: + nullable: true + type: string syncGeneration: nullable: true type: integer @@ -1763,6 +1792,12 @@ spec: type: boolean redeployAgentGeneration: type: integer + schedule: + nullable: true + type: string + scheduleWindow: + nullable: true + type: string type: object status: properties: @@ -2961,6 +2996,12 @@ spec: nullable: true type: array type: object + schedule: + nullable: true + type: string + scheduleWindow: + nullable: true + type: string serviceAccount: nullable: true type: string @@ -3227,6 +3268,12 @@ spec: namespace: nullable: true type: string + schedule: + nullable: true + type: string + scheduleWindow: + nullable: true + type: string serviceAccount: nullable: true type: string @@ -3732,6 +3779,12 @@ spec: namespace: nullable: true type: string + schedule: + nullable: true + type: string + scheduleWindow: + nullable: true + type: string serviceAccount: nullable: true type: string @@ -3875,6 +3928,12 @@ spec: namespace: nullable: true type: string + schedule: + nullable: true + type: string + scheduleWindow: + nullable: true + type: string serviceAccount: nullable: true type: string @@ -4000,6 +4059,11 @@ spec: release: nullable: true type: string + scheduled: + type: boolean + scheduledAt: + nullable: true + type: string syncGeneration: nullable: true type: integer @@ -4438,6 +4502,12 @@ spec: type: boolean redeployAgentGeneration: type: integer + schedule: + nullable: true + type: string + scheduleWindow: + nullable: true + type: string type: object status: properties: diff --git a/go.mod b/go.mod index 6f8935b3b1..da995c34af 100644 --- a/go.mod +++ b/go.mod @@ -49,6 +49,7 @@ require ( github.com/rancher/lasso v0.0.0-20210616224652-fc3ebd901c08 github.com/rancher/wrangler v0.8.4 github.com/rancher/wrangler-cli v0.0.0-20200815040857-81c48cf8ab43 + github.com/robfig/cron v1.1.0 // indirect github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.1.3 github.com/stretchr/testify v1.7.0 diff --git a/modules/agent/pkg/controllers/bundledeployment/controller.go b/modules/agent/pkg/controllers/bundledeployment/controller.go index d267b59d96..d2d9d33359 100644 --- a/modules/agent/pkg/controllers/bundledeployment/controller.go +++ b/modules/agent/pkg/controllers/bundledeployment/controller.go @@ -8,6 +8,8 @@ import ( "sync" "time" + "github.com/robfig/cron" + "github.com/rancher/fleet/modules/agent/pkg/deployer" "github.com/rancher/fleet/modules/agent/pkg/trigger" fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" @@ -90,6 +92,55 @@ func (h *handler) Cleanup(key string, bd *fleet.BundleDeployment) (*fleet.Bundle } func (h *handler) DeployBundle(bd *fleet.BundleDeployment, status fleet.BundleDeploymentStatus) (fleet.BundleDeploymentStatus, error) { + + if bd.Spec.Options.Schedule != "" && status.ScheduledAt == "" { + cronSched, err := cron.ParseStandard(bd.Spec.Options.Schedule) + if err != nil { + return status, err + } + scheduledRun := cronSched.Next(time.Now()) + after := scheduledRun.Sub(time.Now()) + h.bdController.EnqueueAfter(bd.Namespace, bd.Name, after) + status.ScheduledAt = scheduledRun.Format(time.RFC3339) + status.Scheduled = true + condition.Cond(fleet.BundleScheduledCondition).SetStatusBool(&status, true) + condition.Cond(fleet.BundleDeploymentConditionDeployed).SetStatusBool(&status, false) + return status, nil + } + + if bd.Spec.Options.Schedule != "" && status.ScheduledAt != "" { + nextRun, err := time.Parse(time.RFC3339, status.ScheduledAt) + if err != nil { + return status, err + } + window := fleet.DefaultWindow + if bd.Spec.Options.ScheduleWindow != "" { + window = bd.Spec.Options.ScheduleWindow + } + + windowDuration, err := time.ParseDuration(window) + if err != nil { + return status, err + } + + if err != nil { + return status, err + } + if nextRun.After(time.Now()) { + after := nextRun.Sub(time.Now()) + h.bdController.EnqueueAfter(bd.Namespace, bd.Name, after) + return status, nil + } + + // case of disconnected agent during the actual window // + if nextRun.Add(windowDuration).Before(time.Now()) { + // clean up scheduled at to allow object to fall through scheduling + status.ScheduledAt = "" + status.Scheduled = false + return status, nil + } + } + depBundles, ok, err := h.checkDependency(bd) if err != nil { return status, err @@ -108,6 +159,7 @@ func (h *handler) DeployBundle(bd *fleet.BundleDeployment, status fleet.BundleDe if err != nil { return status, err } + status.Scheduled = false status.Release = release status.AppliedDeploymentID = bd.Spec.DeploymentID return status, nil @@ -219,6 +271,11 @@ func (h *handler) cleanupOldAgent(modifiedStatuses []fleet.ModifiedStatus) error } func (h *handler) MonitorBundle(bd *fleet.BundleDeployment, status fleet.BundleDeploymentStatus) (fleet.BundleDeploymentStatus, error) { + + if status.Scheduled { + return status, nil + } + if bd.Spec.DeploymentID != status.AppliedDeploymentID { return status, nil } @@ -271,6 +328,8 @@ func readyError(status fleet.BundleDeploymentStatus) error { if len(status.ModifiedStatus) > 0 { msg = status.ModifiedStatus[0].String() } + } else if status.Scheduled { + msg = "scheduled" } return errors.New(msg) diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/bundle.go b/pkg/apis/fleet.cattle.io/v1alpha1/bundle.go index 711a097538..a594c2675f 100644 --- a/pkg/apis/fleet.cattle.io/v1alpha1/bundle.go +++ b/pkg/apis/fleet.cattle.io/v1alpha1/bundle.go @@ -31,6 +31,8 @@ var ( } ) +const DefaultWindow = "1h" + type BundleState string // +genclient @@ -134,6 +136,7 @@ var ( BundleConditionReady = "Ready" BundleDeploymentConditionReady = "Ready" BundleDeploymentConditionDeployed = "Deployed" + BundleScheduledCondition = "Scheduled" ) type BundleStatus struct { @@ -192,6 +195,8 @@ type BundleDeploymentOptions struct { ForceSyncGeneration int64 `json:"forceSyncGeneration,omitempty"` YAML *YAMLOptions `json:"yaml,omitempty"` Diff *DiffOptions `json:"diff,omitempty"` + Schedule string `json:"schedule,omitempty"` + ScheduleWindow string `json:"scheduleWindow,omitempty"` } type DiffOptions struct { @@ -279,10 +284,12 @@ type BundleDeploymentStatus struct { Release string `json:"release,omitempty"` Ready bool `json:"ready,omitempty"` NonModified bool `json:"nonModified,omitempty"` + Scheduled bool `json:"scheduled,omitempty"` NonReadyStatus []NonReadyStatus `json:"nonReadyStatus,omitempty"` ModifiedStatus []ModifiedStatus `json:"modifiedStatus,omitempty"` Display BundleDeploymentDisplay `json:"display,omitempty"` SyncGeneration *int64 `json:"syncGeneration,omitempty"` + ScheduledAt string `json:"scheduledAt,omitempty"` } type BundleDeploymentDisplay struct { diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/target.go b/pkg/apis/fleet.cattle.io/v1alpha1/target.go index 132a0df6ff..f0a837d82b 100644 --- a/pkg/apis/fleet.cattle.io/v1alpha1/target.go +++ b/pkg/apis/fleet.cattle.io/v1alpha1/target.go @@ -66,6 +66,8 @@ type ClusterSpec struct { KubeConfigSecret string `json:"kubeConfigSecret,omitempty"` RedeployAgentGeneration int64 `json:"redeployAgentGeneration,omitempty"` AgentEnvVars []v1.EnvVar `json:"agentEnvVars,omitempty"` + Schedule string `json:"schedule,omitempty"` + ScheduleWindow string `json:"scheduleWindow,omitempty"` AgentNamespace string `json:"agentNamespace,omitempty"` } diff --git a/pkg/controllers/bundle/controller.go b/pkg/controllers/bundle/controller.go index 6218f3e233..cba87c94d7 100644 --- a/pkg/controllers/bundle/controller.go +++ b/pkg/controllers/bundle/controller.go @@ -3,6 +3,7 @@ package bundle import ( "context" "sort" + "strings" fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" fleetcontrollers "github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io/v1alpha1" @@ -232,6 +233,7 @@ func toRuntimeObjects(targets []*target.Target, bundle *fleet.Bundle) (result [] if target.Deployment == nil { continue } + dp := &fleet.BundleDeployment{ ObjectMeta: v1.ObjectMeta{ Name: target.Deployment.Name, @@ -241,6 +243,19 @@ func toRuntimeObjects(targets []*target.Target, bundle *fleet.Bundle) (result [] Spec: target.Deployment.Spec, } dp.Spec.DependsOn = bundle.Spec.DependsOn + + // Cluster level schedule and window takes priority over bundle specific schedule and window + // Ensure cluster schedule is always applied to all bundles + if !strings.HasPrefix(bundle.Name, "fleet-agent") { + if target.Cluster.Spec.Schedule != "" { + dp.Spec.Options.Schedule = target.Cluster.Spec.Schedule + } + + if target.Cluster.Spec.ScheduleWindow != "" { + dp.Spec.Options.ScheduleWindow = target.Cluster.Spec.ScheduleWindow + } + } + result = append(result, dp) }