diff --git a/Makefile b/Makefile index 16d4e1ce..9fe54983 100644 --- a/Makefile +++ b/Makefile @@ -64,6 +64,7 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust output:rbac:artifacts:config=deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/ cp deploy/helm/jumpstarter/crds/* deploy/operator/config/crd/bases/ + cp deploy/helm/jumpstarter/crds/* deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/ .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. diff --git a/api/v1alpha1/exporter_helpers.go b/api/v1alpha1/exporter_helpers.go index 2890f80a..882be7b7 100644 --- a/api/v1alpha1/exporter_helpers.go +++ b/api/v1alpha1/exporter_helpers.go @@ -4,6 +4,7 @@ import ( "strings" cpb "github.com/jumpstarter-dev/jumpstarter-controller/internal/protocol/jumpstarter/client/v1" + pb "github.com/jumpstarter-dev/jumpstarter-controller/internal/protocol/jumpstarter/v1" "github.com/jumpstarter-dev/jumpstarter-controller/internal/service/utils" "k8s.io/apimachinery/pkg/api/meta" kclient "sigs.k8s.io/controller-runtime/pkg/client" @@ -25,13 +26,37 @@ func (e *Exporter) Usernames(prefix string) []string { } func (e *Exporter) ToProtobuf() *cpb.Exporter { - // get online status from conditions + // get online status from conditions (deprecated, kept for backward compatibility) isOnline := meta.IsStatusConditionTrue(e.Status.Conditions, string(ExporterConditionTypeOnline)) return &cpb.Exporter{ - Name: utils.UnparseExporterIdentifier(kclient.ObjectKeyFromObject(e)), - Labels: e.Labels, - Online: isOnline, + Name: utils.UnparseExporterIdentifier(kclient.ObjectKeyFromObject(e)), + Labels: e.Labels, + Online: isOnline, + Status: stringToProtoStatus(e.Status.ExporterStatusValue), + StatusMessage: e.Status.StatusMessage, + } +} + +// stringToProtoStatus converts the CRD string value to the proto ExporterStatus enum +func stringToProtoStatus(state string) pb.ExporterStatus { + switch state { + case ExporterStatusOffline: + return pb.ExporterStatus_EXPORTER_STATUS_OFFLINE + case ExporterStatusAvailable: + return pb.ExporterStatus_EXPORTER_STATUS_AVAILABLE + case ExporterStatusBeforeLeaseHook: + return pb.ExporterStatus_EXPORTER_STATUS_BEFORE_LEASE_HOOK + case ExporterStatusLeaseReady: + return pb.ExporterStatus_EXPORTER_STATUS_LEASE_READY + case ExporterStatusAfterLeaseHook: + return pb.ExporterStatus_EXPORTER_STATUS_AFTER_LEASE_HOOK + case ExporterStatusBeforeLeaseHookFailed: + return pb.ExporterStatus_EXPORTER_STATUS_BEFORE_LEASE_HOOK_FAILED + case ExporterStatusAfterLeaseHookFailed: + return pb.ExporterStatus_EXPORTER_STATUS_AFTER_LEASE_HOOK_FAILED + default: + return pb.ExporterStatus_EXPORTER_STATUS_UNSPECIFIED } } diff --git a/api/v1alpha1/exporter_types.go b/api/v1alpha1/exporter_types.go index 9f17dd99..8e306f3f 100644 --- a/api/v1alpha1/exporter_types.go +++ b/api/v1alpha1/exporter_types.go @@ -38,6 +38,11 @@ type ExporterStatus struct { LeaseRef *corev1.LocalObjectReference `json:"leaseRef,omitempty"` LastSeen metav1.Time `json:"lastSeen,omitempty"` Endpoint string `json:"endpoint,omitempty"` + // ExporterStatusValue is the current operational status reported by the exporter + // +kubebuilder:validation:Enum=Unspecified;Offline;Available;BeforeLeaseHook;LeaseReady;AfterLeaseHook;BeforeLeaseHookFailed;AfterLeaseHookFailed + ExporterStatusValue string `json:"exporterStatus,omitempty"` + // StatusMessage is an optional human-readable message describing the current state + StatusMessage string `json:"statusMessage,omitempty"` } type ExporterConditionType string @@ -47,8 +52,22 @@ const ( ExporterConditionTypeOnline ExporterConditionType = "Online" ) +// ExporterStatus values - PascalCase for Kubernetes, converted from proto ALL_CAPS +const ( + ExporterStatusUnspecified = "Unspecified" + ExporterStatusOffline = "Offline" + ExporterStatusAvailable = "Available" + ExporterStatusBeforeLeaseHook = "BeforeLeaseHook" + ExporterStatusLeaseReady = "LeaseReady" + ExporterStatusAfterLeaseHook = "AfterLeaseHook" + ExporterStatusBeforeLeaseHookFailed = "BeforeLeaseHookFailed" + ExporterStatusAfterLeaseHookFailed = "AfterLeaseHookFailed" +) + // +kubebuilder:object:root=true // +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.exporterStatus" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.statusMessage",priority=1 // Exporter is the Schema for the exporters API type Exporter struct { diff --git a/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_clients.yaml b/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_clients.yaml new file mode 100644 index 00000000..d9dd6d0c --- /dev/null +++ b/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_clients.yaml @@ -0,0 +1,69 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.3 + name: clients.jumpstarter.dev +spec: + group: jumpstarter.dev + names: + kind: Client + listKind: ClientList + plural: clients + singular: client + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Client is the Schema for the identities API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + 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 + metadata: + type: object + spec: + description: ClientSpec defines the desired state of Identity + properties: + username: + type: string + type: object + status: + description: ClientStatus defines the observed state of Identity + properties: + credential: + description: Status field for the clients + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + endpoint: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_exporteraccesspolicies.yaml b/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_exporteraccesspolicies.yaml new file mode 100644 index 00000000..ec1b7878 --- /dev/null +++ b/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_exporteraccesspolicies.yaml @@ -0,0 +1,166 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.3 + name: exporteraccesspolicies.jumpstarter.dev +spec: + group: jumpstarter.dev + names: + kind: ExporterAccessPolicy + listKind: ExporterAccessPolicyList + plural: exporteraccesspolicies + singular: exporteraccesspolicy + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ExporterAccessPolicy is the Schema for the exporteraccesspolicies + API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + 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 + metadata: + type: object + spec: + description: ExporterAccessPolicySpec defines the desired state of ExporterAccessPolicy. + properties: + exporterSelector: + description: |- + A label selector is a label query over a set of resources. The result of matchLabels and + matchExpressions are ANDed. An empty label selector matches all objects. A null + label selector matches no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + policies: + items: + properties: + from: + items: + properties: + clientSelector: + description: |- + A label selector is a label query over a set of resources. The result of matchLabels and + matchExpressions are ANDed. An empty label selector matches all objects. A null + label selector matches no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + maximumDuration: + type: string + priority: + type: integer + spotAccess: + type: boolean + type: object + type: array + type: object + status: + description: ExporterAccessPolicyStatus defines the observed state of + ExporterAccessPolicy. + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_exporters.yaml b/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_exporters.yaml new file mode 100644 index 00000000..9e4d57bb --- /dev/null +++ b/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_exporters.yaml @@ -0,0 +1,185 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.3 + name: exporters.jumpstarter.dev +spec: + group: jumpstarter.dev + names: + kind: Exporter + listKind: ExporterList + plural: exporters + singular: exporter + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.exporterStatus + name: Status + type: string + - jsonPath: .status.statusMessage + name: Message + priority: 1 + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: Exporter is the Schema for the exporters API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + 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 + metadata: + type: object + spec: + description: ExporterSpec defines the desired state of Exporter + properties: + username: + type: string + type: object + status: + description: ExporterStatus defines the observed state of Exporter + properties: + conditions: + description: Exporter status fields + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + credential: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + devices: + items: + properties: + labels: + additionalProperties: + type: string + type: object + parent_uuid: + type: string + uuid: + type: string + type: object + type: array + endpoint: + type: string + exporterStatus: + description: ExporterStatusValue is the current operational status + reported by the exporter + enum: + - Unspecified + - Offline + - Available + - BeforeLeaseHook + - LeaseReady + - AfterLeaseHook + - BeforeLeaseHookFailed + - AfterLeaseHookFailed + type: string + lastSeen: + format: date-time + type: string + leaseRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + statusMessage: + description: StatusMessage is an optional human-readable message describing + the current state + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_leases.yaml b/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_leases.yaml new file mode 100644 index 00000000..9aafc859 --- /dev/null +++ b/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_leases.yaml @@ -0,0 +1,235 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.3 + name: leases.jumpstarter.dev +spec: + group: jumpstarter.dev + names: + kind: Lease + listKind: LeaseList + plural: leases + singular: lease + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.ended + name: Ended + type: boolean + - jsonPath: .spec.clientRef.name + name: Client + type: string + - jsonPath: .status.exporterRef.name + name: Exporter + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: Lease is the Schema for the exporters API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + 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 + metadata: + type: object + spec: + description: LeaseSpec defines the desired state of Lease + properties: + beginTime: + description: |- + Requested start time. If omitted, lease starts when exporter is acquired. + Immutable after lease starts (cannot change the past). + format: date-time + type: string + clientRef: + description: The client that is requesting the lease + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + duration: + description: |- + Duration of the lease. Must be positive when provided. + Can be omitted (nil) when both BeginTime and EndTime are provided, + in which case it's calculated as EndTime - BeginTime. + type: string + endTime: + description: |- + Requested end time. If specified with BeginTime, Duration is calculated. + Can be updated to extend or shorten active leases. + format: date-time + type: string + release: + description: The release flag requests the controller to end the lease + now + type: boolean + selector: + description: The selector for the exporter to be used + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - clientRef + - selector + type: object + status: + description: LeaseStatus defines the observed state of Lease + properties: + beginTime: + description: |- + If the lease has been acquired an exporter name is assigned + and then it can be used, it will be empty while still pending + format: date-time + type: string + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + endTime: + format: date-time + type: string + ended: + type: boolean + exporterRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + priority: + type: integer + spotAccess: + type: boolean + required: + - ended + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/helm/jumpstarter/crds/jumpstarter.dev_exporters.yaml b/deploy/helm/jumpstarter/crds/jumpstarter.dev_exporters.yaml index 931c28b0..9e4d57bb 100644 --- a/deploy/helm/jumpstarter/crds/jumpstarter.dev_exporters.yaml +++ b/deploy/helm/jumpstarter/crds/jumpstarter.dev_exporters.yaml @@ -14,7 +14,15 @@ spec: singular: exporter scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .status.exporterStatus + name: Status + type: string + - jsonPath: .status.statusMessage + name: Message + priority: 1 + type: string + name: v1alpha1 schema: openAPIV3Schema: description: Exporter is the Schema for the exporters API @@ -133,6 +141,19 @@ spec: type: array endpoint: type: string + exporterStatus: + description: ExporterStatusValue is the current operational status + reported by the exporter + enum: + - Unspecified + - Offline + - Available + - BeforeLeaseHook + - LeaseReady + - AfterLeaseHook + - BeforeLeaseHookFailed + - AfterLeaseHookFailed + type: string lastSeen: format: date-time type: string @@ -152,6 +173,10 @@ spec: type: string type: object x-kubernetes-map-type: atomic + statusMessage: + description: StatusMessage is an optional human-readable message describing + the current state + type: string type: object type: object served: true diff --git a/deploy/operator/config/crd/bases/jumpstarter.dev_exporters.yaml b/deploy/operator/config/crd/bases/jumpstarter.dev_exporters.yaml index 931c28b0..9e4d57bb 100644 --- a/deploy/operator/config/crd/bases/jumpstarter.dev_exporters.yaml +++ b/deploy/operator/config/crd/bases/jumpstarter.dev_exporters.yaml @@ -14,7 +14,15 @@ spec: singular: exporter scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .status.exporterStatus + name: Status + type: string + - jsonPath: .status.statusMessage + name: Message + priority: 1 + type: string + name: v1alpha1 schema: openAPIV3Schema: description: Exporter is the Schema for the exporters API @@ -133,6 +141,19 @@ spec: type: array endpoint: type: string + exporterStatus: + description: ExporterStatusValue is the current operational status + reported by the exporter + enum: + - Unspecified + - Offline + - Available + - BeforeLeaseHook + - LeaseReady + - AfterLeaseHook + - BeforeLeaseHookFailed + - AfterLeaseHookFailed + type: string lastSeen: format: date-time type: string @@ -152,6 +173,10 @@ spec: type: string type: object x-kubernetes-map-type: atomic + statusMessage: + description: StatusMessage is an optional human-readable message describing + the current state + type: string type: object type: object served: true diff --git a/hack/deploy_with_helm.sh b/hack/deploy_with_helm.sh index abdada4b..5121cd42 100755 --- a/hack/deploy_with_helm.sh +++ b/hack/deploy_with_helm.sh @@ -47,8 +47,10 @@ fi echo -e "${GREEN}Performing helm ${METHOD} ...${NC}" # install/update with helm +# --skip-crds: CRDs are managed via templates/crds/ instead of the special crds/ directory helm ${METHOD} --namespace jumpstarter-lab \ --create-namespace \ + --skip-crds \ ${HELM_SETS} \ --set global.timestamp=$(date +%s) \ --values ./deploy/helm/jumpstarter/values.kind.yaml jumpstarter \ diff --git a/internal/controller/exporter_controller.go b/internal/controller/exporter_controller.go index 0862f77c..5a0f7eff 100644 --- a/internal/controller/exporter_controller.go +++ b/internal/controller/exporter_controller.go @@ -167,6 +167,9 @@ func (r *ExporterReconciler) reconcileStatusConditionsOnline( Reason: "Seen", Message: "Never seen", }) + // Reset status to OFFLINE when never seen + exporter.Status.ExporterStatusValue = jumpstarterdevv1alpha1.ExporterStatusOffline + exporter.Status.StatusMessage = "Never seen" // marking the exporter offline, no need to requeue } else if time.Since(exporter.Status.LastSeen.Time) > time.Minute { meta.SetStatusCondition(&exporter.Status.Conditions, metav1.Condition{ @@ -176,17 +179,35 @@ func (r *ExporterReconciler) reconcileStatusConditionsOnline( Reason: "Seen", Message: "Last seen more than 1 minute ago", }) + // Reset status to OFFLINE when exporter hasn't been seen recently + if exporter.Status.ExporterStatusValue != jumpstarterdevv1alpha1.ExporterStatusOffline { + exporter.Status.ExporterStatusValue = jumpstarterdevv1alpha1.ExporterStatusOffline + exporter.Status.StatusMessage = "Connection lost - last seen more than 1 minute ago" + } // marking the exporter offline, no need to requeue } else { - meta.SetStatusCondition(&exporter.Status.Conditions, metav1.Condition{ - Type: string(jumpstarterdevv1alpha1.ExporterConditionTypeOnline), - Status: metav1.ConditionTrue, - ObservedGeneration: exporter.Generation, - Reason: "Seen", - Message: "Last seen less than 1 minute ago", - }) - // marking the exporter online, requeue after 30 seconds - requeueAfter = time.Second * 30 + // Check if exporter explicitly reported Offline status even though LastSeen is recent + // This happens when an exporter gracefully shuts down + if exporter.Status.ExporterStatusValue == jumpstarterdevv1alpha1.ExporterStatusOffline { + meta.SetStatusCondition(&exporter.Status.Conditions, metav1.Condition{ + Type: string(jumpstarterdevv1alpha1.ExporterConditionTypeOnline), + Status: metav1.ConditionFalse, + ObservedGeneration: exporter.Generation, + Reason: "Offline", + Message: exporter.Status.StatusMessage, + }) + // exporter reported offline, no need to requeue + } else { + meta.SetStatusCondition(&exporter.Status.Conditions, metav1.Condition{ + Type: string(jumpstarterdevv1alpha1.ExporterConditionTypeOnline), + Status: metav1.ConditionTrue, + ObservedGeneration: exporter.Generation, + Reason: "Seen", + Message: "Last seen less than 1 minute ago", + }) + // marking the exporter online, requeue after 30 seconds + requeueAfter = time.Second * 30 + } } if exporter.Status.Devices == nil { diff --git a/internal/controller/lease_controller_test.go b/internal/controller/lease_controller_test.go index e3dc97f6..822d15fe 100644 --- a/internal/controller/lease_controller_test.go +++ b/internal/controller/lease_controller_test.go @@ -484,9 +484,13 @@ func setExporterOnlineConditions(ctx context.Context, name string, status metav1 if status == metav1.ConditionTrue { exporter.Status.Devices = []jumpstarterdevv1alpha1.Device{{}} exporter.Status.LastSeen = metav1.Now() + exporter.Status.ExporterStatusValue = jumpstarterdevv1alpha1.ExporterStatusAvailable + exporter.Status.StatusMessage = "Available for leasing" } else { exporter.Status.Devices = nil exporter.Status.LastSeen = metav1.NewTime(metav1.Now().Add(-time.Minute * 2)) + exporter.Status.ExporterStatusValue = jumpstarterdevv1alpha1.ExporterStatusOffline + exporter.Status.StatusMessage = "Offline" } Expect(k8sClient.Status().Update(ctx, exporter)).To(Succeed()) } diff --git a/internal/protocol/jumpstarter/client/v1/client.pb.go b/internal/protocol/jumpstarter/client/v1/client.pb.go index 2364552e..cc23dcdf 100644 --- a/internal/protocol/jumpstarter/client/v1/client.pb.go +++ b/internal/protocol/jumpstarter/client/v1/client.pb.go @@ -41,6 +41,7 @@ type Exporter struct { // Deprecated: Marked as deprecated in jumpstarter/client/v1/client.proto. Online bool `protobuf:"varint,3,opt,name=online,proto3" json:"online,omitempty"` Status v1.ExporterStatus `protobuf:"varint,4,opt,name=status,proto3,enum=jumpstarter.v1.ExporterStatus" json:"status,omitempty"` + StatusMessage string `protobuf:"bytes,5,opt,name=status_message,json=statusMessage,proto3" json:"status_message,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -104,6 +105,13 @@ func (x *Exporter) GetStatus() v1.ExporterStatus { return v1.ExporterStatus(0) } +func (x *Exporter) GetStatusMessage() string { + if x != nil { + return x.StatusMessage + } + return "" +} + type Lease struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -724,12 +732,13 @@ var File_jumpstarter_client_v1_client_proto protoreflect.FileDescriptor const file_jumpstarter_client_v1_client_proto_rawDesc = "" + "\n" + - "\"jumpstarter/client/v1/client.proto\x12\x15jumpstarter.client.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1fjumpstarter/v1/kubernetes.proto\x1a\x1bjumpstarter/v1/common.proto\"\xe0\x02\n" + + "\"jumpstarter/client/v1/client.proto\x12\x15jumpstarter.client.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1fjumpstarter/v1/kubernetes.proto\x1a\x1bjumpstarter/v1/common.proto\"\x8c\x03\n" + "\bExporter\x12\x17\n" + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12C\n" + "\x06labels\x18\x02 \x03(\v2+.jumpstarter.client.v1.Exporter.LabelsEntryR\x06labels\x12\x1d\n" + "\x06online\x18\x03 \x01(\bB\x05\xe0A\x03\x18\x01R\x06online\x12;\n" + - "\x06status\x18\x04 \x01(\x0e2\x1e.jumpstarter.v1.ExporterStatusB\x03\xe0A\x03R\x06status\x1a9\n" + + "\x06status\x18\x04 \x01(\x0e2\x1e.jumpstarter.v1.ExporterStatusB\x03\xe0A\x03R\x06status\x12*\n" + + "\x0estatus_message\x18\x05 \x01(\tB\x03\xe0A\x03R\rstatusMessage\x1a9\n" + "\vLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01:_\xeaA\\\n" + diff --git a/internal/service/controller_service.go b/internal/service/controller_service.go index 97c06693..6ff06d76 100644 --- a/internal/service/controller_service.go +++ b/internal/service/controller_service.go @@ -52,6 +52,7 @@ import ( "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" @@ -211,6 +212,96 @@ func (s *ControllerService) Unregister( return &pb.UnregisterResponse{}, nil } +func (s *ControllerService) ReportStatus( + ctx context.Context, + req *pb.ReportStatusRequest, +) (*pb.ReportStatusResponse, error) { + logger := log.FromContext(ctx) + + exporter, err := s.authenticateExporter(ctx) + if err != nil { + logger.Info("unable to authenticate exporter", "error", err.Error()) + return nil, err + } + + logger = logger.WithValues("exporter", types.NamespacedName{ + Namespace: exporter.Namespace, + Name: exporter.Name, + }) + + // Convert proto enum to CRD string value + exporterStatus := protoStatusToString(req.Status) + + logger.Info("Exporter reporting status", "state", exporterStatus, "message", req.GetMessage()) + + original := client.MergeFrom(exporter.DeepCopy()) + + exporter.Status.ExporterStatusValue = exporterStatus + exporter.Status.StatusMessage = req.GetMessage() + // Also update LastSeen to keep the exporter marked as online + exporter.Status.LastSeen = metav1.Now() + + // Sync the Online condition with the reported status for consistency + // This ensures the deprecated Online boolean field stays consistent with ExporterStatusValue + syncOnlineConditionWithStatus(exporter) + + if err := s.Client.Status().Patch(ctx, exporter, original); err != nil { + logger.Error(err, "unable to update exporter status") + return nil, status.Errorf(codes.Internal, "unable to update exporter status: %s", err) + } + + return &pb.ReportStatusResponse{}, nil +} + +// protoStatusToString converts the proto ExporterStatus enum to the CRD string value +func protoStatusToString(status pb.ExporterStatus) string { + switch status { + case pb.ExporterStatus_EXPORTER_STATUS_OFFLINE: + return jumpstarterdevv1alpha1.ExporterStatusOffline + case pb.ExporterStatus_EXPORTER_STATUS_AVAILABLE: + return jumpstarterdevv1alpha1.ExporterStatusAvailable + case pb.ExporterStatus_EXPORTER_STATUS_BEFORE_LEASE_HOOK: + return jumpstarterdevv1alpha1.ExporterStatusBeforeLeaseHook + case pb.ExporterStatus_EXPORTER_STATUS_LEASE_READY: + return jumpstarterdevv1alpha1.ExporterStatusLeaseReady + case pb.ExporterStatus_EXPORTER_STATUS_AFTER_LEASE_HOOK: + return jumpstarterdevv1alpha1.ExporterStatusAfterLeaseHook + case pb.ExporterStatus_EXPORTER_STATUS_BEFORE_LEASE_HOOK_FAILED: + return jumpstarterdevv1alpha1.ExporterStatusBeforeLeaseHookFailed + case pb.ExporterStatus_EXPORTER_STATUS_AFTER_LEASE_HOOK_FAILED: + return jumpstarterdevv1alpha1.ExporterStatusAfterLeaseHookFailed + default: + return jumpstarterdevv1alpha1.ExporterStatusUnspecified + } +} + +// syncOnlineConditionWithStatus updates the Online condition based on ExporterStatusValue. +// This ensures the deprecated Online boolean field in the protobuf API stays consistent +// with the new ExporterStatusValue field. +func syncOnlineConditionWithStatus(exporter *jumpstarterdevv1alpha1.Exporter) { + isOnline := exporter.Status.ExporterStatusValue != jumpstarterdevv1alpha1.ExporterStatusOffline && + exporter.Status.ExporterStatusValue != jumpstarterdevv1alpha1.ExporterStatusUnspecified && + exporter.Status.ExporterStatusValue != "" + + if isOnline { + meta.SetStatusCondition(&exporter.Status.Conditions, metav1.Condition{ + Type: string(jumpstarterdevv1alpha1.ExporterConditionTypeOnline), + Status: metav1.ConditionTrue, + ObservedGeneration: exporter.Generation, + Reason: "StatusReported", + Message: fmt.Sprintf("Exporter reported status: %s", exporter.Status.ExporterStatusValue), + }) + } else { + meta.SetStatusCondition(&exporter.Status.Conditions, metav1.Condition{ + Type: string(jumpstarterdevv1alpha1.ExporterConditionTypeOnline), + Status: metav1.ConditionFalse, + ObservedGeneration: exporter.Generation, + Reason: "Offline", + Message: exporter.Status.StatusMessage, + }) + } +} + func (s *ControllerService) Listen(req *pb.ListenRequest, stream pb.ControllerService_ListenServer) error { ctx := stream.Context() logger := log.FromContext(ctx)