/*
Copyright 2022 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha3

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// DeviceSelector must have exactly one field set.
type DeviceSelector struct {
	// CEL contains a CEL expression for selecting a device.
	//
	// +optional
	// +oneOf=SelectorType
	CEL *CELDeviceSelector `json:"cel,omitempty" protobuf:"bytes,1,opt,name=cel"`
}

// CELDeviceSelector contains a CEL expression for selecting a device.
type CELDeviceSelector struct {
	// Expression is a CEL expression which evaluates a single device. It
	// must evaluate to true when the device under consideration satisfies
	// the desired criteria, and false when it does not. Any other result
	// is an error and causes allocation of devices to abort.
	//
	// The expression's input is an object named "device", which carries
	// the following properties:
	//  - driver (string): the name of the driver which defines this device.
	//  - attributes (map[string]object): the device's attributes, grouped by prefix
	//    (e.g. device.attributes["dra.example.com"] evaluates to an object with all
	//    of the attributes which were prefixed by "dra.example.com".
	//  - capacity (map[string]object): the device's capacities, grouped by prefix.
	//
	// Example: Consider a device with driver="dra.example.com", which exposes
	// two attributes named "model" and "ext.example.com/family" and which
	// exposes one capacity named "modules". This input to this expression
	// would have the following fields:
	//
	//     device.driver
	//     device.attributes["dra.example.com"].model
	//     device.attributes["ext.example.com"].family
	//     device.capacity["dra.example.com"].modules
	//
	// The device.driver field can be used to check for a specific driver,
	// either as a high-level precondition (i.e. you only want to consider
	// devices from this driver) or as part of a multi-clause expression
	// that is meant to consider devices from different drivers.
	//
	// The value type of each attribute is defined by the device
	// definition, and users who write these expressions must consult the
	// documentation for their specific drivers. The value type of each
	// capacity is Quantity.
	//
	// If an unknown prefix is used as a lookup in either device.attributes
	// or device.capacity, an empty map will be returned. Any reference to
	// an unknown field will cause an evaluation error and allocation to
	// abort.
	//
	// A robust expression should check for the existence of attributes
	// before referencing them.
	//
	// For ease of use, the cel.bind() function is enabled, and can be used
	// to simplify expressions that access multiple attributes with the
	// same domain. For example:
	//
	//     cel.bind(dra, device.attributes["dra.example.com"], dra.someBool && dra.anotherBool)
	//
	// The length of the expression must be smaller or equal to 10 Ki. The
	// cost of evaluating it is also limited based on the estimated number
	// of logical steps.
	//
	// +required
	Expression string `json:"expression" protobuf:"bytes,1,name=expression"`
}

// CELSelectorExpressionMaxCost specifies the cost limit for a single CEL selector
// evaluation.
//
// There is no overall budget for selecting a device, so the actual time
// required for that is proportional to the number of CEL selectors and how
// often they need to be evaluated, which can vary depending on several factors
// (number of devices, cluster utilization, additional constraints).
//
// Validation against this limit and [CELSelectorExpressionMaxLength] happens
// only when setting an expression for the first time or when changing it. If
// the limits are changed in a future Kubernetes release, existing users are
// guaranteed that existing expressions will continue to be valid.
//
// However, the kube-scheduler also applies this cost limit at runtime, so it
// could happen that a valid expression fails at runtime after an up- or
// downgrade. This can also happen without version skew when the cost estimate
// underestimated the actual cost. That this might happen is the reason why
// kube-scheduler enforces the runtime limit instead of relying on validation.
//
// According to
// https://github.com/kubernetes/kubernetes/blob/4aeaf1e99e82da8334c0d6dddd848a194cd44b4f/staging/src/k8s.io/apiserver/pkg/apis/cel/config.go#L20-L22,
// this gives roughly 0.1 second for each expression evaluation.
// However, this depends on how fast the machine is.
const CELSelectorExpressionMaxCost = 1000000

// CELSelectorExpressionMaxLength is the maximum length of a CEL selector expression string.
const CELSelectorExpressionMaxLength = 10 * 1024

// The device this taint is attached to has the "effect" on
// any claim which does not tolerate the taint and, through the claim,
// to pods using the claim.
//
// +protobuf.options.(gogoproto.goproto_stringer)=false
type DeviceTaint struct {
	// The taint key to be applied to a device.
	// Must be a label name.
	//
	// +required
	Key string `json:"key" protobuf:"bytes,1,name=key"`

	// The taint value corresponding to the taint key.
	// Must be a label value.
	//
	// +optional
	Value string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"`

	// The effect of the taint on claims that do not tolerate the taint
	// and through such claims on the pods using them.
	//
	// Valid effects are None, NoSchedule and NoExecute. PreferNoSchedule as used for
	// nodes is not valid here. More effects may get added in the future.
	// Consumers must treat unknown effects like None.
	//
	// +required
	// +k8s:required
	Effect DeviceTaintEffect `json:"effect" protobuf:"bytes,3,name=effect,casttype=DeviceTaintEffect"`

	// ^^^^
	//
	// Implementing PreferNoSchedule would depend on a scoring solution for DRA.
	// It might get added as part of that.
	//
	// A possible future new effect is NoExecuteWithPodDisruptionBudget:
	// honor the pod disruption budget instead of simply deleting pods.
	// This is currently undecided, it could also be a separate field.
	//
	// Validation must be prepared to allow unknown enums in stored objects,
	// which will enable adding new enums within a single release without
	// ratcheting.

	// TimeAdded represents the time at which the taint was added or
	// (only in a DeviceTaintRule) the effect was modified.
	// Added automatically during create or update if not set.
	//
	// In addition, in a DeviceTaintRule a value provided during
	// an update gets replaced with the current time if the provided
	// value is the same as the old one and the new effect is different.
	// Changing the key and/or value while keeping the effect unchanged
	// is possible and does not update the time stamp because the eviction
	// which uses it is either already started (NoExecute) or
	// not started yet (NoEffect, NoSchedule).
	//
	// +optional
	TimeAdded *metav1.Time `json:"timeAdded,omitempty" protobuf:"bytes,4,opt,name=timeAdded"`

	// ^^^
	//
	// This field was defined as "It is only written for NoExecute taints." for node taints.
	// But in practice, Kubernetes never did anything with it (no validation, no defaulting,
	// ignored during pod eviction in pkg/controller/tainteviction).
}

// +enum
// +k8s:enum
type DeviceTaintEffect string

const (
	// No effect, the taint is purely informational.
	DeviceTaintEffectNone DeviceTaintEffect = "None"

	// Do not allow new pods to schedule which use a tainted device unless they tolerate the taint,
	// but allow all pods submitted to Kubelet without going through the scheduler
	// to start, and allow all already-running pods to continue running.
	DeviceTaintEffectNoSchedule DeviceTaintEffect = "NoSchedule"

	// Evict any already-running pods that do not tolerate the device taint.
	DeviceTaintEffectNoExecute DeviceTaintEffect = "NoExecute"
)

// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.33

// DeviceTaintRule adds one taint to all devices which match the selector.
// This has the same effect as if the taint was specified directly
// in the ResourceSlice by the DRA driver.
type DeviceTaintRule struct {
	metav1.TypeMeta `json:",inline"`
	// Standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

	// Spec specifies the selector and one taint.
	//
	// Changing the spec automatically increments the metadata.generation number.
	// +required
	Spec DeviceTaintRuleSpec `json:"spec" protobuf:"bytes,2,name=spec"`

	// Status provides information about what was requested in the spec.
	//
	// +optional
	Status DeviceTaintRuleStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}

// DeviceTaintRuleSpec specifies the selector and one taint.
type DeviceTaintRuleSpec struct {
	// DeviceSelector defines which device(s) the taint is applied to.
	// All selector criteria must be satisfied for a device to
	// match. The empty selector matches all devices. Without
	// a selector, no devices are matches.
	//
	// +optional
	DeviceSelector *DeviceTaintSelector `json:"deviceSelector,omitempty" protobuf:"bytes,1,opt,name=deviceSelector"`

	// The taint that gets applied to matching devices.
	//
	// +required
	Taint DeviceTaint `json:"taint,omitempty" protobuf:"bytes,2,rep,name=taint"`
}

// DeviceTaintSelector defines which device(s) a DeviceTaintRule applies to.
// The empty selector matches all devices. Without a selector, no devices
// are matched.
type DeviceTaintSelector struct {
	// If DeviceClassName is set, the selectors defined there must be
	// satisfied by a device to be selected. This field corresponds
	// to class.metadata.name.
	//
	// +optional
	//
	// Tombstoned since 1.35 because it turned out that supporting this in all cases
	// would depend on copying the device attributes into the ResourceClaim allocation
	// result. Without that the eviction controller cannot evaluate these CEL expressions.
	//
	// DeviceClassName *string `json:"deviceClassName,omitempty" protobuf:"bytes,1,opt,name=deviceClassName"`

	// If driver is set, only devices from that driver are selected.
	// This fields corresponds to slice.spec.driver.
	//
	// +optional
	Driver *string `json:"driver,omitempty" protobuf:"bytes,2,opt,name=driver"`

	// If pool is set, only devices in that pool are selected.
	//
	// Also setting the driver name may be useful to avoid
	// ambiguity when different drivers use the same pool name,
	// but this is not required because selecting pools from
	// different drivers may also be useful, for example when
	// drivers with node-local devices use the node name as
	// their pool name.
	//
	// +optional
	Pool *string `json:"pool,omitempty" protobuf:"bytes,3,opt,name=pool"`

	// If device is set, only devices with that name are selected.
	// This field corresponds to slice.spec.devices[].name.
	//
	// Setting also driver and pool may be required to avoid ambiguity,
	// but is not required.
	//
	// +optional
	Device *string `json:"device,omitempty" protobuf:"bytes,4,opt,name=device"`

	// Selectors contains the same selection criteria as a ResourceClaim.
	// Currently, CEL expressions are supported. All of these selectors
	// must be satisfied.
	//
	// +optional
	// +listType=atomic
	//
	// Tombstoned since 1.35 because it turned out that supporting this in all cases
	// would depend on copying the device attributes into the ResourceClaim allocation
	// result. Without that the eviction controller cannot evaluate these CEL expressions.
	//
	// Selectors []DeviceSelector `json:"selectors,omitempty" protobuf:"bytes,5,rep,name=selectors"`
}

// DeviceTaintRuleStatus provides information about an on-going pod eviction.
type DeviceTaintRuleStatus struct {
	// Conditions provide information about the state of the DeviceTaintRule
	// and the cluster at some point in time,
	// in a machine-readable and human-readable format.
	//
	// The following condition is currently defined as part of this API, more may
	// get added:
	// - Type: EvictionInProgress
	// - Status: True if there are currently pods which need to be evicted, False otherwise
	//   (includes the effects which don't cause eviction).
	// - Reason: not specified, may change
	// - Message: includes information about number of pending pods and already evicted pods
	//   in a human-readable format, updated periodically, may change
	//
	// For `effect: None`, the condition above gets set once for each change to
	// the spec, with the message containing information about what would happen
	// if the effect was `NoExecute`. This feedback can be used to decide whether
	// changing the effect to `NoExecute` will work as intended. It only gets
	// set once to avoid having to constantly update the status.
	//
	// Must have 8 or fewer entries.
	//
	// +optional
	// +listType=map
	// +listMapKey=type
	// +patchStrategy=merge
	// +patchMergeKey=type
	Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
}

// DeviceTaintRuleStatusMaxConditions is the maximum number of conditions in DeviceTaintRuleStatus.
const DeviceTaintRuleStatusMaxConditions = 8

// DeviceTaintConditionEvictionInProgress is the publicly documented condition type for the DeviceTaintRuleStatus.
const DeviceTaintConditionEvictionInProgress = "EvictionInProgress"

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.33

// DeviceTaintRuleList is a collection of DeviceTaintRules.
type DeviceTaintRuleList struct {
	metav1.TypeMeta `json:",inline"`
	// Standard list metadata
	// +optional
	metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

	// Items is the list of DeviceTaintRules.
	Items []DeviceTaintRule `json:"items" protobuf:"bytes,2,rep,name=items"`
}

// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.36
// +k8s:supportsSubresource="/status"

// ResourcePoolStatusRequest triggers a one-time calculation of resource pool status
// based on the provided filters. Once status is set, the request is considered complete and will not be reprocessed.
// Users should delete and recreate requests to get updated information.
type ResourcePoolStatusRequest struct {
	metav1.TypeMeta `json:",inline"`
	// Standard object metadata
	// +required
	metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

	// Spec defines the filters for which pools to include in the status.
	// The spec is immutable once created.
	//
	// +required
	// +k8s:immutable
	Spec ResourcePoolStatusRequestSpec `json:"spec" protobuf:"bytes,2,name=spec"`

	// Status is populated by the controller with the calculated pool status.
	// When status is non-nil, the request is considered complete and the
	// entire object becomes immutable.
	//
	// +optional
	// +k8s:optional
	Status *ResourcePoolStatusRequestStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}

// ResourcePoolStatusRequestSpec defines the filters for the pool status request.
type ResourcePoolStatusRequestSpec struct {
	// Driver specifies the DRA driver name to filter pools.
	// Only pools from ResourceSlices with this driver will be included.
	// Must be a DNS subdomain (e.g., "gpu.example.com").
	//
	// +required
	// +k8s:required
	// +k8s:format=k8s-long-name-caseless
	Driver string `json:"driver" protobuf:"bytes,1,name=driver"`

	// PoolName optionally filters to a specific pool name.
	// If not specified, all pools from the specified driver are included.
	// When specified, must be a non-empty valid resource pool name
	// (DNS subdomains separated by "/").
	//
	// +optional
	// +k8s:optional
	// +k8s:format=k8s-resource-pool-name
	PoolName *string `json:"poolName,omitempty" protobuf:"bytes,2,opt,name=poolName"`

	// Limit optionally specifies the maximum number of pools to return in the status.
	// If more pools match the filter criteria, the response will be truncated
	// (i.e., len(status.pools) < status.poolCount).
	//
	// Default: 100
	// Minimum: 1
	// Maximum: 1000
	//
	// +optional
	// +k8s:optional
	// +default=100
	// +k8s:minimum=1
	// +k8s:maximum=1000
	Limit *int32 `json:"limit,omitempty" protobuf:"varint,3,opt,name=limit"`
}

// ResourcePoolStatusRequestLimitDefault is the default value for spec.limit.
const ResourcePoolStatusRequestLimitDefault int32 = 100

// ResourcePoolStatusRequestLimitMax is the maximum allowed value for spec.limit.
const ResourcePoolStatusRequestLimitMax int32 = 1000

// ResourcePoolStatusRequestStatus contains the calculated pool status information.
type ResourcePoolStatusRequestStatus struct {
	// PoolCount is the total number of pools that matched the filter criteria,
	// regardless of truncation. This helps users understand how many pools exist
	// even when the response is truncated. A value of 0 means no pools matched
	// the filter criteria.
	//
	// +required
	// +k8s:required
	// +k8s:minimum=0
	PoolCount *int32 `json:"poolCount,omitempty" protobuf:"varint,6,opt,name=poolCount"`

	// Pools contains the first `spec.limit` matching pools, sorted by driver
	// then pool name. If `len(pools) < poolCount`, the list was truncated.
	// When omitted, no pools matched the request filters.
	//
	// +optional
	// +k8s:optional
	// +listType=atomic
	// +k8s:listType=atomic
	// +k8s:maxItems=1000
	Pools []PoolStatus `json:"pools,omitempty" protobuf:"bytes,2,rep,name=pools"`

	// Conditions provide information about the state of the request.
	// A condition with type=Complete or type=Failed will always be set
	// when the status is populated.
	//
	// Known condition types:
	// - "Complete": True when the request has been processed successfully
	// - "Failed": True when the request could not be processed
	//
	// +optional
	// +k8s:optional
	// +listType=map
	// +k8s:listType=map
	// +listMapKey=type
	// +k8s:listMapKey=type
	// +patchStrategy=merge
	// +patchMergeKey=type
	// +k8s:maxItems=10
	Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,3,rep,name=conditions"`
}

// PoolStatus contains status information for a single resource pool.
type PoolStatus struct {
	// Driver is the DRA driver name for this pool.
	// Must be a DNS subdomain (e.g., "gpu.example.com").
	//
	// +required
	// +k8s:required
	// +k8s:format=k8s-long-name-caseless
	Driver string `json:"driver,omitempty" protobuf:"bytes,1,name=driver"`

	// PoolName is the name of the pool.
	// Must be a valid resource pool name (DNS subdomains separated by "/").
	//
	// +required
	// +k8s:required
	// +k8s:format=k8s-resource-pool-name
	PoolName string `json:"poolName,omitempty" protobuf:"bytes,2,name=poolName"`

	// Generation is the pool generation observed across all ResourceSlices
	// in this pool. Only the latest generation is reported. During a generation
	// rollout, if not all slices at the latest generation have been published,
	// the pool is included with a validationError and device counts unset.
	//
	// +required
	// +k8s:required
	// +k8s:minimum=0
	Generation int64 `json:"generation" protobuf:"varint,9,opt,name=generation"`

	// ResourceSliceCount is the number of ResourceSlices that make up this pool.
	// May be unset when validationError is set.
	//
	// +optional
	// +k8s:optional
	// +k8s:minimum=1
	ResourceSliceCount *int32 `json:"resourceSliceCount,omitempty" protobuf:"varint,8,opt,name=resourceSliceCount"`

	// TotalDevices is the total number of devices in the pool across all slices.
	// A value of 0 means the pool has no devices.
	// May be unset when validationError is set.
	//
	// +optional
	// +k8s:optional
	// +k8s:minimum=0
	TotalDevices *int32 `json:"totalDevices,omitempty" protobuf:"varint,4,opt,name=totalDevices"`

	// AllocatedDevices is the number of devices currently allocated to claims.
	// A value of 0 means no devices are allocated.
	// May be unset when validationError is set.
	//
	// +optional
	// +k8s:optional
	// +k8s:minimum=0
	AllocatedDevices *int32 `json:"allocatedDevices,omitempty" protobuf:"varint,5,opt,name=allocatedDevices"`

	// AvailableDevices is the number of devices available for allocation.
	// This equals TotalDevices - AllocatedDevices - UnavailableDevices.
	// A value of 0 means no devices are currently available.
	// May be unset when validationError is set.
	//
	// +optional
	// +k8s:optional
	// +k8s:minimum=0
	AvailableDevices *int32 `json:"availableDevices,omitempty" protobuf:"varint,6,opt,name=availableDevices"`

	// UnavailableDevices is the number of devices that are not available
	// due to taints or other conditions, but are not allocated.
	// A value of 0 means all unallocated devices are available.
	// May be unset when validationError is set.
	//
	// +optional
	// +k8s:optional
	// +k8s:minimum=0
	UnavailableDevices *int32 `json:"unavailableDevices,omitempty" protobuf:"varint,7,opt,name=unavailableDevices"`

	// NodeName is the node this pool is associated with.
	// When omitted, the pool is not associated with a specific node.
	// Must be a valid DNS subdomain name (RFC1123).
	//
	// +optional
	// +k8s:optional
	// +k8s:format=k8s-long-name
	NodeName *string `json:"nodeName,omitempty" protobuf:"bytes,3,opt,name=nodeName"`

	// ValidationError is set when the pool's data could not be fully
	// validated (e.g., incomplete slice publication). When set, device
	// count fields and ResourceSliceCount may be unset.
	//
	// +optional
	// +k8s:optional
	// +k8s:maxBytes=256
	ValidationError *string `json:"validationError,omitempty" protobuf:"bytes,10,opt,name=validationError"`
}

// ResourcePoolStatusRequestConditionComplete is the condition type for completed requests.
const ResourcePoolStatusRequestConditionComplete = "Complete"

// ResourcePoolStatusRequestConditionFailed is the condition type for failed requests.
const ResourcePoolStatusRequestConditionFailed = "Failed"

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.36

// ResourcePoolStatusRequestList is a collection of ResourcePoolStatusRequests.
type ResourcePoolStatusRequestList struct {
	metav1.TypeMeta `json:",inline"`
	// Standard list metadata
	// +optional
	metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

	// Items is the list of ResourcePoolStatusRequests.
	Items []ResourcePoolStatusRequest `json:"items" protobuf:"bytes,2,rep,name=items"`
}
