diff --git a/apis/.schemes/config-v1alpha1-LandscaperConfiguration.json b/apis/.schemes/config-v1alpha1-LandscaperConfiguration.json index 941be2aca..dc2bac2b2 100755 --- a/apis/.schemes/config-v1alpha1-LandscaperConfiguration.json +++ b/apis/.schemes/config-v1alpha1-LandscaperConfiguration.json @@ -39,6 +39,24 @@ } } }, + "config-v1alpha1-DeployItemTimeouts": { + "description": "DeployItemTimeouts contains multiple timeout configurations for deploy items", + "type": "object", + "properties": { + "abort": { + "description": "Abort specifies how long the deployer may take to abort handling a deploy item after getting the abort annotation. Allowed values are 'none' (to disable abort timeout detection) and anything that is understood by golang's time.ParseDuration method. Defaults to five minutes if not specified.", + "$ref": "#/definitions/core-v1alpha1-Duration" + }, + "pickup": { + "description": "PickupTimeout defines how long a deployer can take to react on changes to a deploy item before the landscaper will mark it as failed. Allowed values are 'none' (to disable pickup timeout detection) and anything that is understood by golang's time.ParseDuration method. Defaults to five minutes if not specified.", + "$ref": "#/definitions/core-v1alpha1-Duration" + }, + "progressingDefault": { + "description": "ProgressingDefault specifies how long the deployer may take to apply a deploy item by default. The value can be overwritten per deploy item in 'spec.timeout'. Allowed values are 'none' (to disable abort timeout detection) and anything that is understood by golang's time.ParseDuration method. Defaults to ten minutes if not specified.", + "$ref": "#/definitions/core-v1alpha1-Duration" + } + } + }, "config-v1alpha1-LocalRegistryConfiguration": { "description": "LocalRegistryConfiguration contains the configuration for a local registry", "type": "object", @@ -128,6 +146,10 @@ "$ref": "#/definitions/config-v1alpha1-OCIConfiguration" } } + }, + "core-v1alpha1-Duration": { + "description": "Duration is a wrapper for time.Duration that implements JSON marshalling and openapi scheme.", + "type": "string" } }, "description": "LandscaperConfiguration contains all configuration for the landscaper controllers", @@ -140,9 +162,9 @@ "$ref": "#/definitions/config-v1alpha1-CrdManagementConfiguration", "description": "CrdManagement configures whether the landscaper controller should deploy the CRDs it needs into the cluster" }, - "deployItemPickupTimeout": { - "description": "DeployItemPickupTimeout defines how long a deployer can take to react on changes to a deploy item before the landscaper will mark it as failed. Allowed values are 'none' (to disable pickup timeout detection) and anything that is understood by golang's time.ParseDuration method. Defaults to five minutes if not specified.", - "type": "string" + "deployItemTimeouts": { + "$ref": "#/definitions/config-v1alpha1-DeployItemTimeouts", + "description": "DeployItemTimeouts contains configuration for multiple deploy item timeouts" }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", diff --git a/apis/.schemes/mock-v1alpha1-ProviderConfiguration.json b/apis/.schemes/mock-v1alpha1-ProviderConfiguration.json index 1baad82a3..5e5f36c08 100755 --- a/apis/.schemes/mock-v1alpha1-ProviderConfiguration.json +++ b/apis/.schemes/mock-v1alpha1-ProviderConfiguration.json @@ -17,6 +17,10 @@ "format": "byte", "type": "string" }, + "initialPhase": { + "description": "InitialPhase sets the phase of the DeployItem, but only if it is empty or \"Init\" Additionally, setting it will suppress the DeployItem phase being set to \"Succeeded\" after successful reconciliation", + "type": "string" + }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string" diff --git a/apis/config/types_landscaper_config.go b/apis/config/types_landscaper_config.go index 33f95d85e..834738dab 100644 --- a/apis/config/types_landscaper_config.go +++ b/apis/config/types_landscaper_config.go @@ -7,6 +7,8 @@ package config import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + lscore "github.com/gardener/landscaper/apis/core" ) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -25,11 +27,28 @@ type LandscaperConfiguration struct { // CrdManagement configures whether the landscaper controller should deploy the CRDs it needs into the cluster // +optional CrdManagement *CrdManagementConfiguration `json:"crdManagement,omitempty"` - // DeployItemPickupTimeout defines how long a deployer can take to react on changes to a deploy item before the landscaper will mark it as failed. + // DeployItemTimeouts contains configuration for multiple deploy item timeouts + // +optional + DeployItemTimeouts *DeployItemTimeouts `json:"deployItemTimeouts,omitempty"` +} + +// DeployItemTimeouts contains multiple timeout configurations for deploy items +type DeployItemTimeouts struct { + // PickupTimeout defines how long a deployer can take to react on changes to a deploy item before the landscaper will mark it as failed. // Allowed values are 'none' (to disable pickup timeout detection) and anything that is understood by golang's time.ParseDuration method. // Defaults to five minutes if not specified. // +optional - DeployItemPickupTimeout string `json:"deployItemPickupTimeout,omitempty"` + Pickup *lscore.Duration `json:"pickup,omitempty"` + // Abort specifies how long the deployer may take to abort handling a deploy item after getting the abort annotation. + // Allowed values are 'none' (to disable abort timeout detection) and anything that is understood by golang's time.ParseDuration method. + // Defaults to five minutes if not specified. + // +optional + Abort *lscore.Duration `json:"abort,omitempty"` + // ProgressingDefault specifies how long the deployer may take to apply a deploy item by default. The value can be overwritten per deploy item in 'spec.timeout'. + // Allowed values are 'none' (to disable abort timeout detection) and anything that is understood by golang's time.ParseDuration method. + // Defaults to ten minutes if not specified. + // +optional + ProgressingDefault *lscore.Duration `json:"progressingDefault,omitempty"` } // RegistryConfiguration contains the configuration for the used definition registry diff --git a/apis/config/v1alpha1/defaults.go b/apis/config/v1alpha1/defaults.go index 4b84e9a2f..c0844c7ab 100644 --- a/apis/config/v1alpha1/defaults.go +++ b/apis/config/v1alpha1/defaults.go @@ -5,7 +5,11 @@ package v1alpha1 import ( + "time" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/gardener/landscaper/apis/core/v1alpha1" ) func addDefaultingFuncs(scheme *runtime.Scheme) error { @@ -22,8 +26,17 @@ func SetDefaults_LandscaperConfiguration(obj *LandscaperConfiguration) { UseInMemoryOverlay: false, } } - if len(obj.DeployItemPickupTimeout) == 0 { - obj.DeployItemPickupTimeout = "5m" + if obj.DeployItemTimeouts == nil { + obj.DeployItemTimeouts = &DeployItemTimeouts{} + } + if obj.DeployItemTimeouts.Pickup == nil { + obj.DeployItemTimeouts.Pickup = &v1alpha1.Duration{Duration: 5 * time.Minute} + } + if obj.DeployItemTimeouts.Abort == nil { + obj.DeployItemTimeouts.Abort = &v1alpha1.Duration{Duration: 5 * time.Minute} + } + if obj.DeployItemTimeouts.ProgressingDefault == nil { + obj.DeployItemTimeouts.ProgressingDefault = &v1alpha1.Duration{Duration: 10 * time.Minute} } } diff --git a/apis/config/v1alpha1/types_landscaper_config.go b/apis/config/v1alpha1/types_landscaper_config.go index 88b9bac14..4f9cde06f 100644 --- a/apis/config/v1alpha1/types_landscaper_config.go +++ b/apis/config/v1alpha1/types_landscaper_config.go @@ -7,6 +7,8 @@ package v1alpha1 import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" ) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -25,11 +27,28 @@ type LandscaperConfiguration struct { // CrdManagement configures whether the landscaper controller should deploy the CRDs it needs into the cluster // +optional CrdManagement *CrdManagementConfiguration `json:"crdManagement,omitempty"` - // DeployItemPickupTimeout defines how long a deployer can take to react on changes to a deploy item before the landscaper will mark it as failed. + // DeployItemTimeouts contains configuration for multiple deploy item timeouts + // +optional + DeployItemTimeouts *DeployItemTimeouts `json:"deployItemTimeouts,omitempty"` +} + +// DeployItemTimeouts contains multiple timeout configurations for deploy items +type DeployItemTimeouts struct { + // PickupTimeout defines how long a deployer can take to react on changes to a deploy item before the landscaper will mark it as failed. // Allowed values are 'none' (to disable pickup timeout detection) and anything that is understood by golang's time.ParseDuration method. // Defaults to five minutes if not specified. // +optional - DeployItemPickupTimeout string `json:"deployItemPickupTimeout,omitempty"` + Pickup *lsv1alpha1.Duration `json:"pickup,omitempty"` + // Abort specifies how long the deployer may take to abort handling a deploy item after getting the abort annotation. + // Allowed values are 'none' (to disable abort timeout detection) and anything that is understood by golang's time.ParseDuration method. + // Defaults to five minutes if not specified. + // +optional + Abort *lsv1alpha1.Duration `json:"abort,omitempty"` + // ProgressingDefault specifies how long the deployer may take to apply a deploy item by default. The value can be overwritten per deploy item in 'spec.timeout'. + // Allowed values are 'none' (to disable abort timeout detection) and anything that is understood by golang's time.ParseDuration method. + // Defaults to ten minutes if not specified. + // +optional + ProgressingDefault *lsv1alpha1.Duration `json:"progressingDefault,omitempty"` } // RegistryConfiguration contains the configuration for the used definition registry diff --git a/apis/config/v1alpha1/zz_generated.conversion.go b/apis/config/v1alpha1/zz_generated.conversion.go index baa8fa122..e42211302 100644 --- a/apis/config/v1alpha1/zz_generated.conversion.go +++ b/apis/config/v1alpha1/zz_generated.conversion.go @@ -17,6 +17,8 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" config "github.com/gardener/landscaper/apis/config" + core "github.com/gardener/landscaper/apis/core" + corev1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" ) func init() { @@ -36,6 +38,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*DeployItemTimeouts)(nil), (*config.DeployItemTimeouts)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_DeployItemTimeouts_To_config_DeployItemTimeouts(a.(*DeployItemTimeouts), b.(*config.DeployItemTimeouts), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*config.DeployItemTimeouts)(nil), (*DeployItemTimeouts)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_config_DeployItemTimeouts_To_v1alpha1_DeployItemTimeouts(a.(*config.DeployItemTimeouts), b.(*DeployItemTimeouts), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*LandscaperConfiguration)(nil), (*config.LandscaperConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_LandscaperConfiguration_To_config_LandscaperConfiguration(a.(*LandscaperConfiguration), b.(*config.LandscaperConfiguration), scope) }); err != nil { @@ -121,6 +133,30 @@ func Convert_config_CrdManagementConfiguration_To_v1alpha1_CrdManagementConfigur return autoConvert_config_CrdManagementConfiguration_To_v1alpha1_CrdManagementConfiguration(in, out, s) } +func autoConvert_v1alpha1_DeployItemTimeouts_To_config_DeployItemTimeouts(in *DeployItemTimeouts, out *config.DeployItemTimeouts, s conversion.Scope) error { + out.Pickup = (*core.Duration)(unsafe.Pointer(in.Pickup)) + out.Abort = (*core.Duration)(unsafe.Pointer(in.Abort)) + out.ProgressingDefault = (*core.Duration)(unsafe.Pointer(in.ProgressingDefault)) + return nil +} + +// Convert_v1alpha1_DeployItemTimeouts_To_config_DeployItemTimeouts is an autogenerated conversion function. +func Convert_v1alpha1_DeployItemTimeouts_To_config_DeployItemTimeouts(in *DeployItemTimeouts, out *config.DeployItemTimeouts, s conversion.Scope) error { + return autoConvert_v1alpha1_DeployItemTimeouts_To_config_DeployItemTimeouts(in, out, s) +} + +func autoConvert_config_DeployItemTimeouts_To_v1alpha1_DeployItemTimeouts(in *config.DeployItemTimeouts, out *DeployItemTimeouts, s conversion.Scope) error { + out.Pickup = (*corev1alpha1.Duration)(unsafe.Pointer(in.Pickup)) + out.Abort = (*corev1alpha1.Duration)(unsafe.Pointer(in.Abort)) + out.ProgressingDefault = (*corev1alpha1.Duration)(unsafe.Pointer(in.ProgressingDefault)) + return nil +} + +// Convert_config_DeployItemTimeouts_To_v1alpha1_DeployItemTimeouts is an autogenerated conversion function. +func Convert_config_DeployItemTimeouts_To_v1alpha1_DeployItemTimeouts(in *config.DeployItemTimeouts, out *DeployItemTimeouts, s conversion.Scope) error { + return autoConvert_config_DeployItemTimeouts_To_v1alpha1_DeployItemTimeouts(in, out, s) +} + func autoConvert_v1alpha1_LandscaperConfiguration_To_config_LandscaperConfiguration(in *LandscaperConfiguration, out *config.LandscaperConfiguration, s conversion.Scope) error { out.RepositoryContext = (*v2.RepositoryContext)(unsafe.Pointer(in.RepositoryContext)) if err := Convert_v1alpha1_RegistryConfiguration_To_config_RegistryConfiguration(&in.Registry, &out.Registry, s); err != nil { @@ -128,7 +164,7 @@ func autoConvert_v1alpha1_LandscaperConfiguration_To_config_LandscaperConfigurat } out.Metrics = (*config.MetricsConfiguration)(unsafe.Pointer(in.Metrics)) out.CrdManagement = (*config.CrdManagementConfiguration)(unsafe.Pointer(in.CrdManagement)) - out.DeployItemPickupTimeout = in.DeployItemPickupTimeout + out.DeployItemTimeouts = (*config.DeployItemTimeouts)(unsafe.Pointer(in.DeployItemTimeouts)) return nil } @@ -144,7 +180,7 @@ func autoConvert_config_LandscaperConfiguration_To_v1alpha1_LandscaperConfigurat } out.Metrics = (*MetricsConfiguration)(unsafe.Pointer(in.Metrics)) out.CrdManagement = (*CrdManagementConfiguration)(unsafe.Pointer(in.CrdManagement)) - out.DeployItemPickupTimeout = in.DeployItemPickupTimeout + out.DeployItemTimeouts = (*DeployItemTimeouts)(unsafe.Pointer(in.DeployItemTimeouts)) return nil } diff --git a/apis/config/v1alpha1/zz_generated.deepcopy.go b/apis/config/v1alpha1/zz_generated.deepcopy.go index 8b191e571..2e736c412 100644 --- a/apis/config/v1alpha1/zz_generated.deepcopy.go +++ b/apis/config/v1alpha1/zz_generated.deepcopy.go @@ -12,6 +12,8 @@ package v1alpha1 import ( v2 "github.com/gardener/component-spec/bindings-go/apis/v2" runtime "k8s.io/apimachinery/pkg/runtime" + + corev1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -30,6 +32,37 @@ func (in *CrdManagementConfiguration) DeepCopy() *CrdManagementConfiguration { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeployItemTimeouts) DeepCopyInto(out *DeployItemTimeouts) { + *out = *in + if in.Pickup != nil { + in, out := &in.Pickup, &out.Pickup + *out = new(corev1alpha1.Duration) + **out = **in + } + if in.Abort != nil { + in, out := &in.Abort, &out.Abort + *out = new(corev1alpha1.Duration) + **out = **in + } + if in.ProgressingDefault != nil { + in, out := &in.ProgressingDefault, &out.ProgressingDefault + *out = new(corev1alpha1.Duration) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeployItemTimeouts. +func (in *DeployItemTimeouts) DeepCopy() *DeployItemTimeouts { + if in == nil { + return nil + } + out := new(DeployItemTimeouts) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LandscaperConfiguration) DeepCopyInto(out *LandscaperConfiguration) { *out = *in @@ -50,6 +83,11 @@ func (in *LandscaperConfiguration) DeepCopyInto(out *LandscaperConfiguration) { *out = new(CrdManagementConfiguration) **out = **in } + if in.DeployItemTimeouts != nil { + in, out := &in.DeployItemTimeouts, &out.DeployItemTimeouts + *out = new(DeployItemTimeouts) + (*in).DeepCopyInto(*out) + } return } diff --git a/apis/config/zz_generated.deepcopy.go b/apis/config/zz_generated.deepcopy.go index 9aacffe31..e245ab0c5 100644 --- a/apis/config/zz_generated.deepcopy.go +++ b/apis/config/zz_generated.deepcopy.go @@ -12,6 +12,8 @@ package config import ( v2 "github.com/gardener/component-spec/bindings-go/apis/v2" runtime "k8s.io/apimachinery/pkg/runtime" + + core "github.com/gardener/landscaper/apis/core" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -30,6 +32,37 @@ func (in *CrdManagementConfiguration) DeepCopy() *CrdManagementConfiguration { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeployItemTimeouts) DeepCopyInto(out *DeployItemTimeouts) { + *out = *in + if in.Pickup != nil { + in, out := &in.Pickup, &out.Pickup + *out = new(core.Duration) + **out = **in + } + if in.Abort != nil { + in, out := &in.Abort, &out.Abort + *out = new(core.Duration) + **out = **in + } + if in.ProgressingDefault != nil { + in, out := &in.ProgressingDefault, &out.ProgressingDefault + *out = new(core.Duration) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeployItemTimeouts. +func (in *DeployItemTimeouts) DeepCopy() *DeployItemTimeouts { + if in == nil { + return nil + } + out := new(DeployItemTimeouts) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LandscaperConfiguration) DeepCopyInto(out *LandscaperConfiguration) { *out = *in @@ -50,6 +83,11 @@ func (in *LandscaperConfiguration) DeepCopyInto(out *LandscaperConfiguration) { *out = new(CrdManagementConfiguration) **out = **in } + if in.DeployItemTimeouts != nil { + in, out := &in.DeployItemTimeouts, &out.DeployItemTimeouts + *out = new(DeployItemTimeouts) + (*in).DeepCopyInto(*out) + } return } diff --git a/apis/core/types_deployitem.go b/apis/core/types_deployitem.go index a61fa9431..34f9de5ab 100644 --- a/apis/core/types_deployitem.go +++ b/apis/core/types_deployitem.go @@ -54,6 +54,13 @@ type DeployItemSpec struct { // Note that the type information is used to determine the secret key and the type of the secret. // +optional RegistryPullSecrets []ObjectReference `json:"registryPullSecrets,omitempty"` + // Timeout specifies how long the deployer may take to apply the deploy item. + // When the time is exceeded, the landscaper will add the abort annotation to the deploy item + // and later put it in 'Failed' if the deployer doesn't handle the abort properly. + // Value has to be parsable by time.ParseDuration (or 'none' to deactivate the timeout). + // Defaults to ten minutes if not specified. + // +optional + Timeout *Duration `json:"timeout,omitempty"` } // DeployItemStatus contains the status of a deploy item @@ -72,6 +79,10 @@ type DeployItemStatus struct { // LastError describes the last error that occurred. LastError *Error `json:"lastError,omitempty"` + // LastReconcileTime indicates when the reconciliation of the last change to the deploy item has started + // +optional + LastReconcileTime *metav1.Time `json:"lastReconcileTime,omitempty"` + // ProviderStatus contains the provider specific status // +optional ProviderStatus *runtime.RawExtension `json:"providerStatus,omitempty"` diff --git a/apis/core/types_shared.go b/apis/core/types_shared.go index 1f1a76b2f..0e8689c68 100644 --- a/apis/core/types_shared.go +++ b/apis/core/types_shared.go @@ -7,6 +7,8 @@ package core import ( "encoding/json" "errors" + "fmt" + "time" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -36,6 +38,40 @@ func (s *JSONSchemaDefinition) UnmarshalJSON(data []byte) error { func (_ JSONSchemaDefinition) OpenAPISchemaType() []string { return []string{"object"} } func (_ JSONSchemaDefinition) OpenAPISchemaFormat() string { return "" } +// Duration is a wrapper for time.Duration that implements JSON marshalling and openapi scheme. +type Duration struct { + time.Duration +} + +// MarshalJSON implements the json marshaling for a Duration +func (d Duration) MarshalJSON() ([]byte, error) { + if d.Duration == 0 { + return []byte("\"none\""), nil + } + return []byte(fmt.Sprintf("%q", d.Duration.String())), nil +} + +// UnmarshalJSON implements json unmarshaling for a Duration +func (d *Duration) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "none" { + *d = Duration{Duration: 0} + return nil + } + dur, err := time.ParseDuration(s) + if err != nil { + return fmt.Errorf("unable to parse string into a duration: %w", err) + } + *d = Duration{Duration: dur} + return nil +} + +func (_ Duration) OpenAPISchemaType() []string { return []string{"string"} } +func (_ Duration) OpenAPISchemaFormat() string { return "" } + // AnyJSON enhances the json.RawMessages with a dedicated openapi definition so that all // it is correctly generated type AnyJSON struct { diff --git a/apis/core/v1alpha1/constants.go b/apis/core/v1alpha1/constants.go index 5373804cf..9208d802c 100644 --- a/apis/core/v1alpha1/constants.go +++ b/apis/core/v1alpha1/constants.go @@ -22,6 +22,9 @@ const ( // ReconcileTimestampAnnotation is used to recognize timeouts in deployitems ReconcileTimestampAnnotation = "landscaper.gardener.cloud/reconcile-time" + // AbortTimestampAnnotation is used to recognize timeouts in deployitems + AbortTimestampAnnotation = "landscaper.gardener.cloud/abort-time" + // Labels // LandscaperComponentLabelName is the name of the labels the holds the information about landscaper components. diff --git a/apis/core/v1alpha1/helper/helpers.go b/apis/core/v1alpha1/helper/helpers.go index 17838c074..4a9c195d7 100644 --- a/apis/core/v1alpha1/helper/helpers.go +++ b/apis/core/v1alpha1/helper/helpers.go @@ -13,6 +13,13 @@ import ( "github.com/gardener/landscaper/apis/core/v1alpha1" ) +type TimestampAnnotation string + +const ( + ReconcileTimestamp = TimestampAnnotation(v1alpha1.ReconcileTimestampAnnotation) + AbortTimestamp = TimestampAnnotation(v1alpha1.AbortTimestampAnnotation) +) + // HasOperation checks if the obj has the given operation annotation func HasOperation(obj metav1.ObjectMeta, op v1alpha1.Operation) bool { currentOp, ok := obj.Annotations[v1alpha1.OperationAnnotation] @@ -32,24 +39,18 @@ func SetOperation(obj *metav1.ObjectMeta, op v1alpha1.Operation) { metav1.SetMetaDataAnnotation(obj, v1alpha1.OperationAnnotation, string(op)) } -// HasReconcileTimestampAnnotation checks if the obj has the given timeout annotation -func HasReconcileTimestampAnnotation(obj metav1.ObjectMeta) bool { - _, ok := obj.Annotations[v1alpha1.ReconcileTimestampAnnotation] - return ok -} - -func GetReconcileTimestampAnnotation(obj metav1.ObjectMeta) (time.Time, error) { - return time.Parse(time.RFC3339, obj.Annotations[v1alpha1.ReconcileTimestampAnnotation]) +func GetTimestampAnnotation(obj metav1.ObjectMeta, ta TimestampAnnotation) (time.Time, error) { + return time.Parse(time.RFC3339, obj.Annotations[string(ta)]) } -// SetReconcileTimestampAnnotationNow sets the timeout annotation with the current timestamp. -func SetReconcileTimestampAnnotationNow(obj *metav1.ObjectMeta) { - SetReconcileTimestampAnnotation(obj, time.Now()) +// SetTimestampAnnotationNow sets the timeout annotation with the current timestamp. +func SetTimestampAnnotationNow(obj *metav1.ObjectMeta, ta TimestampAnnotation) { + metav1.SetMetaDataAnnotation(obj, string(ta), time.Now().Format(time.RFC3339)) } -// SetReconcileTimestampAnnotation sets the timeout annotation with the given timestamp. -func SetReconcileTimestampAnnotation(obj *metav1.ObjectMeta, ts time.Time) { - metav1.SetMetaDataAnnotation(obj, v1alpha1.ReconcileTimestampAnnotation, ts.Format(time.RFC3339)) +func SetAbortOperationAndTimestamp(obj *metav1.ObjectMeta) { + SetOperation(obj, v1alpha1.AbortOperation) + SetTimestampAnnotationNow(obj, AbortTimestamp) } // InitCondition initializes a new Condition with an Unknown status. diff --git a/apis/core/v1alpha1/types_deployitem.go b/apis/core/v1alpha1/types_deployitem.go index a83173051..f97f66614 100644 --- a/apis/core/v1alpha1/types_deployitem.go +++ b/apis/core/v1alpha1/types_deployitem.go @@ -64,6 +64,13 @@ type DeployItemSpec struct { // Note that the type information is used to determine the secret key and the type of the secret. // +optional RegistryPullSecrets []ObjectReference `json:"registryPullSecrets,omitempty"` + // Timeout specifies how long the deployer may take to apply the deploy item. + // When the time is exceeded, the landscaper will add the abort annotation to the deploy item + // and later put it in 'Failed' if the deployer doesn't handle the abort properly. + // Value has to be parsable by time.ParseDuration (or 'none' to deactivate the timeout). + // Defaults to ten minutes if not specified. + // +optional + Timeout *Duration `json:"timeout,omitempty"` } // DeployItemStatus contains the status of a deploy item. @@ -83,6 +90,10 @@ type DeployItemStatus struct { // LastError describes the last error that occurred. LastError *Error `json:"lastError,omitempty"` + // LastReconcileTime indicates when the reconciliation of the last change to the deploy item has started + // +optional + LastReconcileTime *metav1.Time `json:"lastReconcileTime,omitempty"` + // ProviderStatus contains the provider specific status // +optional // +kubebuilder:validation:XEmbeddedResource diff --git a/apis/core/v1alpha1/types_shared.go b/apis/core/v1alpha1/types_shared.go index 38021c0bd..29c112340 100644 --- a/apis/core/v1alpha1/types_shared.go +++ b/apis/core/v1alpha1/types_shared.go @@ -7,6 +7,8 @@ package v1alpha1 import ( "encoding/json" "errors" + "fmt" + "time" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -37,6 +39,40 @@ func (s *JSONSchemaDefinition) UnmarshalJSON(data []byte) error { func (_ JSONSchemaDefinition) OpenAPISchemaType() []string { return []string{"object"} } func (_ JSONSchemaDefinition) OpenAPISchemaFormat() string { return "" } +// Duration is a wrapper for time.Duration that implements JSON marshalling and openapi scheme. +type Duration struct { + time.Duration +} + +// MarshalJSON implements the json marshaling for a Duration +func (d Duration) MarshalJSON() ([]byte, error) { + if d.Duration == 0 { + return []byte("\"none\""), nil + } + return []byte(fmt.Sprintf("%q", d.Duration.String())), nil +} + +// UnmarshalJSON implements json unmarshaling for a Duration +func (d *Duration) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "none" { + *d = Duration{Duration: 0} + return nil + } + dur, err := time.ParseDuration(s) + if err != nil { + return fmt.Errorf("unable to parse string into a duration: %w", err) + } + *d = Duration{Duration: dur} + return nil +} + +func (_ Duration) OpenAPISchemaType() []string { return []string{"string"} } +func (_ Duration) OpenAPISchemaFormat() string { return "" } + // AnyJSON enhances the json.RawMessages with a dedicated openapi definition so that all // it is correctly generated // +k8s:openapi-gen=true diff --git a/apis/core/v1alpha1/zz_generated.conversion.go b/apis/core/v1alpha1/zz_generated.conversion.go index c8666494e..a6c2c4095 100644 --- a/apis/core/v1alpha1/zz_generated.conversion.go +++ b/apis/core/v1alpha1/zz_generated.conversion.go @@ -11,10 +11,12 @@ package v1alpha1 import ( json "encoding/json" + time "time" unsafe "unsafe" v2 "github.com/gardener/component-spec/bindings-go/apis/v2" - v1 "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" conversion "k8s.io/apimachinery/pkg/conversion" runtime "k8s.io/apimachinery/pkg/runtime" selection "k8s.io/apimachinery/pkg/selection" @@ -219,6 +221,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*Duration)(nil), (*core.Duration)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_Duration_To_core_Duration(a.(*Duration), b.(*core.Duration), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*core.Duration)(nil), (*Duration)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_core_Duration_To_v1alpha1_Duration(a.(*core.Duration), b.(*Duration), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*Error)(nil), (*core.Error)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_Error_To_core_Error(a.(*Error), b.(*core.Error), scope) }); err != nil { @@ -1039,6 +1051,7 @@ func autoConvert_v1alpha1_DeployItemSpec_To_core_DeployItemSpec(in *DeployItemSp out.Target = (*core.ObjectReference)(unsafe.Pointer(in.Target)) out.Configuration = (*runtime.RawExtension)(unsafe.Pointer(in.Configuration)) out.RegistryPullSecrets = *(*[]core.ObjectReference)(unsafe.Pointer(&in.RegistryPullSecrets)) + out.Timeout = (*core.Duration)(unsafe.Pointer(in.Timeout)) return nil } @@ -1052,6 +1065,7 @@ func autoConvert_core_DeployItemSpec_To_v1alpha1_DeployItemSpec(in *core.DeployI out.Target = (*ObjectReference)(unsafe.Pointer(in.Target)) out.Configuration = (*runtime.RawExtension)(unsafe.Pointer(in.Configuration)) out.RegistryPullSecrets = *(*[]ObjectReference)(unsafe.Pointer(&in.RegistryPullSecrets)) + out.Timeout = (*Duration)(unsafe.Pointer(in.Timeout)) return nil } @@ -1065,6 +1079,7 @@ func autoConvert_v1alpha1_DeployItemStatus_To_core_DeployItemStatus(in *DeployIt out.ObservedGeneration = in.ObservedGeneration out.Conditions = *(*[]core.Condition)(unsafe.Pointer(&in.Conditions)) out.LastError = (*core.Error)(unsafe.Pointer(in.LastError)) + out.LastReconcileTime = (*v1.Time)(unsafe.Pointer(in.LastReconcileTime)) out.ProviderStatus = (*runtime.RawExtension)(unsafe.Pointer(in.ProviderStatus)) out.ExportReference = (*core.ObjectReference)(unsafe.Pointer(in.ExportReference)) return nil @@ -1080,6 +1095,7 @@ func autoConvert_core_DeployItemStatus_To_v1alpha1_DeployItemStatus(in *core.Dep out.ObservedGeneration = in.ObservedGeneration out.Conditions = *(*[]Condition)(unsafe.Pointer(&in.Conditions)) out.LastError = (*Error)(unsafe.Pointer(in.LastError)) + out.LastReconcileTime = (*v1.Time)(unsafe.Pointer(in.LastReconcileTime)) out.ProviderStatus = (*runtime.RawExtension)(unsafe.Pointer(in.ProviderStatus)) out.ExportReference = (*ObjectReference)(unsafe.Pointer(in.ExportReference)) return nil @@ -1120,6 +1136,26 @@ func Convert_core_DeployItemTemplate_To_v1alpha1_DeployItemTemplate(in *core.Dep return autoConvert_core_DeployItemTemplate_To_v1alpha1_DeployItemTemplate(in, out, s) } +func autoConvert_v1alpha1_Duration_To_core_Duration(in *Duration, out *core.Duration, s conversion.Scope) error { + out.Duration = time.Duration(in.Duration) + return nil +} + +// Convert_v1alpha1_Duration_To_core_Duration is an autogenerated conversion function. +func Convert_v1alpha1_Duration_To_core_Duration(in *Duration, out *core.Duration, s conversion.Scope) error { + return autoConvert_v1alpha1_Duration_To_core_Duration(in, out, s) +} + +func autoConvert_core_Duration_To_v1alpha1_Duration(in *core.Duration, out *Duration, s conversion.Scope) error { + out.Duration = time.Duration(in.Duration) + return nil +} + +// Convert_core_Duration_To_v1alpha1_Duration is an autogenerated conversion function. +func Convert_core_Duration_To_v1alpha1_Duration(in *core.Duration, out *Duration, s conversion.Scope) error { + return autoConvert_core_Duration_To_v1alpha1_Duration(in, out, s) +} + func autoConvert_v1alpha1_Error_To_core_Error(in *Error, out *core.Error, s conversion.Scope) error { out.Operation = in.Operation out.LastTransitionTime = in.LastTransitionTime @@ -1859,7 +1895,7 @@ func Convert_core_StaticDataSource_To_v1alpha1_StaticDataSource(in *core.StaticD } func autoConvert_v1alpha1_StaticDataValueFrom_To_core_StaticDataValueFrom(in *StaticDataValueFrom, out *core.StaticDataValueFrom, s conversion.Scope) error { - out.SecretKeyRef = (*v1.SecretKeySelector)(unsafe.Pointer(in.SecretKeyRef)) + out.SecretKeyRef = (*corev1.SecretKeySelector)(unsafe.Pointer(in.SecretKeyRef)) out.SecretLabelSelector = (*core.SecretLabelSelectorRef)(unsafe.Pointer(in.SecretLabelSelector)) return nil } @@ -1870,7 +1906,7 @@ func Convert_v1alpha1_StaticDataValueFrom_To_core_StaticDataValueFrom(in *Static } func autoConvert_core_StaticDataValueFrom_To_v1alpha1_StaticDataValueFrom(in *core.StaticDataValueFrom, out *StaticDataValueFrom, s conversion.Scope) error { - out.SecretKeyRef = (*v1.SecretKeySelector)(unsafe.Pointer(in.SecretKeyRef)) + out.SecretKeyRef = (*corev1.SecretKeySelector)(unsafe.Pointer(in.SecretKeyRef)) out.SecretLabelSelector = (*SecretLabelSelectorRef)(unsafe.Pointer(in.SecretLabelSelector)) return nil } diff --git a/apis/core/v1alpha1/zz_generated.deepcopy.go b/apis/core/v1alpha1/zz_generated.deepcopy.go index cd5d7038e..ce1fea78e 100644 --- a/apis/core/v1alpha1/zz_generated.deepcopy.go +++ b/apis/core/v1alpha1/zz_generated.deepcopy.go @@ -446,6 +446,11 @@ func (in *DeployItemSpec) DeepCopyInto(out *DeployItemSpec) { *out = make([]ObjectReference, len(*in)) copy(*out, *in) } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(Duration) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeployItemSpec. @@ -473,6 +478,10 @@ func (in *DeployItemStatus) DeepCopyInto(out *DeployItemStatus) { *out = new(Error) (*in).DeepCopyInto(*out) } + if in.LastReconcileTime != nil { + in, out := &in.LastReconcileTime, &out.LastReconcileTime + *out = (*in).DeepCopy() + } if in.ProviderStatus != nil { in, out := &in.ProviderStatus, &out.ProviderStatus *out = new(runtime.RawExtension) @@ -553,6 +562,21 @@ func (in DeployItemTemplateList) DeepCopy() DeployItemTemplateList { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Duration) DeepCopyInto(out *Duration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Duration. +func (in *Duration) DeepCopy() *Duration { + if in == nil { + return nil + } + out := new(Duration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Error) DeepCopyInto(out *Error) { *out = *in diff --git a/apis/core/zz_generated.deepcopy.go b/apis/core/zz_generated.deepcopy.go index 4f95ebd94..01910cdda 100644 --- a/apis/core/zz_generated.deepcopy.go +++ b/apis/core/zz_generated.deepcopy.go @@ -461,6 +461,11 @@ func (in *DeployItemSpec) DeepCopyInto(out *DeployItemSpec) { *out = make([]ObjectReference, len(*in)) copy(*out, *in) } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(Duration) + **out = **in + } return } @@ -489,6 +494,10 @@ func (in *DeployItemStatus) DeepCopyInto(out *DeployItemStatus) { *out = new(Error) (*in).DeepCopyInto(*out) } + if in.LastReconcileTime != nil { + in, out := &in.LastReconcileTime, &out.LastReconcileTime + *out = (*in).DeepCopy() + } if in.ProviderStatus != nil { in, out := &in.ProviderStatus, &out.ProviderStatus *out = new(runtime.RawExtension) @@ -572,6 +581,22 @@ func (in DeployItemTemplateList) DeepCopy() DeployItemTemplateList { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Duration) DeepCopyInto(out *Duration) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Duration. +func (in *Duration) DeepCopy() *Duration { + if in == nil { + return nil + } + out := new(Duration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Error) DeepCopyInto(out *Error) { *out = *in diff --git a/apis/deployer/mock/types_provider.go b/apis/deployer/mock/types_provider.go index c1442680c..dd51ad2eb 100644 --- a/apis/deployer/mock/types_provider.go +++ b/apis/deployer/mock/types_provider.go @@ -22,6 +22,10 @@ type ProviderConfiguration struct { // Phase sets the phase of the DeployItem Phase *lsv1alpha1.ExecutionPhase `json:"phase,omitempty"` + // InitialPhase sets the phase of the DeployItem, but only if it is empty or "Init" + // Additionally, setting it will suppress the DeployItem phase being set to "Succeeded" after successful reconciliation + InitialPhase *lsv1alpha1.ExecutionPhase `json:"initialPhase,omitempty"` + // ProviderStatus sets the provider status to the given value ProviderStatus *runtime.RawExtension `json:"providerStatus,omitempty"` diff --git a/apis/deployer/mock/v1alpha1/types_provider.go b/apis/deployer/mock/v1alpha1/types_provider.go index 81113d4de..efa90a109 100644 --- a/apis/deployer/mock/v1alpha1/types_provider.go +++ b/apis/deployer/mock/v1alpha1/types_provider.go @@ -22,6 +22,10 @@ type ProviderConfiguration struct { // Phase sets the phase of the DeployItem Phase *lsv1alpha1.ExecutionPhase `json:"phase,omitempty"` + // InitialPhase sets the phase of the DeployItem, but only if it is empty or "Init" + // Additionally, setting it will suppress the DeployItem phase being set to "Succeeded" after successful reconciliation + InitialPhase *lsv1alpha1.ExecutionPhase `json:"initialPhase,omitempty"` + // ProviderStatus sets the provider status to the given value ProviderStatus *runtime.RawExtension `json:"providerStatus,omitempty"` diff --git a/apis/deployer/mock/v1alpha1/zz_generated.conversion.go b/apis/deployer/mock/v1alpha1/zz_generated.conversion.go index 100cfe954..9f42fa9bc 100644 --- a/apis/deployer/mock/v1alpha1/zz_generated.conversion.go +++ b/apis/deployer/mock/v1alpha1/zz_generated.conversion.go @@ -72,6 +72,7 @@ func Convert_mock_Configuration_To_v1alpha1_Configuration(in *mock.Configuration func autoConvert_v1alpha1_ProviderConfiguration_To_mock_ProviderConfiguration(in *ProviderConfiguration, out *mock.ProviderConfiguration, s conversion.Scope) error { out.Phase = (*corev1alpha1.ExecutionPhase)(unsafe.Pointer(in.Phase)) + out.InitialPhase = (*corev1alpha1.ExecutionPhase)(unsafe.Pointer(in.InitialPhase)) out.ProviderStatus = (*runtime.RawExtension)(unsafe.Pointer(in.ProviderStatus)) out.Export = (*json.RawMessage)(unsafe.Pointer(in.Export)) return nil @@ -84,6 +85,7 @@ func Convert_v1alpha1_ProviderConfiguration_To_mock_ProviderConfiguration(in *Pr func autoConvert_mock_ProviderConfiguration_To_v1alpha1_ProviderConfiguration(in *mock.ProviderConfiguration, out *ProviderConfiguration, s conversion.Scope) error { out.Phase = (*corev1alpha1.ExecutionPhase)(unsafe.Pointer(in.Phase)) + out.InitialPhase = (*corev1alpha1.ExecutionPhase)(unsafe.Pointer(in.InitialPhase)) out.ProviderStatus = (*runtime.RawExtension)(unsafe.Pointer(in.ProviderStatus)) out.Export = (*json.RawMessage)(unsafe.Pointer(in.Export)) return nil diff --git a/apis/deployer/mock/v1alpha1/zz_generated.deepcopy.go b/apis/deployer/mock/v1alpha1/zz_generated.deepcopy.go index d4c76fcda..b90cdc6f5 100644 --- a/apis/deployer/mock/v1alpha1/zz_generated.deepcopy.go +++ b/apis/deployer/mock/v1alpha1/zz_generated.deepcopy.go @@ -58,6 +58,11 @@ func (in *ProviderConfiguration) DeepCopyInto(out *ProviderConfiguration) { *out = new(corev1alpha1.ExecutionPhase) **out = **in } + if in.InitialPhase != nil { + in, out := &in.InitialPhase, &out.InitialPhase + *out = new(corev1alpha1.ExecutionPhase) + **out = **in + } if in.ProviderStatus != nil { in, out := &in.ProviderStatus, &out.ProviderStatus *out = new(runtime.RawExtension) diff --git a/apis/deployer/mock/zz_generated.deepcopy.go b/apis/deployer/mock/zz_generated.deepcopy.go index 36ba630d9..e2b570de5 100644 --- a/apis/deployer/mock/zz_generated.deepcopy.go +++ b/apis/deployer/mock/zz_generated.deepcopy.go @@ -58,6 +58,11 @@ func (in *ProviderConfiguration) DeepCopyInto(out *ProviderConfiguration) { *out = new(v1alpha1.ExecutionPhase) **out = **in } + if in.InitialPhase != nil { + in, out := &in.InitialPhase, &out.InitialPhase + *out = new(v1alpha1.ExecutionPhase) + **out = **in + } if in.ProviderStatus != nil { in, out := &in.ProviderStatus, &out.ProviderStatus *out = new(runtime.RawExtension) diff --git a/apis/openapi/openapi_generated.go b/apis/openapi/openapi_generated.go index 43a5d134f..9ebed78e6 100644 --- a/apis/openapi/openapi_generated.go +++ b/apis/openapi/openapi_generated.go @@ -33,6 +33,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/gardener/component-spec/bindings-go/apis/v2.SourceRef": schema_component_spec_bindings_go_apis_v2_SourceRef(ref), "github.com/gardener/component-spec/bindings-go/apis/v2.UnstructuredAccessType": schema_component_spec_bindings_go_apis_v2_UnstructuredAccessType(ref), "github.com/gardener/landscaper/apis/config.CrdManagementConfiguration": schema_gardener_landscaper_apis_config_CrdManagementConfiguration(ref), + "github.com/gardener/landscaper/apis/config.DeployItemTimeouts": schema_gardener_landscaper_apis_config_DeployItemTimeouts(ref), "github.com/gardener/landscaper/apis/config.LandscaperConfiguration": schema_gardener_landscaper_apis_config_LandscaperConfiguration(ref), "github.com/gardener/landscaper/apis/config.LocalRegistryConfiguration": schema_gardener_landscaper_apis_config_LocalRegistryConfiguration(ref), "github.com/gardener/landscaper/apis/config.MetricsConfiguration": schema_gardener_landscaper_apis_config_MetricsConfiguration(ref), @@ -40,6 +41,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/gardener/landscaper/apis/config.OCIConfiguration": schema_gardener_landscaper_apis_config_OCIConfiguration(ref), "github.com/gardener/landscaper/apis/config.RegistryConfiguration": schema_gardener_landscaper_apis_config_RegistryConfiguration(ref), "github.com/gardener/landscaper/apis/config/v1alpha1.CrdManagementConfiguration": schema_landscaper_apis_config_v1alpha1_CrdManagementConfiguration(ref), + "github.com/gardener/landscaper/apis/config/v1alpha1.DeployItemTimeouts": schema_landscaper_apis_config_v1alpha1_DeployItemTimeouts(ref), "github.com/gardener/landscaper/apis/config/v1alpha1.LandscaperConfiguration": schema_landscaper_apis_config_v1alpha1_LandscaperConfiguration(ref), "github.com/gardener/landscaper/apis/config/v1alpha1.LocalRegistryConfiguration": schema_landscaper_apis_config_v1alpha1_LocalRegistryConfiguration(ref), "github.com/gardener/landscaper/apis/config/v1alpha1.MetricsConfiguration": schema_landscaper_apis_config_v1alpha1_MetricsConfiguration(ref), @@ -65,6 +67,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/gardener/landscaper/apis/core/v1alpha1.DeployItemSpec": schema_landscaper_apis_core_v1alpha1_DeployItemSpec(ref), "github.com/gardener/landscaper/apis/core/v1alpha1.DeployItemStatus": schema_landscaper_apis_core_v1alpha1_DeployItemStatus(ref), "github.com/gardener/landscaper/apis/core/v1alpha1.DeployItemTemplate": schema_landscaper_apis_core_v1alpha1_DeployItemTemplate(ref), + "github.com/gardener/landscaper/apis/core/v1alpha1.Duration": schema_landscaper_apis_core_v1alpha1_Duration(ref), "github.com/gardener/landscaper/apis/core/v1alpha1.Error": schema_landscaper_apis_core_v1alpha1_Error(ref), "github.com/gardener/landscaper/apis/core/v1alpha1.Execution": schema_landscaper_apis_core_v1alpha1_Execution(ref), "github.com/gardener/landscaper/apis/core/v1alpha1.ExecutionList": schema_landscaper_apis_core_v1alpha1_ExecutionList(ref), @@ -1039,6 +1042,39 @@ func schema_gardener_landscaper_apis_config_CrdManagementConfiguration(ref commo } } +func schema_gardener_landscaper_apis_config_DeployItemTimeouts(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DeployItemTimeouts contains multiple timeout configurations for deploy items", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "pickup": { + SchemaProps: spec.SchemaProps{ + Description: "PickupTimeout defines how long a deployer can take to react on changes to a deploy item before the landscaper will mark it as failed. Allowed values are 'none' (to disable pickup timeout detection) and anything that is understood by golang's time.ParseDuration method. Defaults to five minutes if not specified.", + Ref: ref("github.com/gardener/landscaper/apis/core.Duration"), + }, + }, + "abort": { + SchemaProps: spec.SchemaProps{ + Description: "Abort specifies how long the deployer may take to abort handling a deploy item after getting the abort annotation. Allowed values are 'none' (to disable abort timeout detection) and anything that is understood by golang's time.ParseDuration method. Defaults to five minutes if not specified.", + Ref: ref("github.com/gardener/landscaper/apis/core.Duration"), + }, + }, + "progressingDefault": { + SchemaProps: spec.SchemaProps{ + Description: "ProgressingDefault specifies how long the deployer may take to apply a deploy item by default. The value can be overwritten per deploy item in 'spec.timeout'. Allowed values are 'none' (to disable abort timeout detection) and anything that is understood by golang's time.ParseDuration method. Defaults to ten minutes if not specified.", + Ref: ref("github.com/gardener/landscaper/apis/core.Duration"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/gardener/landscaper/apis/core.Duration"}, + } +} + func schema_gardener_landscaper_apis_config_LandscaperConfiguration(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -1085,11 +1121,10 @@ func schema_gardener_landscaper_apis_config_LandscaperConfiguration(ref common.R Ref: ref("github.com/gardener/landscaper/apis/config.CrdManagementConfiguration"), }, }, - "deployItemPickupTimeout": { + "deployItemTimeouts": { SchemaProps: spec.SchemaProps{ - Description: "DeployItemPickupTimeout defines how long a deployer can take to react on changes to a deploy item before the landscaper will mark it as failed. Allowed values are 'none' (to disable pickup timeout detection) and anything that is understood by golang's time.ParseDuration method. Defaults to five minutes if not specified.", - Type: []string{"string"}, - Format: "", + Description: "DeployItemTimeouts contains configuration for multiple deploy item timeouts", + Ref: ref("github.com/gardener/landscaper/apis/config.DeployItemTimeouts"), }, }, }, @@ -1097,7 +1132,7 @@ func schema_gardener_landscaper_apis_config_LandscaperConfiguration(ref common.R }, }, Dependencies: []string{ - "github.com/gardener/component-spec/bindings-go/apis/v2.RepositoryContext", "github.com/gardener/landscaper/apis/config.CrdManagementConfiguration", "github.com/gardener/landscaper/apis/config.MetricsConfiguration", "github.com/gardener/landscaper/apis/config.RegistryConfiguration"}, + "github.com/gardener/component-spec/bindings-go/apis/v2.RepositoryContext", "github.com/gardener/landscaper/apis/config.CrdManagementConfiguration", "github.com/gardener/landscaper/apis/config.DeployItemTimeouts", "github.com/gardener/landscaper/apis/config.MetricsConfiguration", "github.com/gardener/landscaper/apis/config.RegistryConfiguration"}, } } @@ -1282,6 +1317,39 @@ func schema_landscaper_apis_config_v1alpha1_CrdManagementConfiguration(ref commo } } +func schema_landscaper_apis_config_v1alpha1_DeployItemTimeouts(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DeployItemTimeouts contains multiple timeout configurations for deploy items", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "pickup": { + SchemaProps: spec.SchemaProps{ + Description: "PickupTimeout defines how long a deployer can take to react on changes to a deploy item before the landscaper will mark it as failed. Allowed values are 'none' (to disable pickup timeout detection) and anything that is understood by golang's time.ParseDuration method. Defaults to five minutes if not specified.", + Ref: ref("github.com/gardener/landscaper/apis/core/v1alpha1.Duration"), + }, + }, + "abort": { + SchemaProps: spec.SchemaProps{ + Description: "Abort specifies how long the deployer may take to abort handling a deploy item after getting the abort annotation. Allowed values are 'none' (to disable abort timeout detection) and anything that is understood by golang's time.ParseDuration method. Defaults to five minutes if not specified.", + Ref: ref("github.com/gardener/landscaper/apis/core/v1alpha1.Duration"), + }, + }, + "progressingDefault": { + SchemaProps: spec.SchemaProps{ + Description: "ProgressingDefault specifies how long the deployer may take to apply a deploy item by default. The value can be overwritten per deploy item in 'spec.timeout'. Allowed values are 'none' (to disable abort timeout detection) and anything that is understood by golang's time.ParseDuration method. Defaults to ten minutes if not specified.", + Ref: ref("github.com/gardener/landscaper/apis/core/v1alpha1.Duration"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/gardener/landscaper/apis/core/v1alpha1.Duration"}, + } +} + func schema_landscaper_apis_config_v1alpha1_LandscaperConfiguration(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -1328,11 +1396,10 @@ func schema_landscaper_apis_config_v1alpha1_LandscaperConfiguration(ref common.R Ref: ref("github.com/gardener/landscaper/apis/config/v1alpha1.CrdManagementConfiguration"), }, }, - "deployItemPickupTimeout": { + "deployItemTimeouts": { SchemaProps: spec.SchemaProps{ - Description: "DeployItemPickupTimeout defines how long a deployer can take to react on changes to a deploy item before the landscaper will mark it as failed. Allowed values are 'none' (to disable pickup timeout detection) and anything that is understood by golang's time.ParseDuration method. Defaults to five minutes if not specified.", - Type: []string{"string"}, - Format: "", + Description: "DeployItemTimeouts contains configuration for multiple deploy item timeouts", + Ref: ref("github.com/gardener/landscaper/apis/config/v1alpha1.DeployItemTimeouts"), }, }, }, @@ -1340,7 +1407,7 @@ func schema_landscaper_apis_config_v1alpha1_LandscaperConfiguration(ref common.R }, }, Dependencies: []string{ - "github.com/gardener/component-spec/bindings-go/apis/v2.RepositoryContext", "github.com/gardener/landscaper/apis/config/v1alpha1.CrdManagementConfiguration", "github.com/gardener/landscaper/apis/config/v1alpha1.MetricsConfiguration", "github.com/gardener/landscaper/apis/config/v1alpha1.RegistryConfiguration"}, + "github.com/gardener/component-spec/bindings-go/apis/v2.RepositoryContext", "github.com/gardener/landscaper/apis/config/v1alpha1.CrdManagementConfiguration", "github.com/gardener/landscaper/apis/config/v1alpha1.DeployItemTimeouts", "github.com/gardener/landscaper/apis/config/v1alpha1.MetricsConfiguration", "github.com/gardener/landscaper/apis/config/v1alpha1.RegistryConfiguration"}, } } @@ -2233,12 +2300,18 @@ func schema_landscaper_apis_core_v1alpha1_DeployItemSpec(ref common.ReferenceCal }, }, }, + "timeout": { + SchemaProps: spec.SchemaProps{ + Description: "Timeout specifies how long the deployer may take to apply the deploy item. When the time is exceeded, the landscaper will add the abort annotation to the deploy item and later put it in 'Failed' if the deployer doesn't handle the abort properly. Value has to be parsable by time.ParseDuration (or 'none' to deactivate the timeout). Defaults to ten minutes if not specified.", + Ref: ref("github.com/gardener/landscaper/apis/core/v1alpha1.Duration"), + }, + }, }, Required: []string{"type"}, }, }, Dependencies: []string{ - "github.com/gardener/landscaper/apis/core/v1alpha1.ObjectReference", "k8s.io/apimachinery/pkg/runtime.RawExtension"}, + "github.com/gardener/landscaper/apis/core/v1alpha1.Duration", "github.com/gardener/landscaper/apis/core/v1alpha1.ObjectReference", "k8s.io/apimachinery/pkg/runtime.RawExtension"}, } } @@ -2284,6 +2357,12 @@ func schema_landscaper_apis_core_v1alpha1_DeployItemStatus(ref common.ReferenceC Ref: ref("github.com/gardener/landscaper/apis/core/v1alpha1.Error"), }, }, + "lastReconcileTime": { + SchemaProps: spec.SchemaProps{ + Description: "LastReconcileTime indicates when the reconciliation of the last change to the deploy item has started", + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), + }, + }, "providerStatus": { SchemaProps: spec.SchemaProps{ Description: "ProviderStatus contains the provider specific status", @@ -2301,7 +2380,7 @@ func schema_landscaper_apis_core_v1alpha1_DeployItemStatus(ref common.ReferenceC }, }, Dependencies: []string{ - "github.com/gardener/landscaper/apis/core/v1alpha1.Condition", "github.com/gardener/landscaper/apis/core/v1alpha1.Error", "github.com/gardener/landscaper/apis/core/v1alpha1.ObjectReference", "k8s.io/apimachinery/pkg/runtime.RawExtension"}, + "github.com/gardener/landscaper/apis/core/v1alpha1.Condition", "github.com/gardener/landscaper/apis/core/v1alpha1.Error", "github.com/gardener/landscaper/apis/core/v1alpha1.ObjectReference", "k8s.io/apimachinery/pkg/apis/meta/v1.Time", "k8s.io/apimachinery/pkg/runtime.RawExtension"}, } } @@ -2380,6 +2459,18 @@ func schema_landscaper_apis_core_v1alpha1_DeployItemTemplate(ref common.Referenc } } +func schema_landscaper_apis_core_v1alpha1_Duration(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Duration is a wrapper for time.Duration that implements JSON marshalling and openapi scheme.", + Type: v1alpha1.Duration{}.OpenAPISchemaType(), + Format: v1alpha1.Duration{}.OpenAPISchemaFormat(), + }, + }, + } +} + func schema_landscaper_apis_core_v1alpha1_Error(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -5546,6 +5637,13 @@ func schema_apis_deployer_mock_v1alpha1_ProviderConfiguration(ref common.Referen Format: "", }, }, + "initialPhase": { + SchemaProps: spec.SchemaProps{ + Description: "InitialPhase sets the phase of the DeployItem, but only if it is empty or \"Init\" Additionally, setting it will suppress the DeployItem phase being set to \"Succeeded\" after successful reconciliation", + Type: []string{"string"}, + Format: "", + }, + }, "providerStatus": { SchemaProps: spec.SchemaProps{ Description: "ProviderStatus sets the provider status to the given value", diff --git a/charts/landscaper/templates/_helpers.tpl b/charts/landscaper/templates/_helpers.tpl index 8ca34181a..3d1ec3d46 100644 --- a/charts/landscaper/templates/_helpers.tpl +++ b/charts/landscaper/templates/_helpers.tpl @@ -112,8 +112,11 @@ crdManagement: forceUpdate: {{ .Values.landscaper.crdManagement.forceUpdate }} {{- end }} {{- end }} -{{- if .Values.landscaper.deployItemPickupTimeout }} -deployItemPickupTimeout: {{ .Values.landscaper.deployItemPickupTimeout }} +{{- if .Values.landscaper.deployItemTimeouts }} +deployItemTimeouts: + {{- range $key, $value := .Values.landscaper.deployItemTimeouts }} + {{ $key }}: {{ $value }} + {{- end }} {{- end }} {{- end }} \ No newline at end of file diff --git a/charts/landscaper/values.yaml b/charts/landscaper/values.yaml index 8e9d64811..b23a24149 100644 --- a/charts/landscaper/values.yaml +++ b/charts/landscaper/values.yaml @@ -38,7 +38,13 @@ landscaper: # kind: Configuration # ... -# deployItemPickupTimeout: 5m +# deployItemTimeouts: +# # how long deployers may take to react on changes to deploy items +# pickup: 5m +# # how long deployers may take to abort processing a deploy item after the annotation has been set +# abort: 5m +# # default for how long deployers may take to process a deploy item before being aborted, can be overwritten via the deploy item's 'spec.timeout' field +# progressingDefault: 10m image: # Overrides the image tag whose default is the chart appVersion. diff --git a/cmd/landscaper-controller/app/app.go b/cmd/landscaper-controller/app/app.go index 08ad58d8c..79be53ca6 100644 --- a/cmd/landscaper-controller/app/app.go +++ b/cmd/landscaper-controller/app/app.go @@ -96,7 +96,7 @@ func (o *options) run(ctx context.Context) error { return fmt.Errorf("unable to setup execution controller: %w", err) } - if err := deployitemactuator.AddControllerToManager(mgr, o.config.DeployItemPickupTimeout); err != nil { + if err := deployitemactuator.AddControllerToManager(mgr, o.config.DeployItemTimeouts.Pickup, o.config.DeployItemTimeouts.Abort, o.config.DeployItemTimeouts.ProgressingDefault); err != nil { return fmt.Errorf("unable to setup deployitem controller: %w", err) } diff --git a/docs/api-reference/core.md b/docs/api-reference/core.md index 79b7361d1..fc119ad37 100644 --- a/docs/api-reference/core.md +++ b/docs/api-reference/core.md @@ -342,6 +342,24 @@ For more info see: +Duration + + + +
Timeout specifies how long the deployer may take to apply the deploy item. +When the time is exceeded, the landscaper will add the abort annotation to the deploy item +and later put it in ‘Failed’ if the deployer doesn’t handle the abort properly. +Value has to be parsable by time.ParseDuration (or ‘none’ to deactivate the timeout). +Defaults to ten minutes if not specified.
+Timeout specifies how long the deployer may take to apply the deploy item. +When the time is exceeded, the landscaper will add the abort annotation to the deploy item +and later put it in ‘Failed’ if the deployer doesn’t handle the abort properly. +Value has to be parsable by time.ParseDuration (or ‘none’ to deactivate the timeout). +Defaults to ten minutes if not specified.
+lastReconcileTime
+
+
+Kubernetes meta/v1.Time
+
+
+LastReconcileTime indicates when the reconciliation of the last change to the deploy item has started
+providerStatus
@@ -1755,6 +1805,37 @@ k8s.io/apimachinery/pkg/runtime.RawExtension
DeployItemType defines the type of the deploy item
++(Appears on: +DeployItemSpec) +
++
Duration is a wrapper for time.Duration that implements JSON marshalling and openapi scheme.
+ +Field | +Description | +
---|---|
+Duration
+
+
+time.Duration
+
+
+ |
++ | +
diff --git a/docs/technical/deployer_contract.md b/docs/technical/deployer_contract.md index cf99ea809..b96a26f32 100644 --- a/docs/technical/deployer_contract.md +++ b/docs/technical/deployer_contract.md @@ -54,6 +54,7 @@ Once handled by its deployer, a status similar to this one will be attached to t status: observedGeneration: 1 phase: Succeeded + lastReconcileTime: "2021-04-15T12:10:51Z" providerStatus: apiVersion: manifest.deployer.landscaper.gardener.cloud/v1alpha1 kind: ProviderStatus @@ -103,15 +104,22 @@ As explained above, even if the type is correct, the deployer might still not be #### 3. Handle Annotations There are two important annotations that need to be handled by the deployer: -The reconcile annotation `landscaper.gardener.cloud/operation: reconcile` indicates that either a human operator or the landscaper wants this deploy item to be reconciled. The deployer has to remove this annotation. In addition, it should set the deploy item's phase to `Init` to show the beginning of a new reconciliation and avoid loss of information in case the deployer dies immediately after removing the annotation. +The operation annotation `landscaper.gardener.cloud/operation` indicates that either a human operator or the landscaper wants a specific operation to be fulfilled on this deploy item. The value of the annotation specifies the expected operation: +- `reconcile`: This deploy item needs to be reconciled. The deployer has to remove this annotation. In addition, it should set the deploy item's phase to `Init` to show the beginning of a new reconciliation and avoid loss of information in case the deployer dies immediately after removing the annotation. In the status, `lastReconcileTime` has to be set to the current timestamp (this value is used to recognize when a deployer is 'stuck' processing a deploy item). +- `abort`: This annotation is usually attached to deploy items in the `Progressing` phase and means that the deployer should stop processing it. The main purpose of this annotation is to give the deployer time to gracefully stop processing the deploy item and clean up any already created resources before setting the phase to `Failed`. What 'aborting gracefully' means is highly specific to the corresponding deployer logic. + +> The landscaper will abort deploy items which are stuck in `Progressing` for too long. The timeout can be configured on the deploy item itself via `spec.timeout` and is defaulted to 10 minutes otherwise. The default can be overwritten by setting `deployItemTimeouts.progressingDefault` in the landscaper configuration. Instead of a time, `none` can be used to disable this check. + +> There is also a timeout for deploy items which take too long to abort. This is tracked via a timestamp annotation `landscaper.gardener.cloud/abort-time` which is set by the landscaper together with the abort operation annotation. After the specified time, landscaper will set the deploy item to `Failed`. The timeout can be configured via `deployItemTimeouts.abort` in the landscaper configuration (use `none` to disable, as above). It defaults to 5 minutes. + The second important annotation is `landscaper.gardener.cloud/reconcile-time`. The landscaper adds this annotation - with the current time as value - whenever it expands an `execution` into its deploy items. If this annotation is still present after a defined time, this is interpreted as no deployer having picked up this deploy item and the landscaper will set its phase to `Failed`. Deployers are expected to remove this annotation whenever they start reconciling a deploy item they are responsible for. -> The pickup timeout duration defaults to 5 minutes and can be configured by setting `deployItemPickupTimeout` in the landscaper configuration. Checking for pickup timeouts can also be disabled by setting the aforementioned value to `none`. +> The pickup timeout duration defaults to 5 minutes and can be configured by setting `deployItemTimeouts.pickup` in the landscaper configuration. As for the other timeouts, checking for pickup timeouts can also be disabled by setting the aforementioned value to `none`. #### 4. Handle Generation Another indicator that something needs to be done is when `status.observedGeneration` differs from `metadata.generation`. The latter one changes every time the `spec` is modified and a difference in both shows that the deployer has not yet reacted on the latest changes to this deploy item. For this logic to work, the deployer has to set `status.observedGeneration` to the deploy item's generation at the beginning of the reconcile loop. Similarly to the reconcile annotation, the deployer should set the phase of the deploy item to `Init` if it updated the observed generation. -> There is an auxiliary method `HandleAnnotationsAndGeneration` that handles steps 3 and 4 [defined here](../../pkg/deployer/utils/utils.go). +> There is an auxiliary method `HandleAnnotationsAndGeneration` that handles steps 3 and 4 [defined here](../../pkg/deployer/lib/utils.go). #### 5. Check for Need for Action For most deployers, there probably isn't anything to do now if the deploy item is still in a final state (phase `Succeeded` or `Failed`) - it was finished before and nothing has changed, so the reconcile can be aborted at this point. Please note that this does not apply to all deployers and only works if the phase is actually set to `Init` when a reconcile annotation or an outdated observed generation is found. diff --git a/hack/install-landscaper-for-integration-test.sh b/hack/install-landscaper-for-integration-test.sh index d4b0030e3..37497353f 100755 --- a/hack/install-landscaper-for-integration-test.sh +++ b/hack/install-landscaper-for-integration-test.sh @@ -59,7 +59,9 @@ landscaper: - helm - manifest - mock - deployItemPickupTimeout: 10s + deployItemTimeouts: + pickup: 10s + abort: 10s " > /tmp/values.yaml touch /tmp/registry-values.yaml diff --git a/pkg/deployer/lib/utils.go b/pkg/deployer/lib/utils.go index 3fcb50b17..7541523e2 100644 --- a/pkg/deployer/lib/utils.go +++ b/pkg/deployer/lib/utils.go @@ -10,6 +10,8 @@ import ( "github.com/go-logr/logr" "sigs.k8s.io/controller-runtime/pkg/client" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" lsv1alpha1helper "github.com/gardener/landscaper/apis/core/v1alpha1/helper" ) @@ -27,9 +29,11 @@ func HandleAnnotationsAndGeneration(ctx context.Context, log logr.Logger, c clie // reconcile necessary due to one of // - reconcile annotation // - outdated generation - log.V(5).Info("reconcile required, setting observed generation and phase", "reconcileAnnotation", hasReconcileAnnotation, "observedGeneration", di.Status.ObservedGeneration, "generation", di.Generation) + log.V(5).Info("reconcile required, setting observed generation, phase, and last change reconcile timestamp", "reconcileAnnotation", hasReconcileAnnotation, "observedGeneration", di.Status.ObservedGeneration, "generation", di.Generation) di.Status.ObservedGeneration = di.Generation di.Status.Phase = lsv1alpha1.ExecutionPhaseInit + now := metav1.Now() + di.Status.LastReconcileTime = &now log.V(7).Info("updating status") if err := c.Status().Update(ctx, di); err != nil { @@ -42,7 +46,7 @@ func HandleAnnotationsAndGeneration(ctx context.Context, log logr.Logger, c clie changedMeta = true delete(di.ObjectMeta.Annotations, lsv1alpha1.OperationAnnotation) } - if lsv1alpha1helper.HasReconcileTimestampAnnotation(di.ObjectMeta) { + if metav1.HasAnnotation(di.ObjectMeta, string(lsv1alpha1helper.ReconcileTimestamp)) { log.V(5).Info("removing timestamp annotation") changedMeta = true delete(di.ObjectMeta.Annotations, lsv1alpha1.ReconcileTimestampAnnotation) diff --git a/pkg/deployer/mock/controller.go b/pkg/deployer/mock/controller.go index 49d1f321b..2bc4ec693 100644 --- a/pkg/deployer/mock/controller.go +++ b/pkg/deployer/mock/controller.go @@ -113,7 +113,13 @@ func (a *controller) reconcile(ctx context.Context, deployItem *lsv1alpha1.Deplo return err } - deployItem.Status.Phase = lsv1alpha1.ExecutionPhaseSucceeded + if config.InitialPhase != nil { + if len(deployItem.Status.Phase) == 0 || deployItem.Status.Phase == lsv1alpha1.ExecutionPhaseInit { + deployItem.Status.Phase = *config.InitialPhase + } + } else { + deployItem.Status.Phase = lsv1alpha1.ExecutionPhaseSucceeded + } if config.Phase != nil { deployItem.Status.Phase = *config.Phase diff --git a/pkg/landscaper/controllers/deployitem/add.go b/pkg/landscaper/controllers/deployitem/add.go index 3d3dd3451..dae9791e0 100644 --- a/pkg/landscaper/controllers/deployitem/add.go +++ b/pkg/landscaper/controllers/deployitem/add.go @@ -8,18 +8,19 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/manager" + lscore "github.com/gardener/landscaper/apis/core" lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" ) -// AddControllerToManager adds the deploy item controller to the controller manager. -// It is responsible for detecting timeouts in deploy items. -func AddControllerToManager(mgr manager.Manager, rawDeployItemPickupTimeout string) error { - if rawDeployItemPickupTimeout == "none" { - // currently the deploy item reconcile loop is only used for pickup timeout detection - // so if that is disabled there is no need to watch deploy items - return nil - } - a, err := NewController(ctrl.Log.WithName("controllers").WithName("DeployItem"), mgr.GetClient(), mgr.GetScheme(), rawDeployItemPickupTimeout) +func AddControllerToManager(mgr manager.Manager, deployItemPickupTimeout, deployItemAbortingTimeout, deployItemDefaultTimeout *lscore.Duration) error { + a, err := NewController( + ctrl.Log.WithName("controllers").WithName("DeployItem"), + mgr.GetClient(), + mgr.GetScheme(), + deployItemPickupTimeout, + deployItemAbortingTimeout, + deployItemDefaultTimeout, + ) if err != nil { return err } diff --git a/pkg/landscaper/controllers/deployitem/controller.go b/pkg/landscaper/controllers/deployitem/controller.go index 75db5bad0..88b4812b7 100644 --- a/pkg/landscaper/controllers/deployitem/controller.go +++ b/pkg/landscaper/controllers/deployitem/controller.go @@ -7,6 +7,7 @@ package deployitem import ( "context" "fmt" + "reflect" "time" "github.com/go-logr/logr" @@ -15,55 +16,62 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + lscore "github.com/gardener/landscaper/apis/core" lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" lsv1alpha1helper "github.com/gardener/landscaper/apis/core/v1alpha1/helper" ) const ( - PickupTimeoutReason = "PickupTimeout" // for error messages - PickupTimeoutOperation = "WaitingForPickup" // for error messages + PickupTimeoutReason = "PickupTimeout" // for error messages + PickupTimeoutOperation = "WaitingForPickup" // for error messages + AbortingTimeoutReason = "AbortingTimeout" // for error messages + AbortingTimeoutOperation = "WaitingForAbort" // for error messages ) // NewController creates a new deploy item controller that handles timeouts // To detect pickup timeouts (when a DeployItem resource is not reconciled by any deployer within a specified timeframe), the controller checks for a timestamp annotation. // It is expected that deployers remove the timestamp annotation from deploy items during reconciliation. If the timestamp annotation exists and is older than a specified duration, // the controller marks the deploy item as failed. -// rawPickupTimeout is a string containing the pickup timeout duration, either as 'none' or as a duration that can be parsed by time.ParseDuration. -func NewController(log logr.Logger, c client.Client, scheme *runtime.Scheme, rawPickupTimeout string) (reconcile.Reconciler, error) { +// pickupTimeout is a string containing the pickup timeout duration, either as 'none' or as a duration that can be parsed by time.ParseDuration. +func NewController(log logr.Logger, c client.Client, scheme *runtime.Scheme, pickupTimeout, abortingTimeout, defaultTimeout *lscore.Duration) (reconcile.Reconciler, error) { con := controller{log: log, c: c, scheme: scheme} - if rawPickupTimeout == "none" { - con.pickupTimeout = nil + if pickupTimeout != nil { + con.pickupTimeout = pickupTimeout.Duration } else { - tmp, err := time.ParseDuration(rawPickupTimeout) - if err != nil { - return nil, fmt.Errorf("unable to parse deploy item pickup timeout into a duration: %w", err) - } - con.pickupTimeout = &tmp + con.pickupTimeout = time.Duration(0) + } + if abortingTimeout != nil { + con.abortingTimeout = abortingTimeout.Duration + } else { + con.abortingTimeout = time.Duration(0) + } + if defaultTimeout != nil { + con.defaultTimeout = defaultTimeout.Duration + } else { + con.defaultTimeout = time.Duration(0) } // log pickup timeout - timeoutLog := "" - if con.pickupTimeout != nil { - timeoutLog = con.pickupTimeout.String() - } - log.Info("deploy item pickup timeout detection", "active", con.pickupTimeout != nil, "timeout", timeoutLog) + log.Info("deploy item pickup timeout detection", "active", con.pickupTimeout != 0, "timeout", con.pickupTimeout.String()) + log.Info("deploy item aborting timeout detection", "active", con.abortingTimeout != 0, "timeout", con.abortingTimeout.String()) + log.Info("deploy item default timeout", "active", con.defaultTimeout != 0, "timeout", con.defaultTimeout.String()) return &con, nil } type controller struct { - log logr.Logger - c client.Client - scheme *runtime.Scheme - pickupTimeout *time.Duration + log logr.Logger + c client.Client + scheme *runtime.Scheme + pickupTimeout time.Duration + abortingTimeout time.Duration + defaultTimeout time.Duration } func (con *controller) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { logger := con.log.WithValues("resource", req.NamespacedName.String()) - if con.pickupTimeout == nil { - logger.V(7).Info("skipping reconcile as pickup timeout detection is disabled") - return reconcile.Result{}, nil - } logger.Info("reconcile") di := &lsv1alpha1.DeployItem{} @@ -75,35 +83,172 @@ func (con *controller) Reconcile(ctx context.Context, req reconcile.Request) (re return reconcile.Result{}, err } + var requeue *time.Duration + var err error + old := di.DeepCopy() + + // detect pickup timeout + if con.pickupTimeout != 0 { + logger.V(7).Info("check for pickup timeout") + requeue, err = con.detectPickupTimeouts(logger, di) + if err != nil { + return reconcile.Result{}, err + } + if !reflect.DeepEqual(old.Status, di.Status) { + if err := con.c.Status().Update(ctx, di); err != nil { + logger.Error(err, "unable to set deployitem status") + return reconcile.Result{}, err + } + // if there was a pickup timeout, no need to check for anything else + return reconcile.Result{}, nil + } + } + + // detect aborting timeout + if con.abortingTimeout != 0 { + logger.V(7).Info("check for aborting timeout") + tmp, err := con.detectAbortingTimeouts(logger, di) + if err != nil { + return reconcile.Result{}, err + } + if requeue == nil { + requeue = tmp + } else if tmp != nil && *tmp < *requeue { + requeue = tmp + } + if !reflect.DeepEqual(old.Status, di.Status) { + if err := con.c.Status().Update(ctx, di); err != nil { + // we might need to expose this as event on the deploy item + logger.Error(err, "unable to set deployitem status") + return reconcile.Result{}, err + } + // if there was an aborting timeout, no need to check for anything else + return reconcile.Result{}, nil + } + } + + // detect progressing timeout + // only do something if progressing timeout detection is neither deactivated on the deploy item, nor defaulted by the deploy item and deactivated by default + if !((di.Spec.Timeout != nil && di.Spec.Timeout.Duration == 0) || (di.Spec.Timeout == nil && con.defaultTimeout == 0)) { + logger.V(7).Info("check for progressing timeout") + tmp, err := con.detectProgressingTimeouts(logger, di) + if err != nil { + return reconcile.Result{}, err + } + if requeue == nil { + requeue = tmp + } else if tmp != nil && *tmp < *requeue { + requeue = tmp + } + if !reflect.DeepEqual(old.Annotations, di.Annotations) { + if err := con.c.Update(ctx, di); err != nil { + logger.Error(err, "unable to update deploy item") + return reconcile.Result{}, err + } + } + } + + if requeue == nil { + return reconcile.Result{}, nil + } + logger.V(5).Info("requeue deploy item", "after", requeue.String()) + return reconcile.Result{RequeueAfter: *requeue}, nil +} + +func (con *controller) detectPickupTimeouts(log logr.Logger, di *lsv1alpha1.DeployItem) (*time.Duration, error) { + logger := log.WithValues("operation", "DetectPickupTimeouts") if di.Status.Phase == lsv1alpha1.ExecutionPhaseFailed && di.Status.LastError != nil && di.Status.LastError.Reason == PickupTimeoutReason { // don't do anything if phase is already failed due to a recent pickup timeout // to avoid multiple simultaneous reconciles which would cause further reconciles in the deployers - return reconcile.Result{}, nil + logger.V(7).Info("deploy item already failed due to pickup timeout, nothing to do") + return nil, nil } - if !lsv1alpha1helper.HasReconcileTimestampAnnotation(di.ObjectMeta) { - return reconcile.Result{}, nil + if !metav1.HasAnnotation(di.ObjectMeta, string(lsv1alpha1helper.ReconcileTimestamp)) { + logger.V(7).Info("deploy item doesn't have reconcile timestamp annotation, nothing to do") + return nil, nil } - ts, err := lsv1alpha1helper.GetReconcileTimestampAnnotation(di.ObjectMeta) + ts, err := lsv1alpha1helper.GetTimestampAnnotation(di.ObjectMeta, lsv1alpha1helper.ReconcileTimestamp) if err != nil { - return reconcile.Result{}, fmt.Errorf("unable to parse timestamp annotation: %w", err) + return nil, fmt.Errorf("unable to parse reconcile timestamp annotation: %w", err) } waitingForPickupDuration := time.Since(ts) - if waitingForPickupDuration >= *con.pickupTimeout { - // no deployer has picked up the deployitem within the timeframe + if waitingForPickupDuration >= con.pickupTimeout { + // no deployer has picked up the deploy item within the timeframe // => pickup timeout + logger.V(5).Info("pickup timeout occurred") di.Status.Phase = lsv1alpha1.ExecutionPhaseFailed - di.Status.LastError = lsv1alpha1helper.UpdatedError(di.Status.LastError, PickupTimeoutOperation, PickupTimeoutReason, fmt.Sprintf("no deployer has reconciled this deployitem within %d seconds", *con.pickupTimeout/time.Second), lsv1alpha1.ErrorTimeout) - if err := con.c.Status().Update(ctx, di); err != nil { - logger.Error(err, "unable to set deployitem status") - return reconcile.Result{}, err - } - return reconcile.Result{}, nil + di.Status.LastError = lsv1alpha1helper.UpdatedError(di.Status.LastError, PickupTimeoutOperation, PickupTimeoutReason, fmt.Sprintf("no deployer has reconciled this deployitem within %d seconds", con.pickupTimeout/time.Second), lsv1alpha1.ErrorTimeout) + return nil, nil } // deploy item neither picked up nor timed out // => requeue shortly after expected timeout - return reconcile.Result{RequeueAfter: *con.pickupTimeout - waitingForPickupDuration + (5 * time.Second)}, nil + requeue := con.pickupTimeout - waitingForPickupDuration + (5 * time.Second) + return &requeue, nil +} + +func (con *controller) detectAbortingTimeouts(log logr.Logger, di *lsv1alpha1.DeployItem) (*time.Duration, error) { + logger := log.WithValues("operation", "DetectAbortingTimeouts") + if di.Status.Phase == lsv1alpha1.ExecutionPhaseFailed && di.Status.LastError != nil && di.Status.LastError.Reason == AbortingTimeoutReason { + // don't do anything if phase is already failed due to a recent aborting timeout + // to avoid multiple simultaneous reconciles which would cause further reconciles in the deployers + logger.V(7).Info("deploy item already failed due to aborting timeout, nothing to do") + return nil, nil + } + + // no aborting timeout if timestamp is missing or deploy item is in a final phase + if !metav1.HasAnnotation(di.ObjectMeta, string(lsv1alpha1helper.AbortTimestamp)) || di.Status.Phase == lsv1alpha1.ExecutionPhaseSucceeded || di.Status.Phase == lsv1alpha1.ExecutionPhaseFailed { + logger.V(7).Info("deploy item doesn't have abort timestamp annotation or is in a final phase, nothing to do") + return nil, nil + } + ts, err := lsv1alpha1helper.GetTimestampAnnotation(di.ObjectMeta, lsv1alpha1helper.AbortTimestamp) + if err != nil { + return nil, fmt.Errorf("unable to parse abort timestamp annotation: %w", err) + } + waitingForAbortDuration := time.Since(ts) + if waitingForAbortDuration >= con.abortingTimeout { + // deploy item has not been aborted within the timeframe + // => aborting timeout + logger.V(5).Info("aborting timeout occurred") + di.Status.Phase = lsv1alpha1.ExecutionPhaseFailed + di.Status.LastError = lsv1alpha1helper.UpdatedError(di.Status.LastError, AbortingTimeoutOperation, AbortingTimeoutReason, fmt.Sprintf("deployer has not aborted progressing this deploy item within %d seconds", con.abortingTimeout/time.Second), lsv1alpha1.ErrorTimeout) + return nil, nil + } + + // deploy item neither aborted nor timed out + // => requeue shortly after expected timeout + requeue := con.abortingTimeout - waitingForAbortDuration + (5 * time.Second) + return &requeue, nil +} + +func (con *controller) detectProgressingTimeouts(log logr.Logger, di *lsv1alpha1.DeployItem) (*time.Duration, error) { + logger := log.WithValues("operation", "DetectProgressingTimeouts") + // no progressing timeout if timestamp is zero or deploy item is in a final phase + if di.Status.LastReconcileTime.IsZero() || di.Status.Phase == lsv1alpha1.ExecutionPhaseSucceeded || di.Status.Phase == lsv1alpha1.ExecutionPhaseFailed { + logger.V(7).Info("deploy item is reconciled for the first time or in a final phase, nothing to do") + return nil, nil + } + + var progressingTimeout time.Duration + if di.Spec.Timeout == nil { // timeout not specified in deploy item, use global default + progressingTimeout = con.defaultTimeout + } else { + progressingTimeout = di.Spec.Timeout.Duration + } + progressingDuration := time.Since(di.Status.LastReconcileTime.Time) + if progressingDuration >= progressingTimeout { + // the deployer has not finished processing this deploy item within the timeframe + // => abort it + logger.V(5).Info("deploy item timed out, setting abort operation annotation") + lsv1alpha1helper.SetAbortOperationAndTimestamp(&di.ObjectMeta) + return nil, nil + } + + // deploy item not yet timed out + // => requeue shortly after expected timeout + requeue := progressingTimeout - progressingDuration + (5 * time.Second) + return &requeue, nil } diff --git a/pkg/landscaper/controllers/deployitem/deployitemcontroller_suite_test.go b/pkg/landscaper/controllers/deployitem/deployitemcontroller_suite_test.go new file mode 100644 index 000000000..6ae08c56d --- /dev/null +++ b/pkg/landscaper/controllers/deployitem/deployitemcontroller_suite_test.go @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package deployitem_test + +import ( + "path/filepath" + "testing" + "time" + + lscore "github.com/gardener/landscaper/apis/core" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/gardener/landscaper/test/utils/envtest" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Deploy Item Controller Test Suite") +} + +var ( + testPickupTimeoutDuration = lscore.Duration{ + Duration: 10 * time.Second, + } + testAbortingTimeoutDuration = lscore.Duration{ + Duration: 10 * time.Second, + } + testProgressingTimeoutDuration = lscore.Duration{ + Duration: 30 * time.Second, + } +) + +var ( + testenv *envtest.Environment + projectRoot = filepath.Join("../../../../") + testdataDir = filepath.Join("./testdata/") +) + +var _ = BeforeSuite(func() { + var err error + testenv, err = envtest.New(projectRoot) + Expect(err).ToNot(HaveOccurred()) + + _, err = testenv.Start() + Expect(err).ToNot(HaveOccurred()) +}) + +var _ = AfterSuite(func() { + Expect(testenv.Stop()).ToNot(HaveOccurred()) +}) diff --git a/pkg/landscaper/controllers/deployitem/reconcile_test.go b/pkg/landscaper/controllers/deployitem/reconcile_test.go new file mode 100644 index 000000000..234348078 --- /dev/null +++ b/pkg/landscaper/controllers/deployitem/reconcile_test.go @@ -0,0 +1,248 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package deployitem_test + +import ( + "context" + "time" + + "github.com/go-logr/logr/testing" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" + lsv1alpha1helper "github.com/gardener/landscaper/apis/core/v1alpha1/helper" + mockv1alpha1 "github.com/gardener/landscaper/apis/deployer/mock/v1alpha1" + "github.com/gardener/landscaper/pkg/api" + mockctlr "github.com/gardener/landscaper/pkg/deployer/mock" + dictrl "github.com/gardener/landscaper/pkg/landscaper/controllers/deployitem" + "github.com/gardener/landscaper/test/utils" + testutils "github.com/gardener/landscaper/test/utils" + "github.com/gardener/landscaper/test/utils/envtest" +) + +var _ = Describe("Deploy Item Controller Reconcile Test", func() { + + var ( + state *envtest.State + deployItemController, mockController reconcile.Reconciler + ) + + BeforeEach(func() { + var err error + + deployItemController, err = dictrl.NewController(testing.NullLogger{}, testenv.Client, api.LandscaperScheme, &testPickupTimeoutDuration, &testAbortingTimeoutDuration, &testProgressingTimeoutDuration) + Expect(err).ToNot(HaveOccurred()) + + mockController, err = mockctlr.NewController(testing.NullLogger{}, testenv.Client, api.LandscaperScheme, &mockv1alpha1.Configuration{}) + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + if state != nil { + ctx := context.Background() + defer ctx.Done() + Expect(testenv.CleanupState(ctx, state)).ToNot(HaveOccurred()) + state = nil + } + }) + + It("Should detect pickup timeouts", func() { + ctx := context.Background() + defer ctx.Done() + + var err error + state, err = testenv.InitResources(ctx, testdataDir) + Expect(err).ToNot(HaveOccurred()) + + By("Prepare test deploy items") + di := &lsv1alpha1.DeployItem{} + diReq := testutils.Request("mock-di-prog", state.Namespace) + // do not reconcile with mock deployer + + By("Set timed out reconcile timestamp annotation") + utils.ExpectNoError(testenv.Client.Get(ctx, diReq.NamespacedName, di)) + timedOut := metav1.Time{Time: time.Now().Add(-(testPickupTimeoutDuration.Duration + (5 * time.Second)))} + metav1.SetMetaDataAnnotation(&di.ObjectMeta, lsv1alpha1.ReconcileTimestampAnnotation, timedOut.Format(time.RFC3339)) + utils.ExpectNoError(testenv.Client.Update(ctx, di)) + + By("Verify that timed out deploy items are in 'Failed' phase") + testutils.ShouldReconcile(ctx, deployItemController, diReq) + utils.ExpectNoError(testenv.Client.Get(ctx, diReq.NamespacedName, di)) + Expect(di.Status).To(MatchFields(IgnoreExtras, Fields{ + "Phase": Equal(lsv1alpha1.ExecutionPhaseFailed), + "LastError": PointTo(MatchFields(IgnoreExtras, Fields{ + "Codes": ContainElement(lsv1alpha1.ErrorTimeout), + "Reason": Equal(dictrl.PickupTimeoutReason), + })), + })) + }) + + It("Should detect progressing timeouts", func() { + ctx := context.Background() + defer ctx.Done() + + var err error + state, err = testenv.InitResources(ctx, testdataDir) + Expect(err).ToNot(HaveOccurred()) + + By("Prepare test deploy items") + di := &lsv1alpha1.DeployItem{} + diReq := testutils.Request("mock-di-prog", state.Namespace) + testutils.ShouldReconcile(ctx, mockController, diReq) + testutils.ShouldReconcile(ctx, mockController, diReq) + + // verify state + utils.ExpectNoError(testenv.Client.Get(ctx, diReq.NamespacedName, di)) + Expect(di.Status.Phase).To(Equal(lsv1alpha1.ExecutionPhaseProgressing)) + Expect(di.Status.LastReconcileTime).NotTo(BeNil()) + old := di.DeepCopy() + + // reconcile with deploy item controller should not do anything to the deploy item + By("Verify that deploy item controller doesn't change anything if no timeout occurred") + testutils.ShouldReconcile(ctx, deployItemController, diReq) + utils.ExpectNoError(testenv.Client.Get(ctx, diReq.NamespacedName, di)) + Expect(di).To(Equal(old)) + + By("Set timed out LastReconcileTime timestamp") + timedOut := metav1.Time{Time: time.Now().Add(-(testProgressingTimeoutDuration.Duration + (5 * time.Second)))} + di.Status.LastReconcileTime = &timedOut + utils.ExpectNoError(testenv.Client.Status().Update(ctx, di)) + + By("Verify that timed out deploy items get an abort annotation") + testutils.ShouldReconcile(ctx, deployItemController, diReq) + utils.ExpectNoError(testenv.Client.Get(ctx, diReq.NamespacedName, di)) + Expect(di.Annotations).NotTo(BeNil()) + Expect(metav1.HasAnnotation(di.ObjectMeta, lsv1alpha1.AbortTimestampAnnotation)).To(BeTrue(), "deploy item should have an abort timestamp annotation") + Expect(lsv1alpha1helper.HasOperation(di.ObjectMeta, lsv1alpha1.AbortOperation)).To(BeTrue(), "deploy item should have an abort operation annotation") + Expect(di.Status.Phase).To(Equal(lsv1alpha1.ExecutionPhaseProgressing)) + }) + + It("Should not detect progressing timeouts for deploy items in a final phase", func() { + ctx := context.Background() + defer ctx.Done() + + var err error + state, err = testenv.InitResources(ctx, testdataDir) + Expect(err).ToNot(HaveOccurred()) + + By("Prepare test deploy items") + diS := &lsv1alpha1.DeployItem{} + diF := &lsv1alpha1.DeployItem{} + diReqS := testutils.Request("mock-di-succ", state.Namespace) + diReqF := testutils.Request("mock-di-fail", state.Namespace) + testutils.ShouldReconcile(ctx, mockController, diReqS) + testutils.ShouldReconcile(ctx, mockController, diReqS) + testutils.ShouldReconcile(ctx, mockController, diReqF) + testutils.ShouldReconcile(ctx, mockController, diReqF) + + // verify state + utils.ExpectNoError(testenv.Client.Get(ctx, diReqS.NamespacedName, diS)) + utils.ExpectNoError(testenv.Client.Get(ctx, diReqF.NamespacedName, diF)) + Expect(diS.Status.Phase).To(Equal(lsv1alpha1.ExecutionPhaseSucceeded)) + Expect(diF.Status.Phase).To(Equal(lsv1alpha1.ExecutionPhaseFailed)) + Expect(diS.Status.LastReconcileTime).NotTo(BeNil()) + Expect(diF.Status.LastReconcileTime).NotTo(BeNil()) + + By("Set timed out LastReconcileTime timestamp") + timedOut := metav1.Time{Time: time.Now().Add(-(testProgressingTimeoutDuration.Duration + (5 * time.Second)))} + diS.Status.LastReconcileTime = &timedOut + diF.Status.LastReconcileTime = &timedOut + utils.ExpectNoError(testenv.Client.Status().Update(ctx, diS)) + utils.ExpectNoError(testenv.Client.Status().Update(ctx, diF)) + + By("Verify that deploy items did not get an abort annotation") + testutils.ShouldReconcile(ctx, deployItemController, diReqS) + testutils.ShouldReconcile(ctx, deployItemController, diReqF) + utils.ExpectNoError(testenv.Client.Get(ctx, diReqS.NamespacedName, diS)) + utils.ExpectNoError(testenv.Client.Get(ctx, diReqF.NamespacedName, diF)) + Expect(diS.Annotations).To(BeNil()) + Expect(diF.Annotations).To(BeNil()) + }) + + It("Should prefer a timeout specified in the deploy item over the default", func() { + ctx := context.Background() + defer ctx.Done() + + var err error + state, err = testenv.InitResources(ctx, testdataDir) + Expect(err).ToNot(HaveOccurred()) + + By("Prepare test deploy items") + di := &lsv1alpha1.DeployItem{} + diReq := testutils.Request("mock-di-prog-timeout", state.Namespace) + testutils.ShouldReconcile(ctx, mockController, diReq) + testutils.ShouldReconcile(ctx, mockController, diReq) + + // verify state + utils.ExpectNoError(testenv.Client.Get(ctx, diReq.NamespacedName, di)) + Expect(di.Status.Phase).To(Equal(lsv1alpha1.ExecutionPhaseProgressing)) + + By("Set timed out LastReconcileTime timestamp (using default timeout duration)") + timedOut := metav1.Time{Time: time.Now().Add(-(testProgressingTimeoutDuration.Duration + (5 * time.Second)))} + di.Status.LastReconcileTime = &timedOut + utils.ExpectNoError(testenv.Client.Status().Update(ctx, di)) + + By("Verify that deploy item is not timed out") + testutils.ShouldReconcile(ctx, deployItemController, diReq) + utils.ExpectNoError(testenv.Client.Get(ctx, diReq.NamespacedName, di)) + Expect(di.Annotations).To(BeNil()) + + By("Set timed out LastReconcileTime timestamp (deploy item specific timeout duration)") + timedOut = metav1.Time{Time: time.Now().Add(-(di.Spec.Timeout.Duration + (5 * time.Second)))} + di.Status.LastReconcileTime = &timedOut + utils.ExpectNoError(testenv.Client.Status().Update(ctx, di)) + + By("Verify that deploy item is timed out") + testutils.ShouldReconcile(ctx, deployItemController, diReq) + utils.ExpectNoError(testenv.Client.Get(ctx, diReq.NamespacedName, di)) + Expect(di.Annotations).NotTo(BeNil()) + Expect(metav1.HasAnnotation(di.ObjectMeta, lsv1alpha1.AbortTimestampAnnotation)).To(BeTrue(), "deploy item should have an abort timestamp annotation") + Expect(lsv1alpha1helper.HasOperation(di.ObjectMeta, lsv1alpha1.AbortOperation)).To(BeTrue(), "deploy item should have an abort operation annotation") + Expect(di.Status.Phase).To(Equal(lsv1alpha1.ExecutionPhaseProgressing)) + }) + + It("Should detect aborting timeouts", func() { + ctx := context.Background() + defer ctx.Done() + + var err error + state, err = testenv.InitResources(ctx, testdataDir) + Expect(err).ToNot(HaveOccurred()) + + By("Prepare test deploy items") + di := &lsv1alpha1.DeployItem{} + diReq := testutils.Request("mock-di-prog", state.Namespace) + testutils.ShouldReconcile(ctx, mockController, diReq) + testutils.ShouldReconcile(ctx, mockController, diReq) + + // verify state + utils.ExpectNoError(testenv.Client.Get(ctx, diReq.NamespacedName, di)) + Expect(di.Status.Phase).To(Equal(lsv1alpha1.ExecutionPhaseProgressing)) + + By("Set timed out abort timestamp annotation") + timedOut := time.Now().Add(-(testAbortingTimeoutDuration.Duration + (5 * time.Second))) + lsv1alpha1helper.SetOperation(&di.ObjectMeta, lsv1alpha1.AbortOperation) + metav1.SetMetaDataAnnotation(&di.ObjectMeta, lsv1alpha1.AbortTimestampAnnotation, timedOut.Format(time.RFC3339)) + utils.ExpectNoError(testenv.Client.Update(ctx, di)) + + By("Verify that timed out deploy items are in 'Failed' phase") + testutils.ShouldReconcile(ctx, deployItemController, diReq) + utils.ExpectNoError(testenv.Client.Get(ctx, diReq.NamespacedName, di)) + Expect(di.Status).To(MatchFields(IgnoreExtras, Fields{ + "Phase": Equal(lsv1alpha1.ExecutionPhaseFailed), + "LastError": PointTo(MatchFields(IgnoreExtras, Fields{ + "Codes": ContainElement(lsv1alpha1.ErrorTimeout), + "Reason": Equal(dictrl.AbortingTimeoutReason), + })), + })) + }) + +}) diff --git a/pkg/landscaper/controllers/deployitem/testdata/00-progressing-mock-di.yaml b/pkg/landscaper/controllers/deployitem/testdata/00-progressing-mock-di.yaml new file mode 100644 index 000000000..e9077e615 --- /dev/null +++ b/pkg/landscaper/controllers/deployitem/testdata/00-progressing-mock-di.yaml @@ -0,0 +1,11 @@ +apiVersion: landscaper.gardener.cloud/v1alpha1 +kind: DeployItem +metadata: + name: mock-di-prog + namespace: {{ .Namespace }} +spec: + type: landscaper.gardener.cloud/mock + config: + apiVersion: mock.deployer.landscaper.gardener.cloud/v1alpha1 + kind: ProviderConfiguration + phase: Progressing diff --git a/pkg/landscaper/controllers/deployitem/testdata/01-succeeded-mock-di.yaml b/pkg/landscaper/controllers/deployitem/testdata/01-succeeded-mock-di.yaml new file mode 100644 index 000000000..defed7990 --- /dev/null +++ b/pkg/landscaper/controllers/deployitem/testdata/01-succeeded-mock-di.yaml @@ -0,0 +1,11 @@ +apiVersion: landscaper.gardener.cloud/v1alpha1 +kind: DeployItem +metadata: + name: mock-di-succ + namespace: {{ .Namespace }} +spec: + type: landscaper.gardener.cloud/mock + config: + apiVersion: mock.deployer.landscaper.gardener.cloud/v1alpha1 + kind: ProviderConfiguration + phase: Succeeded diff --git a/pkg/landscaper/controllers/deployitem/testdata/02-failed-mock-di.yaml b/pkg/landscaper/controllers/deployitem/testdata/02-failed-mock-di.yaml new file mode 100644 index 000000000..ddbbbb704 --- /dev/null +++ b/pkg/landscaper/controllers/deployitem/testdata/02-failed-mock-di.yaml @@ -0,0 +1,11 @@ +apiVersion: landscaper.gardener.cloud/v1alpha1 +kind: DeployItem +metadata: + name: mock-di-fail + namespace: {{ .Namespace }} +spec: + type: landscaper.gardener.cloud/mock + config: + apiVersion: mock.deployer.landscaper.gardener.cloud/v1alpha1 + kind: ProviderConfiguration + phase: Failed diff --git a/pkg/landscaper/controllers/deployitem/testdata/03-progressing-mock-di-with-timeout.yaml b/pkg/landscaper/controllers/deployitem/testdata/03-progressing-mock-di-with-timeout.yaml new file mode 100644 index 000000000..055c944c5 --- /dev/null +++ b/pkg/landscaper/controllers/deployitem/testdata/03-progressing-mock-di-with-timeout.yaml @@ -0,0 +1,12 @@ +apiVersion: landscaper.gardener.cloud/v1alpha1 +kind: DeployItem +metadata: + name: mock-di-prog-timeout + namespace: {{ .Namespace }} +spec: + type: landscaper.gardener.cloud/mock + config: + apiVersion: mock.deployer.landscaper.gardener.cloud/v1alpha1 + kind: ProviderConfiguration + phase: Progressing + timeout: "1h" diff --git a/pkg/landscaper/crdmanager/crdresources/landscaper.gardener.cloud_deployitems.yaml b/pkg/landscaper/crdmanager/crdresources/landscaper.gardener.cloud_deployitems.yaml index ac4df73d2..fb832ff9c 100644 --- a/pkg/landscaper/crdmanager/crdresources/landscaper.gardener.cloud_deployitems.yaml +++ b/pkg/landscaper/crdmanager/crdresources/landscaper.gardener.cloud_deployitems.yaml @@ -77,6 +77,8 @@ spec: required: - name type: object + timeout: + x-kubernetes-preserve-unknown-fields: true type: description: Type is the type of the deployer that should handle the item. type: string @@ -171,6 +173,10 @@ spec: - operation - reason type: object + lastReconcileTime: + description: LastReconcileTime indicates when the reconciliation of the last change to the deploy item has started + format: date-time + type: string observedGeneration: description: ObservedGeneration is the most recent generation observed for this DeployItem. It corresponds to the DeployItem generation, which is updated on mutation by the landscaper. format: int64 diff --git a/pkg/landscaper/execution/helper.go b/pkg/landscaper/execution/helper.go index e3a02236a..ecbc6bc55 100644 --- a/pkg/landscaper/execution/helper.go +++ b/pkg/landscaper/execution/helper.go @@ -20,7 +20,7 @@ import ( // ApplyDeployItemTemplate sets and updates the values defined by deploy item template on a deploy item. func ApplyDeployItemTemplate(di *lsv1alpha1.DeployItem, tmpl lsv1alpha1.DeployItemTemplate) { lsv1alpha1helper.SetOperation(&di.ObjectMeta, lsv1alpha1.ReconcileOperation) - lsv1alpha1helper.SetReconcileTimestampAnnotationNow(&di.ObjectMeta) + lsv1alpha1helper.SetTimestampAnnotationNow(&di.ObjectMeta, lsv1alpha1helper.ReconcileTimestamp) di.Spec.Type = tmpl.Type di.Spec.Target = tmpl.Target di.Spec.Configuration = tmpl.Configuration diff --git a/test/integration/deployitems/pickup_timeout.go b/test/integration/deployitems/pickup_timeout.go deleted file mode 100644 index ffc480070..000000000 --- a/test/integration/deployitems/pickup_timeout.go +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. -// -// SPDX-License-Identifier: Apache-2.0 - -package deployitems - -import ( - "context" - "errors" - "path" - "time" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - - "github.com/onsi/ginkgo" - g "github.com/onsi/gomega" - gs "github.com/onsi/gomega/gstruct" - - lsv1alpha1helper "github.com/gardener/landscaper/apis/core/v1alpha1/helper" - "github.com/gardener/landscaper/pkg/landscaper/controllers/deployitem" - - lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" - "github.com/gardener/landscaper/test/framework" - "github.com/gardener/landscaper/test/utils" - "github.com/gardener/landscaper/test/utils/envtest" -) - -// RegisterTests registers all tests of this package -func RegisterTests(f *framework.Framework) { - PickupTimeoutTests(f) -} - -const ( - waitingForDeployItems = 5 * time.Second // how long to wait for the landscaper to create deploy items from the installation - deployItemPickupTimeout = 10 * time.Second // the landscaper has to be configured accordingly for this test to work! - waitingForFailedState = 10 * time.Second // how long to wait for the landscaper to set the phase to failed after the pickup timed out - resyncTime = 1 * time.Second // after which time to check again if the condition was not fulfilled the last time -) - -func namespacedName(meta metav1.ObjectMeta) types.NamespacedName { - return types.NamespacedName{ - Namespace: meta.Namespace, - Name: meta.Name, - } -} - -func PickupTimeoutTests(f *framework.Framework) { - ginkgo.Describe("Deploy Item Pickup Timeout", func() { - var ( - dumper = f.Register() - testdataDir = path.Join(f.RootPath, "test", "integration", "deployitems", "testdata") - - ctx context.Context - state *envtest.State - cleanup framework.CleanupFunc - ) - - ginkgo.BeforeEach(func() { - ctx = context.Background() - var err error - state, cleanup, err = f.NewState(ctx) - utils.ExpectNoError(err) - dumper.AddNamespaces(state.Namespace) - }) - - ginkgo.AfterEach(func() { - defer ctx.Done() - g.Expect(cleanup(ctx)).ToNot(g.HaveOccurred()) - }) - - ginkgo.It("should detect pickup timeouts", func() { - ginkgo.By("create dummy installation") - inst := &lsv1alpha1.Installation{} - utils.ExpectNoError(utils.ReadResourceFromFile(inst, path.Join(testdataDir, "00-dummy-installation.yaml"))) - inst.SetNamespace(state.Namespace) - utils.ExpectNoError(state.Create(ctx, f.Client, inst)) - - ginkgo.By("verify that deploy items have been created") - di := &lsv1alpha1.DeployItem{} - g.Eventually(func() error { - err := f.Client.Get(ctx, namespacedName(inst.ObjectMeta), inst) - if err != nil { - return err - } - if inst.Status.ExecutionReference == nil { - return errors.New("no execution reference") - } - exec := &lsv1alpha1.Execution{} - err = f.Client.Get(ctx, inst.Status.ExecutionReference.NamespacedName(), exec) - if err != nil { - return err - } - if exec.Status.DeployItemReferences == nil || len(exec.Status.DeployItemReferences) == 0 { - return errors.New("no deployment references defined") - } - err = f.Client.Get(ctx, exec.Status.DeployItemReferences[0].Reference.NamespacedName(), di) - if err != nil { - return err - } - return nil - }, waitingForDeployItems, resyncTime).Should(g.Succeed(), "unable to fetch deploy item") - - ginkgo.By("check for timestamp annotation") - // checking whether the set timestamp is up-to-date is difficult due to potential differences between the - // system times of the machine running the landscaper and the machine running the tests - // so just check for existence of the annotation - g.Expect(lsv1alpha1helper.HasReconcileTimestampAnnotation(di.ObjectMeta)).To(g.BeTrue(), "deploy item should have a reconcile timestamp annotation") - - ginkgo.By("check for pickup timeout") - time.Sleep(deployItemPickupTimeout) - g.Eventually(func() lsv1alpha1.DeployItemStatus { - utils.ExpectNoError(f.Client.Get(ctx, namespacedName(di.ObjectMeta), di)) - return di.Status - }, waitingForFailedState, resyncTime).Should(gs.MatchFields(gs.IgnoreExtras, gs.Fields{ - "Phase": g.Equal(lsv1alpha1.ExecutionPhaseFailed), - "LastError": gs.PointTo(gs.MatchFields(gs.IgnoreExtras, gs.Fields{ - "Codes": g.ContainElement(lsv1alpha1.ErrorTimeout), - "Reason": g.Equal(deployitem.PickupTimeoutReason), - })), - })) - }) - - ginkgo.It("should not detect pickup timeouts for components with working deployers", func() { - ginkgo.By("create mock installation") - inst := &lsv1alpha1.Installation{} - utils.ExpectNoError(utils.ReadResourceFromFile(inst, path.Join(testdataDir, "01-mock-installation.yaml"))) - inst.SetNamespace(state.Namespace) - utils.ExpectNoError(state.Create(ctx, f.Client, inst)) - - ginkgo.By("verify that deploy items have been created") - di := &lsv1alpha1.DeployItem{} - g.Eventually(func() (*lsv1alpha1.DeployItem, error) { - err := f.Client.Get(ctx, namespacedName(inst.ObjectMeta), inst) - if err != nil || inst.Status.ExecutionReference == nil { - return nil, err - } - exec := &lsv1alpha1.Execution{} - err = f.Client.Get(ctx, inst.Status.ExecutionReference.NamespacedName(), exec) - if err != nil || exec.Status.DeployItemReferences == nil || len(exec.Status.DeployItemReferences) == 0 { - return nil, err - } - err = f.Client.Get(ctx, exec.Status.DeployItemReferences[0].Reference.NamespacedName(), di) - if err != nil { - return nil, err - } - return di, err - }, waitingForDeployItems, resyncTime).ShouldNot(g.BeNil(), "unable to fetch deploy item") - - ginkgo.By("verify that deploy item is not timed out") - time.Sleep(deployItemPickupTimeout + waitingForFailedState) // wait for a potential timeout to happen - utils.ExpectNoError(f.Client.Get(ctx, namespacedName(di.ObjectMeta), di)) - // check that deploy item does not have a pickup timeout - g.Expect(di.Status.LastError).To(g.Or(g.BeNil(), gs.MatchFields(gs.IgnoreExtras, gs.Fields{ - "Codes": g.Not(g.ContainElement(lsv1alpha1.ErrorTimeout)), - "Reason": g.Not(g.Equal(deployitem.PickupTimeoutReason)), - }))) - }) - - }) - -} diff --git a/test/integration/deployitems/testdata/02-progressing-mock-di.yaml b/test/integration/deployitems/testdata/02-progressing-mock-di.yaml new file mode 100644 index 000000000..55df21627 --- /dev/null +++ b/test/integration/deployitems/testdata/02-progressing-mock-di.yaml @@ -0,0 +1,11 @@ +apiVersion: landscaper.gardener.cloud/v1alpha1 +kind: DeployItem +metadata: + name: mock-di-prog +spec: + type: landscaper.gardener.cloud/mock + config: + apiVersion: mock.deployer.landscaper.gardener.cloud/v1alpha1 + kind: ProviderConfiguration + initialPhase: Progressing + timeout: 10s diff --git a/test/integration/deployitems/timeouts.go b/test/integration/deployitems/timeouts.go new file mode 100644 index 000000000..9f88379f1 --- /dev/null +++ b/test/integration/deployitems/timeouts.go @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package deployitems + +import ( + "context" + "path" + "path/filepath" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" + lsv1alpha1helper "github.com/gardener/landscaper/apis/core/v1alpha1/helper" + dictrl "github.com/gardener/landscaper/pkg/landscaper/controllers/deployitem" + kutil "github.com/gardener/landscaper/pkg/utils/kubernetes" + "github.com/gardener/landscaper/test/framework" + "github.com/gardener/landscaper/test/utils" + "github.com/gardener/landscaper/test/utils/envtest" +) + +// RegisterTests registers all tests of this package +func RegisterTests(f *framework.Framework) { + TimeoutTests(f) +} + +const ( + waitingForDeployItems = 5 * time.Second // how long to wait for the landscaper to create deploy items from the installation + deployItemPickupTimeout = 10 * time.Second // the landscaper has to be configured accordingly for this test to work! + deployItemAbortingTimeout = 10 * time.Second // the landscaper has to be configured accordingly for this test to work! + waitingForReconcile = 10 * time.Second // how long to wait for the landscaper or the deployer to reconcile and update the deploy item + resyncTime = 1 * time.Second // after which time to check again if the condition was not fulfilled the last time +) + +func maxDuration(durs ...time.Duration) time.Duration { + var res time.Duration + for i, d := range durs { + if i == 0 || d > res { + res = d + } + } + return res +} + +func TimeoutTests(f *framework.Framework) { + var ( + dumper = f.Register() + testdataDir = filepath.Join(f.RootPath, "test", "integration", "deployitems", "testdata") + ) + + Describe("Deploy Item Timeouts", func() { + + var ( + ctx context.Context + state *envtest.State + cleanup framework.CleanupFunc + ) + + BeforeEach(func() { + ctx = context.Background() + var err error + state, cleanup, err = f.NewState(ctx) + utils.ExpectNoError(err) + dumper.AddNamespaces(state.Namespace) + }) + + AfterEach(func() { + defer ctx.Done() + Expect(cleanup(ctx)).ToNot(HaveOccurred()) + }) + + It("should detect timeouts", func() { + By("create test installations") + dummy_inst := &lsv1alpha1.Installation{} + mock_inst := &lsv1alpha1.Installation{} + mock_di_prog := &lsv1alpha1.DeployItem{} + // creates deploy item without responsible deployer => pickup timeout + utils.ExpectNoError(utils.ReadResourceFromFile(dummy_inst, path.Join(testdataDir, "00-dummy-installation.yaml"))) + // creates valid mock deploy item => no timeout + utils.ExpectNoError(utils.ReadResourceFromFile(mock_inst, path.Join(testdataDir, "01-mock-installation.yaml"))) + // creates mock deploy item in 'Progressing' phase => first progressing timeout, then aborting timeout + utils.ExpectNoError(utils.ReadResourceFromFile(mock_di_prog, path.Join(testdataDir, "02-progressing-mock-di.yaml"))) + Expect(mock_di_prog.Spec.Timeout).NotTo(BeNil(), "timeout should be specified in the mock deploy item manifest") + deployItemProgressingTimeout := mock_di_prog.Spec.Timeout.Duration + dummy_inst.SetNamespace(state.Namespace) + mock_inst.SetNamespace(state.Namespace) + mock_di_prog.SetNamespace(state.Namespace) + utils.ExpectNoError(state.Create(ctx, f.Client, dummy_inst)) + utils.ExpectNoError(state.Create(ctx, f.Client, mock_inst)) + + By("verify that deploy items have been created") + dummy_inst_di := &lsv1alpha1.DeployItem{} + mock_inst_di := &lsv1alpha1.DeployItem{} + Eventually(func() (bool, error) { + // fetch installations + err := f.Client.Get(ctx, kutil.ObjectKeyFromObject(dummy_inst), dummy_inst) + if err != nil || dummy_inst.Status.ExecutionReference == nil { + return false, err + } + err = f.Client.Get(ctx, kutil.ObjectKeyFromObject(mock_inst), mock_inst) + if err != nil || mock_inst.Status.ExecutionReference == nil { + return false, err + } + // check for executions + dummy_exec := &lsv1alpha1.Execution{} + mock_exec := &lsv1alpha1.Execution{} + err = f.Client.Get(ctx, dummy_inst.Status.ExecutionReference.NamespacedName(), dummy_exec) + if err != nil || dummy_exec.Status.DeployItemReferences == nil || len(dummy_exec.Status.DeployItemReferences) == 0 { + return false, err + } + err = f.Client.Get(ctx, mock_inst.Status.ExecutionReference.NamespacedName(), mock_exec) + if err != nil || mock_exec.Status.DeployItemReferences == nil || len(mock_exec.Status.DeployItemReferences) == 0 { + return false, err + } + // check executions for deploy item + err = f.Client.Get(ctx, dummy_exec.Status.DeployItemReferences[0].Reference.NamespacedName(), dummy_inst_di) + if err != nil { + return false, err + } + err = f.Client.Get(ctx, mock_exec.Status.DeployItemReferences[0].Reference.NamespacedName(), mock_inst_di) + if err != nil { + return false, err + } + // return true if both deploy items could be fetched + return true, err + }, waitingForDeployItems, resyncTime).Should(BeTrue(), "unable to fetch deploy items") + utils.ExpectNoError(state.Create(ctx, f.Client, mock_di_prog)) + + By("check for reconcile timestamp annotation") + // checking whether the set timestamp is up-to-date is difficult due to potential differences between the + // system times of the machine running the landscaper and the machine running the tests + // so just check for existence of the annotation + Expect(metav1.HasAnnotation(dummy_inst_di.ObjectMeta, string(lsv1alpha1helper.ReconcileTimestamp))).To(BeTrue(), "dummy deploy item should have a reconcile timestamp annotation") + + By("wait for progressing timeout to happen") + time.Sleep(deployItemProgressingTimeout) + + By("verify progressing timeout") + // expected state: + // - mock_di_prog should have had a progressing timeout (abort operation annotation and abort timestamp annotation, but not yet failed) + Eventually(func() lsv1alpha1.DeployItem { // check mock_di_prog first, because it's the only one that will change again (aborting timeout) + utils.ExpectNoError(f.Client.Get(ctx, kutil.ObjectKeyFromObject(mock_di_prog), mock_di_prog)) + return *mock_di_prog + }, waitingForReconcile, resyncTime).Should(MatchFields(IgnoreExtras, Fields{ + "ObjectMeta": MatchFields(IgnoreExtras, Fields{ + "Annotations": MatchKeys(IgnoreExtras, Keys{ + lsv1alpha1.OperationAnnotation: BeEquivalentTo(lsv1alpha1.AbortOperation), + lsv1alpha1.AbortTimestampAnnotation: Not(BeNil()), + }), + }), + "Status": MatchFields(IgnoreExtras, Fields{ + "Phase": Equal(lsv1alpha1.ExecutionPhaseProgressing), + }), + })) + + By("wait for pickup timeout to happen") + if deployItemPickupTimeout > deployItemProgressingTimeout { + time.Sleep(deployItemPickupTimeout - deployItemProgressingTimeout) + } + + By("verify pickup timeout") + // expected state: + // - dummy_inst_di should have had a pickup timeout ('Failed' phase) + // - mock_inst_di should be succeeded and have no reconcile timestamp annotation + startWaitingTime := time.Now() + Eventually(func() lsv1alpha1.DeployItemStatus { + utils.ExpectNoError(f.Client.Get(ctx, kutil.ObjectKeyFromObject(dummy_inst_di), dummy_inst_di)) + return dummy_inst_di.Status + }, waitingForReconcile, resyncTime).Should(MatchFields(IgnoreExtras, Fields{ + "Phase": Equal(lsv1alpha1.ExecutionPhaseFailed), + "LastError": PointTo(MatchFields(IgnoreExtras, Fields{ + "Codes": ContainElement(lsv1alpha1.ErrorTimeout), + "Reason": Equal(dictrl.PickupTimeoutReason), + })), + }), "deploy item of the dummy installation should have had a pickup timeout") + Eventually(func() lsv1alpha1.DeployItem { + utils.ExpectNoError(f.Client.Get(ctx, kutil.ObjectKeyFromObject(mock_inst_di), mock_inst_di)) + return *mock_inst_di + }, maxDuration(0, waitingForReconcile-time.Since(startWaitingTime)), resyncTime).Should(MatchFields(IgnoreExtras, Fields{ + "ObjectMeta": MatchFields(IgnoreExtras, Fields{ + "Annotations": Not(MatchKeys(IgnoreExtras, Keys{ + lsv1alpha1.ReconcileTimestampAnnotation: Not(BeNil()), + })), + }), + "Status": MatchFields(IgnoreExtras, Fields{ + "Phase": Equal(lsv1alpha1.ExecutionPhaseSucceeded), + }), + }), "deploy item of the mock installation should not have had a pickup timeout") + + By("wait for aborting timeout to happen") + time.Sleep(maxDuration(0, deployItemAbortingTimeout-time.Since(startWaitingTime))) + + By("verify aborting timeout") + // expected state: + // - mock_di_prog should have had an aborting timeout ('Failed' phase) + Eventually(func() lsv1alpha1.DeployItemStatus { + utils.ExpectNoError(f.Client.Get(ctx, kutil.ObjectKeyFromObject(mock_di_prog), mock_di_prog)) + return mock_di_prog.Status + }, waitingForReconcile, resyncTime).Should(MatchFields(IgnoreExtras, Fields{ + "Phase": Equal(lsv1alpha1.ExecutionPhaseFailed), + "LastError": PointTo(MatchFields(IgnoreExtras, Fields{ + "Codes": ContainElement(lsv1alpha1.ErrorTimeout), + "Reason": Equal(dictrl.AbortingTimeoutReason), + })), + })) + + }) + + }) + +} diff --git a/test/integration/webhook/webhook.go b/test/integration/webhook/webhook.go index 46e3f8ba5..e12acdc86 100644 --- a/test/integration/webhook/webhook.go +++ b/test/integration/webhook/webhook.go @@ -30,7 +30,7 @@ func RegisterTests(f *framework.Framework) { } func WebhookTest(f *framework.Framework) { - _ = ginkgo.Describe("SimpleWebhookTest", func() { + _ = ginkgo.Describe("WebhookTest", func() { dumper := f.Register() var ( diff --git a/vendor/github.com/gardener/landscaper/apis/config/types_landscaper_config.go b/vendor/github.com/gardener/landscaper/apis/config/types_landscaper_config.go index 33f95d85e..834738dab 100644 --- a/vendor/github.com/gardener/landscaper/apis/config/types_landscaper_config.go +++ b/vendor/github.com/gardener/landscaper/apis/config/types_landscaper_config.go @@ -7,6 +7,8 @@ package config import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + lscore "github.com/gardener/landscaper/apis/core" ) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -25,11 +27,28 @@ type LandscaperConfiguration struct { // CrdManagement configures whether the landscaper controller should deploy the CRDs it needs into the cluster // +optional CrdManagement *CrdManagementConfiguration `json:"crdManagement,omitempty"` - // DeployItemPickupTimeout defines how long a deployer can take to react on changes to a deploy item before the landscaper will mark it as failed. + // DeployItemTimeouts contains configuration for multiple deploy item timeouts + // +optional + DeployItemTimeouts *DeployItemTimeouts `json:"deployItemTimeouts,omitempty"` +} + +// DeployItemTimeouts contains multiple timeout configurations for deploy items +type DeployItemTimeouts struct { + // PickupTimeout defines how long a deployer can take to react on changes to a deploy item before the landscaper will mark it as failed. // Allowed values are 'none' (to disable pickup timeout detection) and anything that is understood by golang's time.ParseDuration method. // Defaults to five minutes if not specified. // +optional - DeployItemPickupTimeout string `json:"deployItemPickupTimeout,omitempty"` + Pickup *lscore.Duration `json:"pickup,omitempty"` + // Abort specifies how long the deployer may take to abort handling a deploy item after getting the abort annotation. + // Allowed values are 'none' (to disable abort timeout detection) and anything that is understood by golang's time.ParseDuration method. + // Defaults to five minutes if not specified. + // +optional + Abort *lscore.Duration `json:"abort,omitempty"` + // ProgressingDefault specifies how long the deployer may take to apply a deploy item by default. The value can be overwritten per deploy item in 'spec.timeout'. + // Allowed values are 'none' (to disable abort timeout detection) and anything that is understood by golang's time.ParseDuration method. + // Defaults to ten minutes if not specified. + // +optional + ProgressingDefault *lscore.Duration `json:"progressingDefault,omitempty"` } // RegistryConfiguration contains the configuration for the used definition registry diff --git a/vendor/github.com/gardener/landscaper/apis/config/v1alpha1/defaults.go b/vendor/github.com/gardener/landscaper/apis/config/v1alpha1/defaults.go index 4b84e9a2f..c0844c7ab 100644 --- a/vendor/github.com/gardener/landscaper/apis/config/v1alpha1/defaults.go +++ b/vendor/github.com/gardener/landscaper/apis/config/v1alpha1/defaults.go @@ -5,7 +5,11 @@ package v1alpha1 import ( + "time" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/gardener/landscaper/apis/core/v1alpha1" ) func addDefaultingFuncs(scheme *runtime.Scheme) error { @@ -22,8 +26,17 @@ func SetDefaults_LandscaperConfiguration(obj *LandscaperConfiguration) { UseInMemoryOverlay: false, } } - if len(obj.DeployItemPickupTimeout) == 0 { - obj.DeployItemPickupTimeout = "5m" + if obj.DeployItemTimeouts == nil { + obj.DeployItemTimeouts = &DeployItemTimeouts{} + } + if obj.DeployItemTimeouts.Pickup == nil { + obj.DeployItemTimeouts.Pickup = &v1alpha1.Duration{Duration: 5 * time.Minute} + } + if obj.DeployItemTimeouts.Abort == nil { + obj.DeployItemTimeouts.Abort = &v1alpha1.Duration{Duration: 5 * time.Minute} + } + if obj.DeployItemTimeouts.ProgressingDefault == nil { + obj.DeployItemTimeouts.ProgressingDefault = &v1alpha1.Duration{Duration: 10 * time.Minute} } } diff --git a/vendor/github.com/gardener/landscaper/apis/config/v1alpha1/types_landscaper_config.go b/vendor/github.com/gardener/landscaper/apis/config/v1alpha1/types_landscaper_config.go index 88b9bac14..4f9cde06f 100644 --- a/vendor/github.com/gardener/landscaper/apis/config/v1alpha1/types_landscaper_config.go +++ b/vendor/github.com/gardener/landscaper/apis/config/v1alpha1/types_landscaper_config.go @@ -7,6 +7,8 @@ package v1alpha1 import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" ) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -25,11 +27,28 @@ type LandscaperConfiguration struct { // CrdManagement configures whether the landscaper controller should deploy the CRDs it needs into the cluster // +optional CrdManagement *CrdManagementConfiguration `json:"crdManagement,omitempty"` - // DeployItemPickupTimeout defines how long a deployer can take to react on changes to a deploy item before the landscaper will mark it as failed. + // DeployItemTimeouts contains configuration for multiple deploy item timeouts + // +optional + DeployItemTimeouts *DeployItemTimeouts `json:"deployItemTimeouts,omitempty"` +} + +// DeployItemTimeouts contains multiple timeout configurations for deploy items +type DeployItemTimeouts struct { + // PickupTimeout defines how long a deployer can take to react on changes to a deploy item before the landscaper will mark it as failed. // Allowed values are 'none' (to disable pickup timeout detection) and anything that is understood by golang's time.ParseDuration method. // Defaults to five minutes if not specified. // +optional - DeployItemPickupTimeout string `json:"deployItemPickupTimeout,omitempty"` + Pickup *lsv1alpha1.Duration `json:"pickup,omitempty"` + // Abort specifies how long the deployer may take to abort handling a deploy item after getting the abort annotation. + // Allowed values are 'none' (to disable abort timeout detection) and anything that is understood by golang's time.ParseDuration method. + // Defaults to five minutes if not specified. + // +optional + Abort *lsv1alpha1.Duration `json:"abort,omitempty"` + // ProgressingDefault specifies how long the deployer may take to apply a deploy item by default. The value can be overwritten per deploy item in 'spec.timeout'. + // Allowed values are 'none' (to disable abort timeout detection) and anything that is understood by golang's time.ParseDuration method. + // Defaults to ten minutes if not specified. + // +optional + ProgressingDefault *lsv1alpha1.Duration `json:"progressingDefault,omitempty"` } // RegistryConfiguration contains the configuration for the used definition registry diff --git a/vendor/github.com/gardener/landscaper/apis/config/v1alpha1/zz_generated.conversion.go b/vendor/github.com/gardener/landscaper/apis/config/v1alpha1/zz_generated.conversion.go index baa8fa122..e42211302 100644 --- a/vendor/github.com/gardener/landscaper/apis/config/v1alpha1/zz_generated.conversion.go +++ b/vendor/github.com/gardener/landscaper/apis/config/v1alpha1/zz_generated.conversion.go @@ -17,6 +17,8 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" config "github.com/gardener/landscaper/apis/config" + core "github.com/gardener/landscaper/apis/core" + corev1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" ) func init() { @@ -36,6 +38,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*DeployItemTimeouts)(nil), (*config.DeployItemTimeouts)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_DeployItemTimeouts_To_config_DeployItemTimeouts(a.(*DeployItemTimeouts), b.(*config.DeployItemTimeouts), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*config.DeployItemTimeouts)(nil), (*DeployItemTimeouts)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_config_DeployItemTimeouts_To_v1alpha1_DeployItemTimeouts(a.(*config.DeployItemTimeouts), b.(*DeployItemTimeouts), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*LandscaperConfiguration)(nil), (*config.LandscaperConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_LandscaperConfiguration_To_config_LandscaperConfiguration(a.(*LandscaperConfiguration), b.(*config.LandscaperConfiguration), scope) }); err != nil { @@ -121,6 +133,30 @@ func Convert_config_CrdManagementConfiguration_To_v1alpha1_CrdManagementConfigur return autoConvert_config_CrdManagementConfiguration_To_v1alpha1_CrdManagementConfiguration(in, out, s) } +func autoConvert_v1alpha1_DeployItemTimeouts_To_config_DeployItemTimeouts(in *DeployItemTimeouts, out *config.DeployItemTimeouts, s conversion.Scope) error { + out.Pickup = (*core.Duration)(unsafe.Pointer(in.Pickup)) + out.Abort = (*core.Duration)(unsafe.Pointer(in.Abort)) + out.ProgressingDefault = (*core.Duration)(unsafe.Pointer(in.ProgressingDefault)) + return nil +} + +// Convert_v1alpha1_DeployItemTimeouts_To_config_DeployItemTimeouts is an autogenerated conversion function. +func Convert_v1alpha1_DeployItemTimeouts_To_config_DeployItemTimeouts(in *DeployItemTimeouts, out *config.DeployItemTimeouts, s conversion.Scope) error { + return autoConvert_v1alpha1_DeployItemTimeouts_To_config_DeployItemTimeouts(in, out, s) +} + +func autoConvert_config_DeployItemTimeouts_To_v1alpha1_DeployItemTimeouts(in *config.DeployItemTimeouts, out *DeployItemTimeouts, s conversion.Scope) error { + out.Pickup = (*corev1alpha1.Duration)(unsafe.Pointer(in.Pickup)) + out.Abort = (*corev1alpha1.Duration)(unsafe.Pointer(in.Abort)) + out.ProgressingDefault = (*corev1alpha1.Duration)(unsafe.Pointer(in.ProgressingDefault)) + return nil +} + +// Convert_config_DeployItemTimeouts_To_v1alpha1_DeployItemTimeouts is an autogenerated conversion function. +func Convert_config_DeployItemTimeouts_To_v1alpha1_DeployItemTimeouts(in *config.DeployItemTimeouts, out *DeployItemTimeouts, s conversion.Scope) error { + return autoConvert_config_DeployItemTimeouts_To_v1alpha1_DeployItemTimeouts(in, out, s) +} + func autoConvert_v1alpha1_LandscaperConfiguration_To_config_LandscaperConfiguration(in *LandscaperConfiguration, out *config.LandscaperConfiguration, s conversion.Scope) error { out.RepositoryContext = (*v2.RepositoryContext)(unsafe.Pointer(in.RepositoryContext)) if err := Convert_v1alpha1_RegistryConfiguration_To_config_RegistryConfiguration(&in.Registry, &out.Registry, s); err != nil { @@ -128,7 +164,7 @@ func autoConvert_v1alpha1_LandscaperConfiguration_To_config_LandscaperConfigurat } out.Metrics = (*config.MetricsConfiguration)(unsafe.Pointer(in.Metrics)) out.CrdManagement = (*config.CrdManagementConfiguration)(unsafe.Pointer(in.CrdManagement)) - out.DeployItemPickupTimeout = in.DeployItemPickupTimeout + out.DeployItemTimeouts = (*config.DeployItemTimeouts)(unsafe.Pointer(in.DeployItemTimeouts)) return nil } @@ -144,7 +180,7 @@ func autoConvert_config_LandscaperConfiguration_To_v1alpha1_LandscaperConfigurat } out.Metrics = (*MetricsConfiguration)(unsafe.Pointer(in.Metrics)) out.CrdManagement = (*CrdManagementConfiguration)(unsafe.Pointer(in.CrdManagement)) - out.DeployItemPickupTimeout = in.DeployItemPickupTimeout + out.DeployItemTimeouts = (*DeployItemTimeouts)(unsafe.Pointer(in.DeployItemTimeouts)) return nil } diff --git a/vendor/github.com/gardener/landscaper/apis/config/v1alpha1/zz_generated.deepcopy.go b/vendor/github.com/gardener/landscaper/apis/config/v1alpha1/zz_generated.deepcopy.go index 8b191e571..2e736c412 100644 --- a/vendor/github.com/gardener/landscaper/apis/config/v1alpha1/zz_generated.deepcopy.go +++ b/vendor/github.com/gardener/landscaper/apis/config/v1alpha1/zz_generated.deepcopy.go @@ -12,6 +12,8 @@ package v1alpha1 import ( v2 "github.com/gardener/component-spec/bindings-go/apis/v2" runtime "k8s.io/apimachinery/pkg/runtime" + + corev1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -30,6 +32,37 @@ func (in *CrdManagementConfiguration) DeepCopy() *CrdManagementConfiguration { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeployItemTimeouts) DeepCopyInto(out *DeployItemTimeouts) { + *out = *in + if in.Pickup != nil { + in, out := &in.Pickup, &out.Pickup + *out = new(corev1alpha1.Duration) + **out = **in + } + if in.Abort != nil { + in, out := &in.Abort, &out.Abort + *out = new(corev1alpha1.Duration) + **out = **in + } + if in.ProgressingDefault != nil { + in, out := &in.ProgressingDefault, &out.ProgressingDefault + *out = new(corev1alpha1.Duration) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeployItemTimeouts. +func (in *DeployItemTimeouts) DeepCopy() *DeployItemTimeouts { + if in == nil { + return nil + } + out := new(DeployItemTimeouts) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LandscaperConfiguration) DeepCopyInto(out *LandscaperConfiguration) { *out = *in @@ -50,6 +83,11 @@ func (in *LandscaperConfiguration) DeepCopyInto(out *LandscaperConfiguration) { *out = new(CrdManagementConfiguration) **out = **in } + if in.DeployItemTimeouts != nil { + in, out := &in.DeployItemTimeouts, &out.DeployItemTimeouts + *out = new(DeployItemTimeouts) + (*in).DeepCopyInto(*out) + } return } diff --git a/vendor/github.com/gardener/landscaper/apis/config/zz_generated.deepcopy.go b/vendor/github.com/gardener/landscaper/apis/config/zz_generated.deepcopy.go index 9aacffe31..e245ab0c5 100644 --- a/vendor/github.com/gardener/landscaper/apis/config/zz_generated.deepcopy.go +++ b/vendor/github.com/gardener/landscaper/apis/config/zz_generated.deepcopy.go @@ -12,6 +12,8 @@ package config import ( v2 "github.com/gardener/component-spec/bindings-go/apis/v2" runtime "k8s.io/apimachinery/pkg/runtime" + + core "github.com/gardener/landscaper/apis/core" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -30,6 +32,37 @@ func (in *CrdManagementConfiguration) DeepCopy() *CrdManagementConfiguration { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeployItemTimeouts) DeepCopyInto(out *DeployItemTimeouts) { + *out = *in + if in.Pickup != nil { + in, out := &in.Pickup, &out.Pickup + *out = new(core.Duration) + **out = **in + } + if in.Abort != nil { + in, out := &in.Abort, &out.Abort + *out = new(core.Duration) + **out = **in + } + if in.ProgressingDefault != nil { + in, out := &in.ProgressingDefault, &out.ProgressingDefault + *out = new(core.Duration) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeployItemTimeouts. +func (in *DeployItemTimeouts) DeepCopy() *DeployItemTimeouts { + if in == nil { + return nil + } + out := new(DeployItemTimeouts) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LandscaperConfiguration) DeepCopyInto(out *LandscaperConfiguration) { *out = *in @@ -50,6 +83,11 @@ func (in *LandscaperConfiguration) DeepCopyInto(out *LandscaperConfiguration) { *out = new(CrdManagementConfiguration) **out = **in } + if in.DeployItemTimeouts != nil { + in, out := &in.DeployItemTimeouts, &out.DeployItemTimeouts + *out = new(DeployItemTimeouts) + (*in).DeepCopyInto(*out) + } return } diff --git a/vendor/github.com/gardener/landscaper/apis/core/types_deployitem.go b/vendor/github.com/gardener/landscaper/apis/core/types_deployitem.go index a61fa9431..34f9de5ab 100644 --- a/vendor/github.com/gardener/landscaper/apis/core/types_deployitem.go +++ b/vendor/github.com/gardener/landscaper/apis/core/types_deployitem.go @@ -54,6 +54,13 @@ type DeployItemSpec struct { // Note that the type information is used to determine the secret key and the type of the secret. // +optional RegistryPullSecrets []ObjectReference `json:"registryPullSecrets,omitempty"` + // Timeout specifies how long the deployer may take to apply the deploy item. + // When the time is exceeded, the landscaper will add the abort annotation to the deploy item + // and later put it in 'Failed' if the deployer doesn't handle the abort properly. + // Value has to be parsable by time.ParseDuration (or 'none' to deactivate the timeout). + // Defaults to ten minutes if not specified. + // +optional + Timeout *Duration `json:"timeout,omitempty"` } // DeployItemStatus contains the status of a deploy item @@ -72,6 +79,10 @@ type DeployItemStatus struct { // LastError describes the last error that occurred. LastError *Error `json:"lastError,omitempty"` + // LastReconcileTime indicates when the reconciliation of the last change to the deploy item has started + // +optional + LastReconcileTime *metav1.Time `json:"lastReconcileTime,omitempty"` + // ProviderStatus contains the provider specific status // +optional ProviderStatus *runtime.RawExtension `json:"providerStatus,omitempty"` diff --git a/vendor/github.com/gardener/landscaper/apis/core/types_shared.go b/vendor/github.com/gardener/landscaper/apis/core/types_shared.go index 1f1a76b2f..0e8689c68 100644 --- a/vendor/github.com/gardener/landscaper/apis/core/types_shared.go +++ b/vendor/github.com/gardener/landscaper/apis/core/types_shared.go @@ -7,6 +7,8 @@ package core import ( "encoding/json" "errors" + "fmt" + "time" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -36,6 +38,40 @@ func (s *JSONSchemaDefinition) UnmarshalJSON(data []byte) error { func (_ JSONSchemaDefinition) OpenAPISchemaType() []string { return []string{"object"} } func (_ JSONSchemaDefinition) OpenAPISchemaFormat() string { return "" } +// Duration is a wrapper for time.Duration that implements JSON marshalling and openapi scheme. +type Duration struct { + time.Duration +} + +// MarshalJSON implements the json marshaling for a Duration +func (d Duration) MarshalJSON() ([]byte, error) { + if d.Duration == 0 { + return []byte("\"none\""), nil + } + return []byte(fmt.Sprintf("%q", d.Duration.String())), nil +} + +// UnmarshalJSON implements json unmarshaling for a Duration +func (d *Duration) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "none" { + *d = Duration{Duration: 0} + return nil + } + dur, err := time.ParseDuration(s) + if err != nil { + return fmt.Errorf("unable to parse string into a duration: %w", err) + } + *d = Duration{Duration: dur} + return nil +} + +func (_ Duration) OpenAPISchemaType() []string { return []string{"string"} } +func (_ Duration) OpenAPISchemaFormat() string { return "" } + // AnyJSON enhances the json.RawMessages with a dedicated openapi definition so that all // it is correctly generated type AnyJSON struct { diff --git a/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/constants.go b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/constants.go index 5373804cf..9208d802c 100644 --- a/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/constants.go +++ b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/constants.go @@ -22,6 +22,9 @@ const ( // ReconcileTimestampAnnotation is used to recognize timeouts in deployitems ReconcileTimestampAnnotation = "landscaper.gardener.cloud/reconcile-time" + // AbortTimestampAnnotation is used to recognize timeouts in deployitems + AbortTimestampAnnotation = "landscaper.gardener.cloud/abort-time" + // Labels // LandscaperComponentLabelName is the name of the labels the holds the information about landscaper components. diff --git a/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/helper/helpers.go b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/helper/helpers.go index 17838c074..4a9c195d7 100644 --- a/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/helper/helpers.go +++ b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/helper/helpers.go @@ -13,6 +13,13 @@ import ( "github.com/gardener/landscaper/apis/core/v1alpha1" ) +type TimestampAnnotation string + +const ( + ReconcileTimestamp = TimestampAnnotation(v1alpha1.ReconcileTimestampAnnotation) + AbortTimestamp = TimestampAnnotation(v1alpha1.AbortTimestampAnnotation) +) + // HasOperation checks if the obj has the given operation annotation func HasOperation(obj metav1.ObjectMeta, op v1alpha1.Operation) bool { currentOp, ok := obj.Annotations[v1alpha1.OperationAnnotation] @@ -32,24 +39,18 @@ func SetOperation(obj *metav1.ObjectMeta, op v1alpha1.Operation) { metav1.SetMetaDataAnnotation(obj, v1alpha1.OperationAnnotation, string(op)) } -// HasReconcileTimestampAnnotation checks if the obj has the given timeout annotation -func HasReconcileTimestampAnnotation(obj metav1.ObjectMeta) bool { - _, ok := obj.Annotations[v1alpha1.ReconcileTimestampAnnotation] - return ok -} - -func GetReconcileTimestampAnnotation(obj metav1.ObjectMeta) (time.Time, error) { - return time.Parse(time.RFC3339, obj.Annotations[v1alpha1.ReconcileTimestampAnnotation]) +func GetTimestampAnnotation(obj metav1.ObjectMeta, ta TimestampAnnotation) (time.Time, error) { + return time.Parse(time.RFC3339, obj.Annotations[string(ta)]) } -// SetReconcileTimestampAnnotationNow sets the timeout annotation with the current timestamp. -func SetReconcileTimestampAnnotationNow(obj *metav1.ObjectMeta) { - SetReconcileTimestampAnnotation(obj, time.Now()) +// SetTimestampAnnotationNow sets the timeout annotation with the current timestamp. +func SetTimestampAnnotationNow(obj *metav1.ObjectMeta, ta TimestampAnnotation) { + metav1.SetMetaDataAnnotation(obj, string(ta), time.Now().Format(time.RFC3339)) } -// SetReconcileTimestampAnnotation sets the timeout annotation with the given timestamp. -func SetReconcileTimestampAnnotation(obj *metav1.ObjectMeta, ts time.Time) { - metav1.SetMetaDataAnnotation(obj, v1alpha1.ReconcileTimestampAnnotation, ts.Format(time.RFC3339)) +func SetAbortOperationAndTimestamp(obj *metav1.ObjectMeta) { + SetOperation(obj, v1alpha1.AbortOperation) + SetTimestampAnnotationNow(obj, AbortTimestamp) } // InitCondition initializes a new Condition with an Unknown status. diff --git a/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/types_deployitem.go b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/types_deployitem.go index a83173051..f97f66614 100644 --- a/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/types_deployitem.go +++ b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/types_deployitem.go @@ -64,6 +64,13 @@ type DeployItemSpec struct { // Note that the type information is used to determine the secret key and the type of the secret. // +optional RegistryPullSecrets []ObjectReference `json:"registryPullSecrets,omitempty"` + // Timeout specifies how long the deployer may take to apply the deploy item. + // When the time is exceeded, the landscaper will add the abort annotation to the deploy item + // and later put it in 'Failed' if the deployer doesn't handle the abort properly. + // Value has to be parsable by time.ParseDuration (or 'none' to deactivate the timeout). + // Defaults to ten minutes if not specified. + // +optional + Timeout *Duration `json:"timeout,omitempty"` } // DeployItemStatus contains the status of a deploy item. @@ -83,6 +90,10 @@ type DeployItemStatus struct { // LastError describes the last error that occurred. LastError *Error `json:"lastError,omitempty"` + // LastReconcileTime indicates when the reconciliation of the last change to the deploy item has started + // +optional + LastReconcileTime *metav1.Time `json:"lastReconcileTime,omitempty"` + // ProviderStatus contains the provider specific status // +optional // +kubebuilder:validation:XEmbeddedResource diff --git a/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/types_shared.go b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/types_shared.go index 38021c0bd..29c112340 100644 --- a/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/types_shared.go +++ b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/types_shared.go @@ -7,6 +7,8 @@ package v1alpha1 import ( "encoding/json" "errors" + "fmt" + "time" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -37,6 +39,40 @@ func (s *JSONSchemaDefinition) UnmarshalJSON(data []byte) error { func (_ JSONSchemaDefinition) OpenAPISchemaType() []string { return []string{"object"} } func (_ JSONSchemaDefinition) OpenAPISchemaFormat() string { return "" } +// Duration is a wrapper for time.Duration that implements JSON marshalling and openapi scheme. +type Duration struct { + time.Duration +} + +// MarshalJSON implements the json marshaling for a Duration +func (d Duration) MarshalJSON() ([]byte, error) { + if d.Duration == 0 { + return []byte("\"none\""), nil + } + return []byte(fmt.Sprintf("%q", d.Duration.String())), nil +} + +// UnmarshalJSON implements json unmarshaling for a Duration +func (d *Duration) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "none" { + *d = Duration{Duration: 0} + return nil + } + dur, err := time.ParseDuration(s) + if err != nil { + return fmt.Errorf("unable to parse string into a duration: %w", err) + } + *d = Duration{Duration: dur} + return nil +} + +func (_ Duration) OpenAPISchemaType() []string { return []string{"string"} } +func (_ Duration) OpenAPISchemaFormat() string { return "" } + // AnyJSON enhances the json.RawMessages with a dedicated openapi definition so that all // it is correctly generated // +k8s:openapi-gen=true diff --git a/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/zz_generated.conversion.go b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/zz_generated.conversion.go index c8666494e..a6c2c4095 100644 --- a/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/zz_generated.conversion.go +++ b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/zz_generated.conversion.go @@ -11,10 +11,12 @@ package v1alpha1 import ( json "encoding/json" + time "time" unsafe "unsafe" v2 "github.com/gardener/component-spec/bindings-go/apis/v2" - v1 "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" conversion "k8s.io/apimachinery/pkg/conversion" runtime "k8s.io/apimachinery/pkg/runtime" selection "k8s.io/apimachinery/pkg/selection" @@ -219,6 +221,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*Duration)(nil), (*core.Duration)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_Duration_To_core_Duration(a.(*Duration), b.(*core.Duration), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*core.Duration)(nil), (*Duration)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_core_Duration_To_v1alpha1_Duration(a.(*core.Duration), b.(*Duration), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*Error)(nil), (*core.Error)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_Error_To_core_Error(a.(*Error), b.(*core.Error), scope) }); err != nil { @@ -1039,6 +1051,7 @@ func autoConvert_v1alpha1_DeployItemSpec_To_core_DeployItemSpec(in *DeployItemSp out.Target = (*core.ObjectReference)(unsafe.Pointer(in.Target)) out.Configuration = (*runtime.RawExtension)(unsafe.Pointer(in.Configuration)) out.RegistryPullSecrets = *(*[]core.ObjectReference)(unsafe.Pointer(&in.RegistryPullSecrets)) + out.Timeout = (*core.Duration)(unsafe.Pointer(in.Timeout)) return nil } @@ -1052,6 +1065,7 @@ func autoConvert_core_DeployItemSpec_To_v1alpha1_DeployItemSpec(in *core.DeployI out.Target = (*ObjectReference)(unsafe.Pointer(in.Target)) out.Configuration = (*runtime.RawExtension)(unsafe.Pointer(in.Configuration)) out.RegistryPullSecrets = *(*[]ObjectReference)(unsafe.Pointer(&in.RegistryPullSecrets)) + out.Timeout = (*Duration)(unsafe.Pointer(in.Timeout)) return nil } @@ -1065,6 +1079,7 @@ func autoConvert_v1alpha1_DeployItemStatus_To_core_DeployItemStatus(in *DeployIt out.ObservedGeneration = in.ObservedGeneration out.Conditions = *(*[]core.Condition)(unsafe.Pointer(&in.Conditions)) out.LastError = (*core.Error)(unsafe.Pointer(in.LastError)) + out.LastReconcileTime = (*v1.Time)(unsafe.Pointer(in.LastReconcileTime)) out.ProviderStatus = (*runtime.RawExtension)(unsafe.Pointer(in.ProviderStatus)) out.ExportReference = (*core.ObjectReference)(unsafe.Pointer(in.ExportReference)) return nil @@ -1080,6 +1095,7 @@ func autoConvert_core_DeployItemStatus_To_v1alpha1_DeployItemStatus(in *core.Dep out.ObservedGeneration = in.ObservedGeneration out.Conditions = *(*[]Condition)(unsafe.Pointer(&in.Conditions)) out.LastError = (*Error)(unsafe.Pointer(in.LastError)) + out.LastReconcileTime = (*v1.Time)(unsafe.Pointer(in.LastReconcileTime)) out.ProviderStatus = (*runtime.RawExtension)(unsafe.Pointer(in.ProviderStatus)) out.ExportReference = (*ObjectReference)(unsafe.Pointer(in.ExportReference)) return nil @@ -1120,6 +1136,26 @@ func Convert_core_DeployItemTemplate_To_v1alpha1_DeployItemTemplate(in *core.Dep return autoConvert_core_DeployItemTemplate_To_v1alpha1_DeployItemTemplate(in, out, s) } +func autoConvert_v1alpha1_Duration_To_core_Duration(in *Duration, out *core.Duration, s conversion.Scope) error { + out.Duration = time.Duration(in.Duration) + return nil +} + +// Convert_v1alpha1_Duration_To_core_Duration is an autogenerated conversion function. +func Convert_v1alpha1_Duration_To_core_Duration(in *Duration, out *core.Duration, s conversion.Scope) error { + return autoConvert_v1alpha1_Duration_To_core_Duration(in, out, s) +} + +func autoConvert_core_Duration_To_v1alpha1_Duration(in *core.Duration, out *Duration, s conversion.Scope) error { + out.Duration = time.Duration(in.Duration) + return nil +} + +// Convert_core_Duration_To_v1alpha1_Duration is an autogenerated conversion function. +func Convert_core_Duration_To_v1alpha1_Duration(in *core.Duration, out *Duration, s conversion.Scope) error { + return autoConvert_core_Duration_To_v1alpha1_Duration(in, out, s) +} + func autoConvert_v1alpha1_Error_To_core_Error(in *Error, out *core.Error, s conversion.Scope) error { out.Operation = in.Operation out.LastTransitionTime = in.LastTransitionTime @@ -1859,7 +1895,7 @@ func Convert_core_StaticDataSource_To_v1alpha1_StaticDataSource(in *core.StaticD } func autoConvert_v1alpha1_StaticDataValueFrom_To_core_StaticDataValueFrom(in *StaticDataValueFrom, out *core.StaticDataValueFrom, s conversion.Scope) error { - out.SecretKeyRef = (*v1.SecretKeySelector)(unsafe.Pointer(in.SecretKeyRef)) + out.SecretKeyRef = (*corev1.SecretKeySelector)(unsafe.Pointer(in.SecretKeyRef)) out.SecretLabelSelector = (*core.SecretLabelSelectorRef)(unsafe.Pointer(in.SecretLabelSelector)) return nil } @@ -1870,7 +1906,7 @@ func Convert_v1alpha1_StaticDataValueFrom_To_core_StaticDataValueFrom(in *Static } func autoConvert_core_StaticDataValueFrom_To_v1alpha1_StaticDataValueFrom(in *core.StaticDataValueFrom, out *StaticDataValueFrom, s conversion.Scope) error { - out.SecretKeyRef = (*v1.SecretKeySelector)(unsafe.Pointer(in.SecretKeyRef)) + out.SecretKeyRef = (*corev1.SecretKeySelector)(unsafe.Pointer(in.SecretKeyRef)) out.SecretLabelSelector = (*SecretLabelSelectorRef)(unsafe.Pointer(in.SecretLabelSelector)) return nil } diff --git a/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/zz_generated.deepcopy.go b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/zz_generated.deepcopy.go index cd5d7038e..ce1fea78e 100644 --- a/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/zz_generated.deepcopy.go +++ b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/zz_generated.deepcopy.go @@ -446,6 +446,11 @@ func (in *DeployItemSpec) DeepCopyInto(out *DeployItemSpec) { *out = make([]ObjectReference, len(*in)) copy(*out, *in) } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(Duration) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeployItemSpec. @@ -473,6 +478,10 @@ func (in *DeployItemStatus) DeepCopyInto(out *DeployItemStatus) { *out = new(Error) (*in).DeepCopyInto(*out) } + if in.LastReconcileTime != nil { + in, out := &in.LastReconcileTime, &out.LastReconcileTime + *out = (*in).DeepCopy() + } if in.ProviderStatus != nil { in, out := &in.ProviderStatus, &out.ProviderStatus *out = new(runtime.RawExtension) @@ -553,6 +562,21 @@ func (in DeployItemTemplateList) DeepCopy() DeployItemTemplateList { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Duration) DeepCopyInto(out *Duration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Duration. +func (in *Duration) DeepCopy() *Duration { + if in == nil { + return nil + } + out := new(Duration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Error) DeepCopyInto(out *Error) { *out = *in diff --git a/vendor/github.com/gardener/landscaper/apis/core/zz_generated.deepcopy.go b/vendor/github.com/gardener/landscaper/apis/core/zz_generated.deepcopy.go index 4f95ebd94..01910cdda 100644 --- a/vendor/github.com/gardener/landscaper/apis/core/zz_generated.deepcopy.go +++ b/vendor/github.com/gardener/landscaper/apis/core/zz_generated.deepcopy.go @@ -461,6 +461,11 @@ func (in *DeployItemSpec) DeepCopyInto(out *DeployItemSpec) { *out = make([]ObjectReference, len(*in)) copy(*out, *in) } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(Duration) + **out = **in + } return } @@ -489,6 +494,10 @@ func (in *DeployItemStatus) DeepCopyInto(out *DeployItemStatus) { *out = new(Error) (*in).DeepCopyInto(*out) } + if in.LastReconcileTime != nil { + in, out := &in.LastReconcileTime, &out.LastReconcileTime + *out = (*in).DeepCopy() + } if in.ProviderStatus != nil { in, out := &in.ProviderStatus, &out.ProviderStatus *out = new(runtime.RawExtension) @@ -572,6 +581,22 @@ func (in DeployItemTemplateList) DeepCopy() DeployItemTemplateList { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Duration) DeepCopyInto(out *Duration) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Duration. +func (in *Duration) DeepCopy() *Duration { + if in == nil { + return nil + } + out := new(Duration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Error) DeepCopyInto(out *Error) { *out = *in diff --git a/vendor/github.com/gardener/landscaper/apis/deployer/mock/types_provider.go b/vendor/github.com/gardener/landscaper/apis/deployer/mock/types_provider.go index c1442680c..dd51ad2eb 100644 --- a/vendor/github.com/gardener/landscaper/apis/deployer/mock/types_provider.go +++ b/vendor/github.com/gardener/landscaper/apis/deployer/mock/types_provider.go @@ -22,6 +22,10 @@ type ProviderConfiguration struct { // Phase sets the phase of the DeployItem Phase *lsv1alpha1.ExecutionPhase `json:"phase,omitempty"` + // InitialPhase sets the phase of the DeployItem, but only if it is empty or "Init" + // Additionally, setting it will suppress the DeployItem phase being set to "Succeeded" after successful reconciliation + InitialPhase *lsv1alpha1.ExecutionPhase `json:"initialPhase,omitempty"` + // ProviderStatus sets the provider status to the given value ProviderStatus *runtime.RawExtension `json:"providerStatus,omitempty"` diff --git a/vendor/github.com/gardener/landscaper/apis/deployer/mock/v1alpha1/types_provider.go b/vendor/github.com/gardener/landscaper/apis/deployer/mock/v1alpha1/types_provider.go index 81113d4de..efa90a109 100644 --- a/vendor/github.com/gardener/landscaper/apis/deployer/mock/v1alpha1/types_provider.go +++ b/vendor/github.com/gardener/landscaper/apis/deployer/mock/v1alpha1/types_provider.go @@ -22,6 +22,10 @@ type ProviderConfiguration struct { // Phase sets the phase of the DeployItem Phase *lsv1alpha1.ExecutionPhase `json:"phase,omitempty"` + // InitialPhase sets the phase of the DeployItem, but only if it is empty or "Init" + // Additionally, setting it will suppress the DeployItem phase being set to "Succeeded" after successful reconciliation + InitialPhase *lsv1alpha1.ExecutionPhase `json:"initialPhase,omitempty"` + // ProviderStatus sets the provider status to the given value ProviderStatus *runtime.RawExtension `json:"providerStatus,omitempty"` diff --git a/vendor/github.com/gardener/landscaper/apis/deployer/mock/v1alpha1/zz_generated.conversion.go b/vendor/github.com/gardener/landscaper/apis/deployer/mock/v1alpha1/zz_generated.conversion.go index 100cfe954..9f42fa9bc 100644 --- a/vendor/github.com/gardener/landscaper/apis/deployer/mock/v1alpha1/zz_generated.conversion.go +++ b/vendor/github.com/gardener/landscaper/apis/deployer/mock/v1alpha1/zz_generated.conversion.go @@ -72,6 +72,7 @@ func Convert_mock_Configuration_To_v1alpha1_Configuration(in *mock.Configuration func autoConvert_v1alpha1_ProviderConfiguration_To_mock_ProviderConfiguration(in *ProviderConfiguration, out *mock.ProviderConfiguration, s conversion.Scope) error { out.Phase = (*corev1alpha1.ExecutionPhase)(unsafe.Pointer(in.Phase)) + out.InitialPhase = (*corev1alpha1.ExecutionPhase)(unsafe.Pointer(in.InitialPhase)) out.ProviderStatus = (*runtime.RawExtension)(unsafe.Pointer(in.ProviderStatus)) out.Export = (*json.RawMessage)(unsafe.Pointer(in.Export)) return nil @@ -84,6 +85,7 @@ func Convert_v1alpha1_ProviderConfiguration_To_mock_ProviderConfiguration(in *Pr func autoConvert_mock_ProviderConfiguration_To_v1alpha1_ProviderConfiguration(in *mock.ProviderConfiguration, out *ProviderConfiguration, s conversion.Scope) error { out.Phase = (*corev1alpha1.ExecutionPhase)(unsafe.Pointer(in.Phase)) + out.InitialPhase = (*corev1alpha1.ExecutionPhase)(unsafe.Pointer(in.InitialPhase)) out.ProviderStatus = (*runtime.RawExtension)(unsafe.Pointer(in.ProviderStatus)) out.Export = (*json.RawMessage)(unsafe.Pointer(in.Export)) return nil diff --git a/vendor/github.com/gardener/landscaper/apis/deployer/mock/v1alpha1/zz_generated.deepcopy.go b/vendor/github.com/gardener/landscaper/apis/deployer/mock/v1alpha1/zz_generated.deepcopy.go index d4c76fcda..b90cdc6f5 100644 --- a/vendor/github.com/gardener/landscaper/apis/deployer/mock/v1alpha1/zz_generated.deepcopy.go +++ b/vendor/github.com/gardener/landscaper/apis/deployer/mock/v1alpha1/zz_generated.deepcopy.go @@ -58,6 +58,11 @@ func (in *ProviderConfiguration) DeepCopyInto(out *ProviderConfiguration) { *out = new(corev1alpha1.ExecutionPhase) **out = **in } + if in.InitialPhase != nil { + in, out := &in.InitialPhase, &out.InitialPhase + *out = new(corev1alpha1.ExecutionPhase) + **out = **in + } if in.ProviderStatus != nil { in, out := &in.ProviderStatus, &out.ProviderStatus *out = new(runtime.RawExtension) diff --git a/vendor/github.com/gardener/landscaper/apis/deployer/mock/zz_generated.deepcopy.go b/vendor/github.com/gardener/landscaper/apis/deployer/mock/zz_generated.deepcopy.go index 36ba630d9..e2b570de5 100644 --- a/vendor/github.com/gardener/landscaper/apis/deployer/mock/zz_generated.deepcopy.go +++ b/vendor/github.com/gardener/landscaper/apis/deployer/mock/zz_generated.deepcopy.go @@ -58,6 +58,11 @@ func (in *ProviderConfiguration) DeepCopyInto(out *ProviderConfiguration) { *out = new(v1alpha1.ExecutionPhase) **out = **in } + if in.InitialPhase != nil { + in, out := &in.InitialPhase, &out.InitialPhase + *out = new(v1alpha1.ExecutionPhase) + **out = **in + } if in.ProviderStatus != nil { in, out := &in.ProviderStatus, &out.ProviderStatus *out = new(runtime.RawExtension)