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..19c21ddb 100644 --- a/internal/protocol/jumpstarter/client/v1/client.pb.go +++ b/internal/protocol/jumpstarter/client/v1/client.pb.go @@ -7,7 +7,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: jumpstarter/client/v1/client.proto @@ -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/protocol/jumpstarter/client/v1/client_grpc.pb.go b/internal/protocol/jumpstarter/client/v1/client_grpc.pb.go index e3a4a5a9..6055721c 100644 --- a/internal/protocol/jumpstarter/client/v1/client_grpc.pb.go +++ b/internal/protocol/jumpstarter/client/v1/client_grpc.pb.go @@ -7,7 +7,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc (unknown) // source: jumpstarter/client/v1/client.proto @@ -149,25 +149,25 @@ type ClientServiceServer interface { type UnimplementedClientServiceServer struct{} func (UnimplementedClientServiceServer) GetExporter(context.Context, *GetExporterRequest) (*Exporter, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetExporter not implemented") + return nil, status.Error(codes.Unimplemented, "method GetExporter not implemented") } func (UnimplementedClientServiceServer) ListExporters(context.Context, *ListExportersRequest) (*ListExportersResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListExporters not implemented") + return nil, status.Error(codes.Unimplemented, "method ListExporters not implemented") } func (UnimplementedClientServiceServer) GetLease(context.Context, *GetLeaseRequest) (*Lease, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetLease not implemented") + return nil, status.Error(codes.Unimplemented, "method GetLease not implemented") } func (UnimplementedClientServiceServer) ListLeases(context.Context, *ListLeasesRequest) (*ListLeasesResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListLeases not implemented") + return nil, status.Error(codes.Unimplemented, "method ListLeases not implemented") } func (UnimplementedClientServiceServer) CreateLease(context.Context, *CreateLeaseRequest) (*Lease, error) { - return nil, status.Errorf(codes.Unimplemented, "method CreateLease not implemented") + return nil, status.Error(codes.Unimplemented, "method CreateLease not implemented") } func (UnimplementedClientServiceServer) UpdateLease(context.Context, *UpdateLeaseRequest) (*Lease, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateLease not implemented") + return nil, status.Error(codes.Unimplemented, "method UpdateLease not implemented") } func (UnimplementedClientServiceServer) DeleteLease(context.Context, *DeleteLeaseRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeleteLease not implemented") + return nil, status.Error(codes.Unimplemented, "method DeleteLease not implemented") } func (UnimplementedClientServiceServer) mustEmbedUnimplementedClientServiceServer() {} func (UnimplementedClientServiceServer) testEmbeddedByValue() {} @@ -180,7 +180,7 @@ type UnsafeClientServiceServer interface { } func RegisterClientServiceServer(s grpc.ServiceRegistrar, srv ClientServiceServer) { - // If the following call pancis, it indicates UnimplementedClientServiceServer was + // If the following call panics, it indicates UnimplementedClientServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/internal/protocol/jumpstarter/v1/common.pb.go b/internal/protocol/jumpstarter/v1/common.pb.go index 1ff75334..030ecf76 100644 --- a/internal/protocol/jumpstarter/v1/common.pb.go +++ b/internal/protocol/jumpstarter/v1/common.pb.go @@ -2,7 +2,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: jumpstarter/v1/common.proto diff --git a/internal/protocol/jumpstarter/v1/jumpstarter.pb.go b/internal/protocol/jumpstarter/v1/jumpstarter.pb.go index 0228f1de..5da8062b 100644 --- a/internal/protocol/jumpstarter/v1/jumpstarter.pb.go +++ b/internal/protocol/jumpstarter/v1/jumpstarter.pb.go @@ -2,7 +2,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: jumpstarter/v1/jumpstarter.proto @@ -1705,6 +1705,94 @@ func (x *GetStatusResponse) GetMessage() string { return "" } +type EndSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EndSessionRequest) Reset() { + *x = EndSessionRequest{} + mi := &file_jumpstarter_v1_jumpstarter_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EndSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EndSessionRequest) ProtoMessage() {} + +func (x *EndSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_jumpstarter_v1_jumpstarter_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EndSessionRequest.ProtoReflect.Descriptor instead. +func (*EndSessionRequest) Descriptor() ([]byte, []int) { + return file_jumpstarter_v1_jumpstarter_proto_rawDescGZIP(), []int{33} +} + +type EndSessionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + Message *string `protobuf:"bytes,2,opt,name=message,proto3,oneof" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EndSessionResponse) Reset() { + *x = EndSessionResponse{} + mi := &file_jumpstarter_v1_jumpstarter_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EndSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EndSessionResponse) ProtoMessage() {} + +func (x *EndSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_jumpstarter_v1_jumpstarter_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EndSessionResponse.ProtoReflect.Descriptor instead. +func (*EndSessionResponse) Descriptor() ([]byte, []int) { + return file_jumpstarter_v1_jumpstarter_proto_rawDescGZIP(), []int{34} +} + +func (x *EndSessionResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *EndSessionResponse) GetMessage() string { + if x != nil && x.Message != nil { + return *x.Message + } + return "" +} + var File_jumpstarter_v1_jumpstarter_proto protoreflect.FileDescriptor const file_jumpstarter_v1_jumpstarter_proto_rawDesc = "" + @@ -1834,6 +1922,12 @@ const file_jumpstarter_v1_jumpstarter_proto_rawDesc = "" + "\x06status\x18\x01 \x01(\x0e2\x1e.jumpstarter.v1.ExporterStatusR\x06status\x12\x1d\n" + "\amessage\x18\x02 \x01(\tH\x00R\amessage\x88\x01\x01B\n" + "\n" + + "\b_message\"\x13\n" + + "\x11EndSessionRequest\"Y\n" + + "\x12EndSessionResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1d\n" + + "\amessage\x18\x02 \x01(\tH\x00R\amessage\x88\x01\x01B\n" + + "\n" + "\b_message2\x92\a\n" + "\x11ControllerService\x12M\n" + "\bRegister\x12\x1f.jumpstarter.v1.RegisterRequest\x1a .jumpstarter.v1.RegisterResponse\x12S\n" + @@ -1848,7 +1942,7 @@ const file_jumpstarter_v1_jumpstarter_proto_rawDesc = "" + "\fRequestLease\x12#.jumpstarter.v1.RequestLeaseRequest\x1a$.jumpstarter.v1.RequestLeaseResponse\x12Y\n" + "\fReleaseLease\x12#.jumpstarter.v1.ReleaseLeaseRequest\x1a$.jumpstarter.v1.ReleaseLeaseResponse\x12S\n" + "\n" + - "ListLeases\x12!.jumpstarter.v1.ListLeasesRequest\x1a\".jumpstarter.v1.ListLeasesResponse2\x82\x04\n" + + "ListLeases\x12!.jumpstarter.v1.ListLeasesRequest\x1a\".jumpstarter.v1.ListLeasesResponse2\xd7\x04\n" + "\x0fExporterService\x12F\n" + "\tGetReport\x12\x16.google.protobuf.Empty\x1a!.jumpstarter.v1.GetReportResponse\x12S\n" + "\n" + @@ -1856,7 +1950,9 @@ const file_jumpstarter_v1_jumpstarter_proto_rawDesc = "" + "\x13StreamingDriverCall\x12*.jumpstarter.v1.StreamingDriverCallRequest\x1a+.jumpstarter.v1.StreamingDriverCallResponse0\x01\x12H\n" + "\tLogStream\x12\x16.google.protobuf.Empty\x1a!.jumpstarter.v1.LogStreamResponse0\x01\x12D\n" + "\x05Reset\x12\x1c.jumpstarter.v1.ResetRequest\x1a\x1d.jumpstarter.v1.ResetResponse\x12P\n" + - "\tGetStatus\x12 .jumpstarter.v1.GetStatusRequest\x1a!.jumpstarter.v1.GetStatusResponseB\xe1\x01\n" + + "\tGetStatus\x12 .jumpstarter.v1.GetStatusRequest\x1a!.jumpstarter.v1.GetStatusResponse\x12S\n" + + "\n" + + "EndSession\x12!.jumpstarter.v1.EndSessionRequest\x1a\".jumpstarter.v1.EndSessionResponseB\xe1\x01\n" + "\x12com.jumpstarter.v1B\x10JumpstarterProtoP\x01Z`github.com/jumpstarter-dev/jumpstarter-controller/internal/protocol/jumpstarter/v1;jumpstarterv1\xa2\x02\x03JXX\xaa\x02\x0eJumpstarter.V1\xca\x02\x0eJumpstarter\\V1\xe2\x02\x1aJumpstarter\\V1\\GPBMetadata\xea\x02\x0fJumpstarter::V1b\x06proto3" var ( @@ -1871,7 +1967,7 @@ func file_jumpstarter_v1_jumpstarter_proto_rawDescGZIP() []byte { return file_jumpstarter_v1_jumpstarter_proto_rawDescData } -var file_jumpstarter_v1_jumpstarter_proto_msgTypes = make([]protoimpl.MessageInfo, 37) +var file_jumpstarter_v1_jumpstarter_proto_msgTypes = make([]protoimpl.MessageInfo, 39) var file_jumpstarter_v1_jumpstarter_proto_goTypes = []any{ (*RegisterRequest)(nil), // 0: jumpstarter.v1.RegisterRequest (*DriverInstanceReport)(nil), // 1: jumpstarter.v1.DriverInstanceReport @@ -1906,41 +2002,43 @@ var file_jumpstarter_v1_jumpstarter_proto_goTypes = []any{ (*ListLeasesResponse)(nil), // 30: jumpstarter.v1.ListLeasesResponse (*GetStatusRequest)(nil), // 31: jumpstarter.v1.GetStatusRequest (*GetStatusResponse)(nil), // 32: jumpstarter.v1.GetStatusResponse - nil, // 33: jumpstarter.v1.RegisterRequest.LabelsEntry - nil, // 34: jumpstarter.v1.DriverInstanceReport.LabelsEntry - nil, // 35: jumpstarter.v1.DriverInstanceReport.MethodsDescriptionEntry - nil, // 36: jumpstarter.v1.GetReportResponse.LabelsEntry - (ExporterStatus)(0), // 37: jumpstarter.v1.ExporterStatus - (*structpb.Value)(nil), // 38: google.protobuf.Value - (LogSource)(0), // 39: jumpstarter.v1.LogSource - (*durationpb.Duration)(nil), // 40: google.protobuf.Duration - (*LabelSelector)(nil), // 41: jumpstarter.v1.LabelSelector - (*timestamppb.Timestamp)(nil), // 42: google.protobuf.Timestamp - (*Condition)(nil), // 43: jumpstarter.v1.Condition - (*emptypb.Empty)(nil), // 44: google.protobuf.Empty + (*EndSessionRequest)(nil), // 33: jumpstarter.v1.EndSessionRequest + (*EndSessionResponse)(nil), // 34: jumpstarter.v1.EndSessionResponse + nil, // 35: jumpstarter.v1.RegisterRequest.LabelsEntry + nil, // 36: jumpstarter.v1.DriverInstanceReport.LabelsEntry + nil, // 37: jumpstarter.v1.DriverInstanceReport.MethodsDescriptionEntry + nil, // 38: jumpstarter.v1.GetReportResponse.LabelsEntry + (ExporterStatus)(0), // 39: jumpstarter.v1.ExporterStatus + (*structpb.Value)(nil), // 40: google.protobuf.Value + (LogSource)(0), // 41: jumpstarter.v1.LogSource + (*durationpb.Duration)(nil), // 42: google.protobuf.Duration + (*LabelSelector)(nil), // 43: jumpstarter.v1.LabelSelector + (*timestamppb.Timestamp)(nil), // 44: google.protobuf.Timestamp + (*Condition)(nil), // 45: jumpstarter.v1.Condition + (*emptypb.Empty)(nil), // 46: google.protobuf.Empty } var file_jumpstarter_v1_jumpstarter_proto_depIdxs = []int32{ - 33, // 0: jumpstarter.v1.RegisterRequest.labels:type_name -> jumpstarter.v1.RegisterRequest.LabelsEntry + 35, // 0: jumpstarter.v1.RegisterRequest.labels:type_name -> jumpstarter.v1.RegisterRequest.LabelsEntry 1, // 1: jumpstarter.v1.RegisterRequest.reports:type_name -> jumpstarter.v1.DriverInstanceReport - 34, // 2: jumpstarter.v1.DriverInstanceReport.labels:type_name -> jumpstarter.v1.DriverInstanceReport.LabelsEntry - 35, // 3: jumpstarter.v1.DriverInstanceReport.methods_description:type_name -> jumpstarter.v1.DriverInstanceReport.MethodsDescriptionEntry - 37, // 4: jumpstarter.v1.ReportStatusRequest.status:type_name -> jumpstarter.v1.ExporterStatus - 36, // 5: jumpstarter.v1.GetReportResponse.labels:type_name -> jumpstarter.v1.GetReportResponse.LabelsEntry + 36, // 2: jumpstarter.v1.DriverInstanceReport.labels:type_name -> jumpstarter.v1.DriverInstanceReport.LabelsEntry + 37, // 3: jumpstarter.v1.DriverInstanceReport.methods_description:type_name -> jumpstarter.v1.DriverInstanceReport.MethodsDescriptionEntry + 39, // 4: jumpstarter.v1.ReportStatusRequest.status:type_name -> jumpstarter.v1.ExporterStatus + 38, // 5: jumpstarter.v1.GetReportResponse.labels:type_name -> jumpstarter.v1.GetReportResponse.LabelsEntry 1, // 6: jumpstarter.v1.GetReportResponse.reports:type_name -> jumpstarter.v1.DriverInstanceReport 15, // 7: jumpstarter.v1.GetReportResponse.alternative_endpoints:type_name -> jumpstarter.v1.Endpoint - 38, // 8: jumpstarter.v1.DriverCallRequest.args:type_name -> google.protobuf.Value - 38, // 9: jumpstarter.v1.DriverCallResponse.result:type_name -> google.protobuf.Value - 38, // 10: jumpstarter.v1.StreamingDriverCallRequest.args:type_name -> google.protobuf.Value - 38, // 11: jumpstarter.v1.StreamingDriverCallResponse.result:type_name -> google.protobuf.Value - 39, // 12: jumpstarter.v1.LogStreamResponse.source:type_name -> jumpstarter.v1.LogSource - 40, // 13: jumpstarter.v1.GetLeaseResponse.duration:type_name -> google.protobuf.Duration - 41, // 14: jumpstarter.v1.GetLeaseResponse.selector:type_name -> jumpstarter.v1.LabelSelector - 42, // 15: jumpstarter.v1.GetLeaseResponse.begin_time:type_name -> google.protobuf.Timestamp - 42, // 16: jumpstarter.v1.GetLeaseResponse.end_time:type_name -> google.protobuf.Timestamp - 43, // 17: jumpstarter.v1.GetLeaseResponse.conditions:type_name -> jumpstarter.v1.Condition - 40, // 18: jumpstarter.v1.RequestLeaseRequest.duration:type_name -> google.protobuf.Duration - 41, // 19: jumpstarter.v1.RequestLeaseRequest.selector:type_name -> jumpstarter.v1.LabelSelector - 37, // 20: jumpstarter.v1.GetStatusResponse.status:type_name -> jumpstarter.v1.ExporterStatus + 40, // 8: jumpstarter.v1.DriverCallRequest.args:type_name -> google.protobuf.Value + 40, // 9: jumpstarter.v1.DriverCallResponse.result:type_name -> google.protobuf.Value + 40, // 10: jumpstarter.v1.StreamingDriverCallRequest.args:type_name -> google.protobuf.Value + 40, // 11: jumpstarter.v1.StreamingDriverCallResponse.result:type_name -> google.protobuf.Value + 41, // 12: jumpstarter.v1.LogStreamResponse.source:type_name -> jumpstarter.v1.LogSource + 42, // 13: jumpstarter.v1.GetLeaseResponse.duration:type_name -> google.protobuf.Duration + 43, // 14: jumpstarter.v1.GetLeaseResponse.selector:type_name -> jumpstarter.v1.LabelSelector + 44, // 15: jumpstarter.v1.GetLeaseResponse.begin_time:type_name -> google.protobuf.Timestamp + 44, // 16: jumpstarter.v1.GetLeaseResponse.end_time:type_name -> google.protobuf.Timestamp + 45, // 17: jumpstarter.v1.GetLeaseResponse.conditions:type_name -> jumpstarter.v1.Condition + 42, // 18: jumpstarter.v1.RequestLeaseRequest.duration:type_name -> google.protobuf.Duration + 43, // 19: jumpstarter.v1.RequestLeaseRequest.selector:type_name -> jumpstarter.v1.LabelSelector + 39, // 20: jumpstarter.v1.GetStatusResponse.status:type_name -> jumpstarter.v1.ExporterStatus 0, // 21: jumpstarter.v1.ControllerService.Register:input_type -> jumpstarter.v1.RegisterRequest 3, // 22: jumpstarter.v1.ControllerService.Unregister:input_type -> jumpstarter.v1.UnregisterRequest 12, // 23: jumpstarter.v1.ControllerService.ReportStatus:input_type -> jumpstarter.v1.ReportStatusRequest @@ -1952,31 +2050,33 @@ var file_jumpstarter_v1_jumpstarter_proto_depIdxs = []int32{ 25, // 29: jumpstarter.v1.ControllerService.RequestLease:input_type -> jumpstarter.v1.RequestLeaseRequest 27, // 30: jumpstarter.v1.ControllerService.ReleaseLease:input_type -> jumpstarter.v1.ReleaseLeaseRequest 29, // 31: jumpstarter.v1.ControllerService.ListLeases:input_type -> jumpstarter.v1.ListLeasesRequest - 44, // 32: jumpstarter.v1.ExporterService.GetReport:input_type -> google.protobuf.Empty + 46, // 32: jumpstarter.v1.ExporterService.GetReport:input_type -> google.protobuf.Empty 16, // 33: jumpstarter.v1.ExporterService.DriverCall:input_type -> jumpstarter.v1.DriverCallRequest 18, // 34: jumpstarter.v1.ExporterService.StreamingDriverCall:input_type -> jumpstarter.v1.StreamingDriverCallRequest - 44, // 35: jumpstarter.v1.ExporterService.LogStream:input_type -> google.protobuf.Empty + 46, // 35: jumpstarter.v1.ExporterService.LogStream:input_type -> google.protobuf.Empty 21, // 36: jumpstarter.v1.ExporterService.Reset:input_type -> jumpstarter.v1.ResetRequest 31, // 37: jumpstarter.v1.ExporterService.GetStatus:input_type -> jumpstarter.v1.GetStatusRequest - 2, // 38: jumpstarter.v1.ControllerService.Register:output_type -> jumpstarter.v1.RegisterResponse - 4, // 39: jumpstarter.v1.ControllerService.Unregister:output_type -> jumpstarter.v1.UnregisterResponse - 13, // 40: jumpstarter.v1.ControllerService.ReportStatus:output_type -> jumpstarter.v1.ReportStatusResponse - 6, // 41: jumpstarter.v1.ControllerService.Listen:output_type -> jumpstarter.v1.ListenResponse - 8, // 42: jumpstarter.v1.ControllerService.Status:output_type -> jumpstarter.v1.StatusResponse - 10, // 43: jumpstarter.v1.ControllerService.Dial:output_type -> jumpstarter.v1.DialResponse - 44, // 44: jumpstarter.v1.ControllerService.AuditStream:output_type -> google.protobuf.Empty - 24, // 45: jumpstarter.v1.ControllerService.GetLease:output_type -> jumpstarter.v1.GetLeaseResponse - 26, // 46: jumpstarter.v1.ControllerService.RequestLease:output_type -> jumpstarter.v1.RequestLeaseResponse - 28, // 47: jumpstarter.v1.ControllerService.ReleaseLease:output_type -> jumpstarter.v1.ReleaseLeaseResponse - 30, // 48: jumpstarter.v1.ControllerService.ListLeases:output_type -> jumpstarter.v1.ListLeasesResponse - 14, // 49: jumpstarter.v1.ExporterService.GetReport:output_type -> jumpstarter.v1.GetReportResponse - 17, // 50: jumpstarter.v1.ExporterService.DriverCall:output_type -> jumpstarter.v1.DriverCallResponse - 19, // 51: jumpstarter.v1.ExporterService.StreamingDriverCall:output_type -> jumpstarter.v1.StreamingDriverCallResponse - 20, // 52: jumpstarter.v1.ExporterService.LogStream:output_type -> jumpstarter.v1.LogStreamResponse - 22, // 53: jumpstarter.v1.ExporterService.Reset:output_type -> jumpstarter.v1.ResetResponse - 32, // 54: jumpstarter.v1.ExporterService.GetStatus:output_type -> jumpstarter.v1.GetStatusResponse - 38, // [38:55] is the sub-list for method output_type - 21, // [21:38] is the sub-list for method input_type + 33, // 38: jumpstarter.v1.ExporterService.EndSession:input_type -> jumpstarter.v1.EndSessionRequest + 2, // 39: jumpstarter.v1.ControllerService.Register:output_type -> jumpstarter.v1.RegisterResponse + 4, // 40: jumpstarter.v1.ControllerService.Unregister:output_type -> jumpstarter.v1.UnregisterResponse + 13, // 41: jumpstarter.v1.ControllerService.ReportStatus:output_type -> jumpstarter.v1.ReportStatusResponse + 6, // 42: jumpstarter.v1.ControllerService.Listen:output_type -> jumpstarter.v1.ListenResponse + 8, // 43: jumpstarter.v1.ControllerService.Status:output_type -> jumpstarter.v1.StatusResponse + 10, // 44: jumpstarter.v1.ControllerService.Dial:output_type -> jumpstarter.v1.DialResponse + 46, // 45: jumpstarter.v1.ControllerService.AuditStream:output_type -> google.protobuf.Empty + 24, // 46: jumpstarter.v1.ControllerService.GetLease:output_type -> jumpstarter.v1.GetLeaseResponse + 26, // 47: jumpstarter.v1.ControllerService.RequestLease:output_type -> jumpstarter.v1.RequestLeaseResponse + 28, // 48: jumpstarter.v1.ControllerService.ReleaseLease:output_type -> jumpstarter.v1.ReleaseLeaseResponse + 30, // 49: jumpstarter.v1.ControllerService.ListLeases:output_type -> jumpstarter.v1.ListLeasesResponse + 14, // 50: jumpstarter.v1.ExporterService.GetReport:output_type -> jumpstarter.v1.GetReportResponse + 17, // 51: jumpstarter.v1.ExporterService.DriverCall:output_type -> jumpstarter.v1.DriverCallResponse + 19, // 52: jumpstarter.v1.ExporterService.StreamingDriverCall:output_type -> jumpstarter.v1.StreamingDriverCallResponse + 20, // 53: jumpstarter.v1.ExporterService.LogStream:output_type -> jumpstarter.v1.LogStreamResponse + 22, // 54: jumpstarter.v1.ExporterService.Reset:output_type -> jumpstarter.v1.ResetResponse + 32, // 55: jumpstarter.v1.ExporterService.GetStatus:output_type -> jumpstarter.v1.GetStatusResponse + 34, // 56: jumpstarter.v1.ExporterService.EndSession:output_type -> jumpstarter.v1.EndSessionResponse + 39, // [39:57] is the sub-list for method output_type + 21, // [21:39] is the sub-list for method input_type 21, // [21:21] is the sub-list for extension type_name 21, // [21:21] is the sub-list for extension extendee 0, // [0:21] is the sub-list for field type_name @@ -1995,13 +2095,14 @@ func file_jumpstarter_v1_jumpstarter_proto_init() { file_jumpstarter_v1_jumpstarter_proto_msgTypes[20].OneofWrappers = []any{} file_jumpstarter_v1_jumpstarter_proto_msgTypes[24].OneofWrappers = []any{} file_jumpstarter_v1_jumpstarter_proto_msgTypes[32].OneofWrappers = []any{} + file_jumpstarter_v1_jumpstarter_proto_msgTypes[34].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_jumpstarter_v1_jumpstarter_proto_rawDesc), len(file_jumpstarter_v1_jumpstarter_proto_rawDesc)), NumEnums: 0, - NumMessages: 37, + NumMessages: 39, NumExtensions: 0, NumServices: 2, }, diff --git a/internal/protocol/jumpstarter/v1/jumpstarter_grpc.pb.go b/internal/protocol/jumpstarter/v1/jumpstarter_grpc.pb.go index d0fbd1bd..adf91969 100644 --- a/internal/protocol/jumpstarter/v1/jumpstarter_grpc.pb.go +++ b/internal/protocol/jumpstarter/v1/jumpstarter_grpc.pb.go @@ -2,7 +2,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc (unknown) // source: jumpstarter/v1/jumpstarter.proto @@ -261,37 +261,37 @@ type ControllerServiceServer interface { type UnimplementedControllerServiceServer struct{} func (UnimplementedControllerServiceServer) Register(context.Context, *RegisterRequest) (*RegisterResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Register not implemented") + return nil, status.Error(codes.Unimplemented, "method Register not implemented") } func (UnimplementedControllerServiceServer) Unregister(context.Context, *UnregisterRequest) (*UnregisterResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Unregister not implemented") + return nil, status.Error(codes.Unimplemented, "method Unregister not implemented") } func (UnimplementedControllerServiceServer) ReportStatus(context.Context, *ReportStatusRequest) (*ReportStatusResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ReportStatus not implemented") + return nil, status.Error(codes.Unimplemented, "method ReportStatus not implemented") } func (UnimplementedControllerServiceServer) Listen(*ListenRequest, grpc.ServerStreamingServer[ListenResponse]) error { - return status.Errorf(codes.Unimplemented, "method Listen not implemented") + return status.Error(codes.Unimplemented, "method Listen not implemented") } func (UnimplementedControllerServiceServer) Status(*StatusRequest, grpc.ServerStreamingServer[StatusResponse]) error { - return status.Errorf(codes.Unimplemented, "method Status not implemented") + return status.Error(codes.Unimplemented, "method Status not implemented") } func (UnimplementedControllerServiceServer) Dial(context.Context, *DialRequest) (*DialResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Dial not implemented") + return nil, status.Error(codes.Unimplemented, "method Dial not implemented") } func (UnimplementedControllerServiceServer) AuditStream(grpc.ClientStreamingServer[AuditStreamRequest, emptypb.Empty]) error { - return status.Errorf(codes.Unimplemented, "method AuditStream not implemented") + return status.Error(codes.Unimplemented, "method AuditStream not implemented") } func (UnimplementedControllerServiceServer) GetLease(context.Context, *GetLeaseRequest) (*GetLeaseResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetLease not implemented") + return nil, status.Error(codes.Unimplemented, "method GetLease not implemented") } func (UnimplementedControllerServiceServer) RequestLease(context.Context, *RequestLeaseRequest) (*RequestLeaseResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method RequestLease not implemented") + return nil, status.Error(codes.Unimplemented, "method RequestLease not implemented") } func (UnimplementedControllerServiceServer) ReleaseLease(context.Context, *ReleaseLeaseRequest) (*ReleaseLeaseResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ReleaseLease not implemented") + return nil, status.Error(codes.Unimplemented, "method ReleaseLease not implemented") } func (UnimplementedControllerServiceServer) ListLeases(context.Context, *ListLeasesRequest) (*ListLeasesResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListLeases not implemented") + return nil, status.Error(codes.Unimplemented, "method ListLeases not implemented") } func (UnimplementedControllerServiceServer) mustEmbedUnimplementedControllerServiceServer() {} func (UnimplementedControllerServiceServer) testEmbeddedByValue() {} @@ -304,7 +304,7 @@ type UnsafeControllerServiceServer interface { } func RegisterControllerServiceServer(s grpc.ServiceRegistrar, srv ControllerServiceServer) { - // If the following call pancis, it indicates UnimplementedControllerServiceServer was + // If the following call panics, it indicates UnimplementedControllerServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. @@ -554,6 +554,7 @@ const ( ExporterService_LogStream_FullMethodName = "/jumpstarter.v1.ExporterService/LogStream" ExporterService_Reset_FullMethodName = "/jumpstarter.v1.ExporterService/Reset" ExporterService_GetStatus_FullMethodName = "/jumpstarter.v1.ExporterService/GetStatus" + ExporterService_EndSession_FullMethodName = "/jumpstarter.v1.ExporterService/EndSession" ) // ExporterServiceClient is the client API for ExporterService service. @@ -570,6 +571,10 @@ type ExporterServiceClient interface { LogStream(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogStreamResponse], error) Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*ResetResponse, error) GetStatus(ctx context.Context, in *GetStatusRequest, opts ...grpc.CallOption) (*GetStatusResponse, error) + // End the current session, triggering the afterLease hook + // The client should keep the connection open to receive hook logs via LogStream + // Returns after the afterLease hook completes + EndSession(ctx context.Context, in *EndSessionRequest, opts ...grpc.CallOption) (*EndSessionResponse, error) } type exporterServiceClient struct { @@ -658,6 +663,16 @@ func (c *exporterServiceClient) GetStatus(ctx context.Context, in *GetStatusRequ return out, nil } +func (c *exporterServiceClient) EndSession(ctx context.Context, in *EndSessionRequest, opts ...grpc.CallOption) (*EndSessionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EndSessionResponse) + err := c.cc.Invoke(ctx, ExporterService_EndSession_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // ExporterServiceServer is the server API for ExporterService service. // All implementations must embed UnimplementedExporterServiceServer // for forward compatibility. @@ -672,6 +687,10 @@ type ExporterServiceServer interface { LogStream(*emptypb.Empty, grpc.ServerStreamingServer[LogStreamResponse]) error Reset(context.Context, *ResetRequest) (*ResetResponse, error) GetStatus(context.Context, *GetStatusRequest) (*GetStatusResponse, error) + // End the current session, triggering the afterLease hook + // The client should keep the connection open to receive hook logs via LogStream + // Returns after the afterLease hook completes + EndSession(context.Context, *EndSessionRequest) (*EndSessionResponse, error) mustEmbedUnimplementedExporterServiceServer() } @@ -683,22 +702,25 @@ type ExporterServiceServer interface { type UnimplementedExporterServiceServer struct{} func (UnimplementedExporterServiceServer) GetReport(context.Context, *emptypb.Empty) (*GetReportResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetReport not implemented") + return nil, status.Error(codes.Unimplemented, "method GetReport not implemented") } func (UnimplementedExporterServiceServer) DriverCall(context.Context, *DriverCallRequest) (*DriverCallResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method DriverCall not implemented") + return nil, status.Error(codes.Unimplemented, "method DriverCall not implemented") } func (UnimplementedExporterServiceServer) StreamingDriverCall(*StreamingDriverCallRequest, grpc.ServerStreamingServer[StreamingDriverCallResponse]) error { - return status.Errorf(codes.Unimplemented, "method StreamingDriverCall not implemented") + return status.Error(codes.Unimplemented, "method StreamingDriverCall not implemented") } func (UnimplementedExporterServiceServer) LogStream(*emptypb.Empty, grpc.ServerStreamingServer[LogStreamResponse]) error { - return status.Errorf(codes.Unimplemented, "method LogStream not implemented") + return status.Error(codes.Unimplemented, "method LogStream not implemented") } func (UnimplementedExporterServiceServer) Reset(context.Context, *ResetRequest) (*ResetResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Reset not implemented") + return nil, status.Error(codes.Unimplemented, "method Reset not implemented") } func (UnimplementedExporterServiceServer) GetStatus(context.Context, *GetStatusRequest) (*GetStatusResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetStatus not implemented") + return nil, status.Error(codes.Unimplemented, "method GetStatus not implemented") +} +func (UnimplementedExporterServiceServer) EndSession(context.Context, *EndSessionRequest) (*EndSessionResponse, error) { + return nil, status.Error(codes.Unimplemented, "method EndSession not implemented") } func (UnimplementedExporterServiceServer) mustEmbedUnimplementedExporterServiceServer() {} func (UnimplementedExporterServiceServer) testEmbeddedByValue() {} @@ -711,7 +733,7 @@ type UnsafeExporterServiceServer interface { } func RegisterExporterServiceServer(s grpc.ServiceRegistrar, srv ExporterServiceServer) { - // If the following call pancis, it indicates UnimplementedExporterServiceServer was + // If the following call panics, it indicates UnimplementedExporterServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. @@ -815,6 +837,24 @@ func _ExporterService_GetStatus_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } +func _ExporterService_EndSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EndSessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ExporterServiceServer).EndSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ExporterService_EndSession_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ExporterServiceServer).EndSession(ctx, req.(*EndSessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + // ExporterService_ServiceDesc is the grpc.ServiceDesc for ExporterService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -838,6 +878,10 @@ var ExporterService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetStatus", Handler: _ExporterService_GetStatus_Handler, }, + { + MethodName: "EndSession", + Handler: _ExporterService_EndSession_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/internal/protocol/jumpstarter/v1/kubernetes.pb.go b/internal/protocol/jumpstarter/v1/kubernetes.pb.go index dac8801a..f4d0dde1 100644 --- a/internal/protocol/jumpstarter/v1/kubernetes.pb.go +++ b/internal/protocol/jumpstarter/v1/kubernetes.pb.go @@ -2,7 +2,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: jumpstarter/v1/kubernetes.proto diff --git a/internal/protocol/jumpstarter/v1/router.pb.go b/internal/protocol/jumpstarter/v1/router.pb.go index 49991bcc..be6fd8ef 100644 --- a/internal/protocol/jumpstarter/v1/router.pb.go +++ b/internal/protocol/jumpstarter/v1/router.pb.go @@ -2,7 +2,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: jumpstarter/v1/router.proto diff --git a/internal/protocol/jumpstarter/v1/router_grpc.pb.go b/internal/protocol/jumpstarter/v1/router_grpc.pb.go index 3bc8ee39..bb69ab64 100644 --- a/internal/protocol/jumpstarter/v1/router_grpc.pb.go +++ b/internal/protocol/jumpstarter/v1/router_grpc.pb.go @@ -2,7 +2,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc (unknown) // source: jumpstarter/v1/router.proto @@ -84,7 +84,7 @@ type RouterServiceServer interface { type UnimplementedRouterServiceServer struct{} func (UnimplementedRouterServiceServer) Stream(grpc.BidiStreamingServer[StreamRequest, StreamResponse]) error { - return status.Errorf(codes.Unimplemented, "method Stream not implemented") + return status.Error(codes.Unimplemented, "method Stream not implemented") } func (UnimplementedRouterServiceServer) mustEmbedUnimplementedRouterServiceServer() {} func (UnimplementedRouterServiceServer) testEmbeddedByValue() {} @@ -97,7 +97,7 @@ type UnsafeRouterServiceServer interface { } func RegisterRouterServiceServer(s grpc.ServiceRegistrar, srv RouterServiceServer) { - // If the following call pancis, it indicates UnimplementedRouterServiceServer was + // If the following call panics, it indicates UnimplementedRouterServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. 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)