diff --git a/README.md b/README.md index 8c88ca35..436ec592 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,10 @@ Octopus is an edge device management system based on Kubernetes, it is very ligh - [Idea](#idea) - [Workflow](#workflow) - [Walkthrough](#walkthrough) + + [Deploy Octopus](#deploy-octopus) + + [Deploy Device Model & Device Adaptor](#deploy-device-model--device-adaptor) + + [Create DeviceLink](#create-devicelink) + + [Manage Device](#manage-device) - [Documentation](#documentation) - [License](#license) @@ -79,7 +83,7 @@ In this walkthrough, we try to use Octopus to manage a dummy device. We will per 1. Deploy Octopus 1. Deploy Device Model & Device Adaptor -1. Create Device Link +1. Create DeviceLink 1. Manage Device ### Deploy Octopus @@ -257,15 +261,17 @@ octopus-adaptor-dummy-manager-rolebinding 43s ``` -### Create Device Link +### Create DeviceLink -Next, we are going to connect a device via `DeviceLink`. A link consists of 3 parts: `Adaptor`, `Model` and `Device spec`: +Next, we are going to connect a device via `DeviceLink`. A link mainly consists of 3 fields: `adaptor`, `model` and `template(device spec)`: -- `Adaptor` describes how to access the device, this connection process calls adaptation. In order to connect a device, we should indicate the name of the adaptor, the name of the device-connectable node and the parameters of this connection. -- `Model` describes the model of device, it is the [TypeMeta](https://github.com/kubernetes/apimachinery/blob/master/pkg/apis/meta/v1/types.go) of the device model CRD. -- `Device spec` describes the desired status of device, it is determined by the device model CRD. +- `adaptor` describes how to access the device, this accessing process calls **Adaptation**. In order to adapt a device, we need to indicate the name of the adaptor and the name of the device-connectable node. +- `model` describes the model of device, it is the [TypeMeta](https://github.com/kubernetes/apimachinery/blob/master/pkg/apis/meta/v1/types.go) of the device model CRD. +- `template(device spec)` describes the desired status of device, it is determined by the device model CRD. -We can imagine that there is a device named `living-room-fan` on the `edge-worker` node, we can try to connect it in. +In addition, we can also use the `references` field to refer the [ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap/) and [Secret](https://kubernetes.io/docs/concepts/configuration/secret/) under the same Namespace, even use the downward API to fetch the information in `DeviceLink`. + +We can imagine that there is a device named `living-room-fan` on the `edge-worker` node, and then we can connect it by Octopus: ```yaml apiVersion: edge.cattle.io/v1alpha1 @@ -292,7 +298,7 @@ spec: ``` -There are [several states](./docs/octopus/state_of_devicelink.md) of a link, if we found the **DeviceConnected** `PHASE` is on **Healthy** `STATUS`, we can query the same name instance of device model CRD, now the device is in our cluster: +After deployed the above `DeviceLink` into a cluster, we could find that there are [several states](./docs/octopus/state_of_devicelink.md) of a link. If the **DeviceConnected** `PHASE` is on **Healthy** `STATUS`, we can query the same name instance of device model CRD, now the device is in our cluster: ```shell script $ kubectl get devicelink living-room-fan -n default diff --git a/api/v1alpha1/devicelink_types.go b/api/v1alpha1/devicelink_types.go index e8164feb..1fdbbc71 100644 --- a/api/v1alpha1/devicelink_types.go +++ b/api/v1alpha1/devicelink_types.go @@ -1,10 +1,83 @@ package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) +// DeviceLinkReferenceSecretSource defines the source of a same name Secret instance. +type DeviceLinkReferenceSecretSource struct { + // Specifies the name of the Secret in the same Namespace to use. + // +kubebuilder:validation:Required + Name string `json:"name"` + + // Specifies the key of the Secret's data. + // If not specified, all keys of the Secret will be projected into the parameter values. + // If specified, the listed keys will be projected into the parameter value. + // If a key is specified which is not present in the Secret, + // the connection will error unless it is marked optional. + // +optional + Items []string `json:"items,omitempty"` +} + +// DeviceLinkReferenceConfigMapSource defines the source of a same name ConfigMap instance. +type DeviceLinkReferenceConfigMapSource struct { + // Specifies the name of the ConfigMap in the same Namespace to use. + // +kubebuilder:validation:Required + Name string `json:"name"` + + // Specifies the key of the ConfigMap's data. + // If not specified, all keys of the ConfigMap will be projected into the parameter values. + // If specified, the listed keys will be projected into the parameter value. + // If a key is specified which is not present in the ConfigMap, + // the connection will error unless it is marked optional. + // +optional + Items []string `json:"items,omitempty"` +} + +// DeviceLinkReferenceDownwardAPISourceItem defines the downward API item for projecting the DeviceLink. +type DeviceLinkReferenceDownwardAPISourceItem struct { + // Specifies the key of the downward API's data. + // +kubebuilder:validation:Required + Name string `json:"name"` + + // Specifies that how to select a field of the DeviceLink, + // only annotations, labels, name, namespace and status are supported. + // +kubebuilder:validation:Required + FieldRef *corev1.ObjectFieldSelector `json:"fieldRef"` +} + +// DeviceLinkReferenceDownwardAPISource defines the downward API for projecting the DeviceLink. +type DeviceLinkReferenceDownwardAPISource struct { + // Specifies a list of downward API. + // +kubebuilder:validation:MinItems=1 + Items []DeviceLinkReferenceDownwardAPISourceItem `json:"items"` +} + +// DeviceLinkReferenceSource defines the parameter source. +type DeviceLinkReferenceSource struct { + // Secret represents a Secret of the same Namespace that should populate this connection. + // +optional + Secret *DeviceLinkReferenceSecretSource `json:"secret,omitempty"` + + // ConfigMap represents a ConfigMap of the same Namespace that should populate this connection. + // +optional + ConfigMap *DeviceLinkReferenceConfigMapSource `json:"configMap,omitempty"` + + // DownwardAPI represents the downward API about the DeviceLink.¬ + // +optional + DownwardAPI *DeviceLinkReferenceDownwardAPISource `json:"downwardAPI,omitempty"` +} + +// DeviceLinkReference defines the parameter that should be passed to the adaptor during connecting. +type DeviceLinkReference struct { + DeviceLinkReferenceSource `json:",inline"` + + // Specifies the name of the parameter. + Name string `json:"name,omitempty"` +} + // DeviceAdaptor defines the properties of device adaptor type DeviceAdaptor struct { // Specifies the node of adaptor to be matched. @@ -12,10 +85,12 @@ type DeviceAdaptor struct { Node string `json:"node,omitempty"` // Specifies the name of adaptor to be used. - // +optional + // +kubebuilder:validation:Required Name string `json:"name,omitempty"` - // Specifies the parameter of adaptor to be used. + // [Deprecated] Specifies the parameter of adaptor to be used. + // This field has been deprecated, it should define the connection parameter + // as a part of device model. // +kubebuilder:pruning:PreserveUnknownFields // +optional Parameters *runtime.RawExtension `json:"parameters,omitempty"` @@ -96,6 +171,10 @@ type DeviceLinkSpec struct { // +kubebuilder:validation:Required Model metav1.TypeMeta `json:"model"` + // Specifies the references of device to be used. + // +optional + References []DeviceLinkReference `json:"references,omitempty"` + // Describe the device that will be created. // +kubebuilder:validation:Required Template DeviceTemplateSpec `json:"template"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 0e056852..3114e269 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -20,6 +20,7 @@ limitations under the License. package v1alpha1 import ( + "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -119,11 +120,146 @@ func (in *DeviceLinkList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceLinkReference) DeepCopyInto(out *DeviceLinkReference) { + *out = *in + in.DeviceLinkReferenceSource.DeepCopyInto(&out.DeviceLinkReferenceSource) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceLinkReference. +func (in *DeviceLinkReference) DeepCopy() *DeviceLinkReference { + if in == nil { + return nil + } + out := new(DeviceLinkReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceLinkReferenceConfigMapSource) DeepCopyInto(out *DeviceLinkReferenceConfigMapSource) { + *out = *in + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceLinkReferenceConfigMapSource. +func (in *DeviceLinkReferenceConfigMapSource) DeepCopy() *DeviceLinkReferenceConfigMapSource { + if in == nil { + return nil + } + out := new(DeviceLinkReferenceConfigMapSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceLinkReferenceDownwardAPISource) DeepCopyInto(out *DeviceLinkReferenceDownwardAPISource) { + *out = *in + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DeviceLinkReferenceDownwardAPISourceItem, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceLinkReferenceDownwardAPISource. +func (in *DeviceLinkReferenceDownwardAPISource) DeepCopy() *DeviceLinkReferenceDownwardAPISource { + if in == nil { + return nil + } + out := new(DeviceLinkReferenceDownwardAPISource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceLinkReferenceDownwardAPISourceItem) DeepCopyInto(out *DeviceLinkReferenceDownwardAPISourceItem) { + *out = *in + if in.FieldRef != nil { + in, out := &in.FieldRef, &out.FieldRef + *out = new(v1.ObjectFieldSelector) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceLinkReferenceDownwardAPISourceItem. +func (in *DeviceLinkReferenceDownwardAPISourceItem) DeepCopy() *DeviceLinkReferenceDownwardAPISourceItem { + if in == nil { + return nil + } + out := new(DeviceLinkReferenceDownwardAPISourceItem) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceLinkReferenceSecretSource) DeepCopyInto(out *DeviceLinkReferenceSecretSource) { + *out = *in + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceLinkReferenceSecretSource. +func (in *DeviceLinkReferenceSecretSource) DeepCopy() *DeviceLinkReferenceSecretSource { + if in == nil { + return nil + } + out := new(DeviceLinkReferenceSecretSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceLinkReferenceSource) DeepCopyInto(out *DeviceLinkReferenceSource) { + *out = *in + if in.Secret != nil { + in, out := &in.Secret, &out.Secret + *out = new(DeviceLinkReferenceSecretSource) + (*in).DeepCopyInto(*out) + } + if in.ConfigMap != nil { + in, out := &in.ConfigMap, &out.ConfigMap + *out = new(DeviceLinkReferenceConfigMapSource) + (*in).DeepCopyInto(*out) + } + if in.DownwardAPI != nil { + in, out := &in.DownwardAPI, &out.DownwardAPI + *out = new(DeviceLinkReferenceDownwardAPISource) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceLinkReferenceSource. +func (in *DeviceLinkReferenceSource) DeepCopy() *DeviceLinkReferenceSource { + if in == nil { + return nil + } + out := new(DeviceLinkReferenceSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DeviceLinkSpec) DeepCopyInto(out *DeviceLinkSpec) { *out = *in in.Adaptor.DeepCopyInto(&out.Adaptor) out.Model = in.Model + if in.References != nil { + in, out := &in.References, &out.References + *out = make([]DeviceLinkReference, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } in.Template.DeepCopyInto(&out.Template) } diff --git a/deploy/e2e/all_in_one.yaml b/deploy/e2e/all_in_one.yaml index 03368281..1941b56b 100644 --- a/deploy/e2e/all_in_one.yaml +++ b/deploy/e2e/all_in_one.yaml @@ -89,7 +89,9 @@ spec: description: Specifies the node of adaptor to be matched. type: string parameters: - description: Specifies the parameter of adaptor to be used. + description: '[Deprecated] Specifies the parameter of adaptor + to be used. This field has been deprecated, it should define + the connection parameter as a part of device model.' type: object x-kubernetes-preserve-unknown-fields: true type: object @@ -109,6 +111,98 @@ spec: More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string type: object + references: + description: Specifies the references of device to be used. + items: + description: DeviceLinkReference defines the parameter that should + be passed to the adaptor during connecting. + properties: + configMap: + description: ConfigMap represents a ConfigMap of the same Namespace + that should populate this connection. + properties: + items: + description: Specifies the key of the ConfigMap's data. + If not specified, all keys of the ConfigMap will be projected + into the parameter values. If specified, the listed keys + will be projected into the parameter value. If a key is + specified which is not present in the ConfigMap, the connection + will error unless it is marked optional. + items: + type: string + type: array + name: + description: Specifies the name of the ConfigMap in the + same Namespace to use. + type: string + required: + - name + type: object + downwardAPI: + description: DownwardAPI represents the downward API about the + DeviceLink.¬ + properties: + items: + description: Specifies a list of downward API. + items: + description: DeviceLinkReferenceDownwardAPISourceItem + defines the downward API item for projecting the DeviceLink. + properties: + fieldRef: + description: Specifies that how to select a field + of the DeviceLink, only annotations, labels, name, + namespace and status are supported. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + name: + description: Specifies the key of the downward API's + data. + type: string + required: + - fieldRef + - name + type: object + minItems: 1 + type: array + required: + - items + type: object + name: + description: Specifies the name of the parameter. + type: string + secret: + description: Secret represents a Secret of the same Namespace + that should populate this connection. + properties: + items: + description: Specifies the key of the Secret's data. If + not specified, all keys of the Secret will be projected + into the parameter values. If specified, the listed keys + will be projected into the parameter value. If a key is + specified which is not present in the Secret, the connection + will error unless it is marked optional. + items: + type: string + type: array + name: + description: Specifies the name of the Secret in the same + Namespace to use. + type: string + required: + - name + type: object + type: object + type: array template: description: Describe the device that will be created. properties: @@ -345,6 +439,14 @@ metadata: app.kubernetes.io/version: master name: octopus-manager-role rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch - apiGroups: - "" resources: @@ -362,6 +464,14 @@ rules: - patch - update - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch - apiGroups: - apiextensions.k8s.io resources: diff --git a/deploy/e2e/all_in_one_without_webhook.yaml b/deploy/e2e/all_in_one_without_webhook.yaml index f8283508..3df300ff 100644 --- a/deploy/e2e/all_in_one_without_webhook.yaml +++ b/deploy/e2e/all_in_one_without_webhook.yaml @@ -76,7 +76,9 @@ spec: description: Specifies the node of adaptor to be matched. type: string parameters: - description: Specifies the parameter of adaptor to be used. + description: '[Deprecated] Specifies the parameter of adaptor + to be used. This field has been deprecated, it should define + the connection parameter as a part of device model.' type: object x-kubernetes-preserve-unknown-fields: true type: object @@ -96,6 +98,98 @@ spec: More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string type: object + references: + description: Specifies the references of device to be used. + items: + description: DeviceLinkReference defines the parameter that should + be passed to the adaptor during connecting. + properties: + configMap: + description: ConfigMap represents a ConfigMap of the same Namespace + that should populate this connection. + properties: + items: + description: Specifies the key of the ConfigMap's data. + If not specified, all keys of the ConfigMap will be projected + into the parameter values. If specified, the listed keys + will be projected into the parameter value. If a key is + specified which is not present in the ConfigMap, the connection + will error unless it is marked optional. + items: + type: string + type: array + name: + description: Specifies the name of the ConfigMap in the + same Namespace to use. + type: string + required: + - name + type: object + downwardAPI: + description: DownwardAPI represents the downward API about the + DeviceLink.¬ + properties: + items: + description: Specifies a list of downward API. + items: + description: DeviceLinkReferenceDownwardAPISourceItem + defines the downward API item for projecting the DeviceLink. + properties: + fieldRef: + description: Specifies that how to select a field + of the DeviceLink, only annotations, labels, name, + namespace and status are supported. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + name: + description: Specifies the key of the downward API's + data. + type: string + required: + - fieldRef + - name + type: object + minItems: 1 + type: array + required: + - items + type: object + name: + description: Specifies the name of the parameter. + type: string + secret: + description: Secret represents a Secret of the same Namespace + that should populate this connection. + properties: + items: + description: Specifies the key of the Secret's data. If + not specified, all keys of the Secret will be projected + into the parameter values. If specified, the listed keys + will be projected into the parameter value. If a key is + specified which is not present in the Secret, the connection + will error unless it is marked optional. + items: + type: string + type: array + name: + description: Specifies the name of the Secret in the same + Namespace to use. + type: string + required: + - name + type: object + type: object + type: array template: description: Describe the device that will be created. properties: @@ -262,6 +356,14 @@ metadata: app.kubernetes.io/version: master name: octopus-manager-role rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch - apiGroups: - "" resources: @@ -279,6 +381,14 @@ rules: - patch - update - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch - apiGroups: - apiextensions.k8s.io resources: diff --git a/deploy/manifests/crd/base/edge.cattle.io_devicelinks.yaml b/deploy/manifests/crd/base/edge.cattle.io_devicelinks.yaml index bf8a91a8..04104b7b 100644 --- a/deploy/manifests/crd/base/edge.cattle.io_devicelinks.yaml +++ b/deploy/manifests/crd/base/edge.cattle.io_devicelinks.yaml @@ -67,7 +67,9 @@ spec: description: Specifies the node of adaptor to be matched. type: string parameters: - description: Specifies the parameter of adaptor to be used. + description: '[Deprecated] Specifies the parameter of adaptor + to be used. This field has been deprecated, it should define + the connection parameter as a part of device model.' type: object x-kubernetes-preserve-unknown-fields: true type: object @@ -87,6 +89,98 @@ spec: More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string type: object + references: + description: Specifies the references of device to be used. + items: + description: DeviceLinkReference defines the parameter that should + be passed to the adaptor during connecting. + properties: + configMap: + description: ConfigMap represents a ConfigMap of the same Namespace + that should populate this connection. + properties: + items: + description: Specifies the key of the ConfigMap's data. + If not specified, all keys of the ConfigMap will be projected + into the parameter values. If specified, the listed keys + will be projected into the parameter value. If a key is + specified which is not present in the ConfigMap, the connection + will error unless it is marked optional. + items: + type: string + type: array + name: + description: Specifies the name of the ConfigMap in the + same Namespace to use. + type: string + required: + - name + type: object + downwardAPI: + description: DownwardAPI represents the downward API about the + DeviceLink.¬ + properties: + items: + description: Specifies a list of downward API. + items: + description: DeviceLinkReferenceDownwardAPISourceItem + defines the downward API item for projecting the DeviceLink. + properties: + fieldRef: + description: Specifies that how to select a field + of the DeviceLink, only annotations, labels, name, + namespace and status are supported. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + name: + description: Specifies the key of the downward API's + data. + type: string + required: + - fieldRef + - name + type: object + minItems: 1 + type: array + required: + - items + type: object + name: + description: Specifies the name of the parameter. + type: string + secret: + description: Secret represents a Secret of the same Namespace + that should populate this connection. + properties: + items: + description: Specifies the key of the Secret's data. If + not specified, all keys of the Secret will be projected + into the parameter values. If specified, the listed keys + will be projected into the parameter value. If a key is + specified which is not present in the Secret, the connection + will error unless it is marked optional. + items: + type: string + type: array + name: + description: Specifies the name of the Secret in the same + Namespace to use. + type: string + required: + - name + type: object + type: object + type: array template: description: Describe the device that will be created. properties: diff --git a/deploy/manifests/rbac/role.yaml b/deploy/manifests/rbac/role.yaml index 36e2683a..01f3642e 100644 --- a/deploy/manifests/rbac/role.yaml +++ b/deploy/manifests/rbac/role.yaml @@ -6,6 +6,14 @@ metadata: creationTimestamp: null name: manager-role rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch - apiGroups: - "" resources: @@ -23,6 +31,14 @@ rules: - patch - update - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch - apiGroups: - apiextensions.k8s.io resources: diff --git a/docs/adaptors/design_of_adaptor.md b/docs/adaptors/design_of_adaptor.md index 581f24d5..061eadab 100644 --- a/docs/adaptors/design_of_adaptor.md +++ b/docs/adaptors/design_of_adaptor.md @@ -3,7 +3,10 @@ - [Idea](#idea) -- [Available Adaptor List](#available-adaptor-list) + + [APIs](#apis) + + [Registration](#registration) + + [Connection](#connection) +- [Available Adaptors](#available-adaptors) @@ -80,9 +83,17 @@ At the same time, the implementation of adapter can be connected to a single dev Please view [here](./develop.md) for more detail about developing an adaptor. -The access management of adaptors takes inspiration from [Kubernetes Device Plugins management](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/). The workflow includes the following steps: +### APIs -1. The `limb` starts a gRPC service with a Unix socket on host path to receive registration requests from adaptors: +The access management of adaptors takes inspiration from [Kubernetes Device Plugins management](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/). The available version of access management APIs is `v1alpha1`. + +| Versions | Available | Current | +|:---:|:---:|:---:| +| [`v1alpha1`](./design_of_adaptor.md) | * | * | + +Following the below steps can allow an adaptor to interact with `limb`: + +1. The `limb` starts a gRPC service with a Unix socket on host path to receive registration requests from adaptors: ```proto // Registration is the service advertised by the Limb, // any adaptor start its service until Limb approved this register request. @@ -91,7 +102,7 @@ The access management of adaptors takes inspiration from [Kubernetes Device Plug } message RegisterRequest { - // Name of the adaptor in the form `adaptor-vendor.com/adaptor-vendor`. + // Name of the adaptor in the form `adaptor-vendor.com/adaptor-name`. string name = 1; // Version of the API the adaptor was built against. string version = 2; @@ -99,20 +110,26 @@ The access management of adaptors takes inspiration from [Kubernetes Device Plug string endpoint = 3; } ``` -1. The adaptor starts a gRPC service with a Unix socket under host path `/var/lib/octopus/adaptors`, that implements the following interfaces: +1. The adaptor starts a gRPC service with a Unix socket under host path `/var/lib/octopus/adaptors`, that implements the following interfaces: ```proto // Connection is the service advertised by the adaptor. service Connection { rpc Connect (stream ConnectRequest) returns (stream ConnectResponse) {} } + message ConnectRequestReferenceEntry { + map items = 1; + } + message ConnectRequest { - // Parameters for the connection, it's in form JSON bytes. + // [Deprecated] Parameters for the connection, it's in form JSON bytes. bytes parameters = 1; // Model for the device. k8s.io.apimachinery.pkg.apis.meta.v1.TypeMeta model = 2; // Desired device, it's in form JSON bytes. bytes device = 3; + // References for the device, i.e: Secret, ConfigMap and Downward API. + map references = 4; } message ConnectResponse { @@ -123,7 +140,29 @@ The access management of adaptors takes inspiration from [Kubernetes Device Plug 1. The adaptor registers itself with the `limb` through the Unix socket at host path `/var/lib/octopus/adaptors/limb.socket`. 1. After successfully registering itself, the adaptor runs in serving mode, during which it keeps connecting devices and reports back to the `limb` upon any device state changes. -## Available Adaptor List +#### Registration + +The **Registration** can let the `limb` to know the existence of an adaptor, on this phase, the `limb` acts as a server and the adaptor acts as a client. The adaptor constructs a registration request with its `name`, `version` and accessing `endpoint`, and then request to `limb`. After successful registered, the `limb` will keep watching the adaptor and notify those DeviceLinks related to the registered adaptor. + +- The `name` is the name of the adaptor, it's strongly recommended that to use this pattern `adaptor-vendor.com/adaptor-name` to named an adaptor, each adaptor must have one unique `name`. + > The second adaptor who has the same `name` will overwrite the previous one. +- The `version` is the API version of accessing management, for now, it's fixed in `v1alpha1`. +- The accessing `endpiont` is the name of unix socket, each adaptor must have one unique `endpoint`. + > The second adaptor who has the same registered `endpoint` will never register successfully until the previous one quits. + +#### Connection + +The **Connection** can let the `limb` to connect to an adaptor, on this phase, the adaptor acts as a server and the `limb` acts as a client. The `limb` constructs a connection request with the `parameters`, `model`, `device` and `references`, and then request to the target adaptor. + +- The `parameters` is the parameters used for connection, it's in form JSON bytes. + > This `parameters` field has been **DEPRECATED**, it should define the connection parameter as a part of device model. +- The `model` is the device's model, it's useful to help adaptor to distinguish multiple models, or maintain the compatibility if there are difference versions in one model. +- The `device` is the device's instance, it's in form JSON bytes, which is JSON bytes of a complete `model` instance and contains `spec` and `status` data. + > The adaptor should select the corresponding deserialized receiving object according to the `model` to receive this field's data. + > Since the receiving object (device instance) is a legal CRD instance, it strictly conforms to the resource management convention of Kubernetes, so a device can be uniquely identified by Namespace and Name. +- The `references` is the reference sources of the device, it allows the device to use the ConfigMap and Secret under the same Namespace, or downward API of the parent DeviceLink instance. + +## Available Adaptors - [dummy](../../adaptors/dummy) - [ble](../../adaptors/ble) diff --git a/docs/octopus/state_of_devicelink.md b/docs/octopus/state_of_devicelink.md index 85b34b45..1cf22c5e 100644 --- a/docs/octopus/state_of_devicelink.md +++ b/docs/octopus/state_of_devicelink.md @@ -24,8 +24,6 @@ spec: adaptor: node: edge-worker name: adaptors.edge.cattle.io/dummy - parameters: - ip: 192.168.2.47 model: apiVersion: "devices.edge.cattle.io/v1alpha1" kind: "DummyDevice" @@ -114,7 +112,7 @@ You can view [Design of Adaptor](../adaptors/design_of_adaptor.md) to learn how ' ``` -After the device instance is successfully created, the `limb` will use the `spec.adaptor.parameters` and `spec.template.spec` to connect that real device via adaptor: +After the device instance is successfully created, the `limb` will use the `spec.template.spec` to connect that real device via adaptor: ```text │ diff --git a/pkg/adaptor/api/v1alpha1/api.pb.go b/pkg/adaptor/api/v1alpha1/api.pb.go index e4f724fb..44e4cd6e 100644 --- a/pkg/adaptor/api/v1alpha1/api.pb.go +++ b/pkg/adaptor/api/v1alpha1/api.pb.go @@ -22,6 +22,7 @@ import ( fmt "fmt" _ "github.com/gogo/protobuf/gogoproto" proto "github.com/gogo/protobuf/proto" + github_com_gogo_protobuf_sortkeys "github.com/gogo/protobuf/sortkeys" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" @@ -80,7 +81,7 @@ func (m *Empty) XXX_DiscardUnknown() { var xxx_messageInfo_Empty proto.InternalMessageInfo type RegisterRequest struct { - // Name of the adaptor in the form `adaptor-vendor.com/adaptor-vendor`. + // Name of the adaptor in the form `adaptor-vendor.com/adaptor-name`. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Version of the API the adaptor was built against. Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` @@ -141,19 +142,64 @@ func (m *RegisterRequest) GetEndpoint() string { return "" } +type ConnectRequestReferenceEntry struct { + Items map[string][]byte `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (m *ConnectRequestReferenceEntry) Reset() { *m = ConnectRequestReferenceEntry{} } +func (*ConnectRequestReferenceEntry) ProtoMessage() {} +func (*ConnectRequestReferenceEntry) Descriptor() ([]byte, []int) { + return fileDescriptor_00212fb1f9d3bf1c, []int{2} +} +func (m *ConnectRequestReferenceEntry) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ConnectRequestReferenceEntry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ConnectRequestReferenceEntry.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *ConnectRequestReferenceEntry) XXX_Merge(src proto.Message) { + xxx_messageInfo_ConnectRequestReferenceEntry.Merge(m, src) +} +func (m *ConnectRequestReferenceEntry) XXX_Size() int { + return m.Size() +} +func (m *ConnectRequestReferenceEntry) XXX_DiscardUnknown() { + xxx_messageInfo_ConnectRequestReferenceEntry.DiscardUnknown(m) +} + +var xxx_messageInfo_ConnectRequestReferenceEntry proto.InternalMessageInfo + +func (m *ConnectRequestReferenceEntry) GetItems() map[string][]byte { + if m != nil { + return m.Items + } + return nil +} + type ConnectRequest struct { - // Parameters for the connection, it's in form JSON bytes. + // [Deprecated] Parameters for the connection, it's in form JSON bytes. Parameters []byte `protobuf:"bytes,1,opt,name=parameters,proto3" json:"parameters,omitempty"` // Model for the device. Model *v1.TypeMeta `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` // Desired device, it's in form JSON bytes. Device []byte `protobuf:"bytes,3,opt,name=device,proto3" json:"device,omitempty"` + // References for the device, i.e: Secret, ConfigMap and Downward API. + References map[string]*ConnectRequestReferenceEntry `protobuf:"bytes,4,rep,name=references,proto3" json:"references,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } func (m *ConnectRequest) Reset() { *m = ConnectRequest{} } func (*ConnectRequest) ProtoMessage() {} func (*ConnectRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_00212fb1f9d3bf1c, []int{2} + return fileDescriptor_00212fb1f9d3bf1c, []int{3} } func (m *ConnectRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -203,6 +249,13 @@ func (m *ConnectRequest) GetDevice() []byte { return nil } +func (m *ConnectRequest) GetReferences() map[string]*ConnectRequestReferenceEntry { + if m != nil { + return m.References + } + return nil +} + type ConnectResponse struct { // Observed device, it's in form JSON bytes. Device []byte `protobuf:"bytes,1,opt,name=device,proto3" json:"device,omitempty"` @@ -211,7 +264,7 @@ type ConnectResponse struct { func (m *ConnectResponse) Reset() { *m = ConnectResponse{} } func (*ConnectResponse) ProtoMessage() {} func (*ConnectResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_00212fb1f9d3bf1c, []int{3} + return fileDescriptor_00212fb1f9d3bf1c, []int{4} } func (m *ConnectResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -250,39 +303,50 @@ func (m *ConnectResponse) GetDevice() []byte { func init() { proto.RegisterType((*Empty)(nil), "v1alpha1.Empty") proto.RegisterType((*RegisterRequest)(nil), "v1alpha1.RegisterRequest") + proto.RegisterType((*ConnectRequestReferenceEntry)(nil), "v1alpha1.ConnectRequestReferenceEntry") + proto.RegisterMapType((map[string][]byte)(nil), "v1alpha1.ConnectRequestReferenceEntry.ItemsEntry") proto.RegisterType((*ConnectRequest)(nil), "v1alpha1.ConnectRequest") + proto.RegisterMapType((map[string]*ConnectRequestReferenceEntry)(nil), "v1alpha1.ConnectRequest.ReferencesEntry") proto.RegisterType((*ConnectResponse)(nil), "v1alpha1.ConnectResponse") } func init() { proto.RegisterFile("api.proto", fileDescriptor_00212fb1f9d3bf1c) } var fileDescriptor_00212fb1f9d3bf1c = []byte{ - // 397 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x51, 0xb1, 0x8e, 0xd4, 0x30, - 0x10, 0x8d, 0x81, 0xbb, 0xdd, 0x1b, 0x56, 0xac, 0xe4, 0x02, 0xe5, 0x52, 0x58, 0x28, 0xa2, 0x38, - 0x0a, 0x1c, 0x76, 0xa1, 0xb8, 0x1a, 0x0e, 0x89, 0x86, 0x26, 0xa2, 0xa3, 0xf2, 0x26, 0x43, 0xd6, - 0xda, 0x8b, 0x6d, 0x6c, 0x6f, 0xa4, 0xed, 0xf8, 0x02, 0xc4, 0x67, 0x5d, 0x79, 0xe5, 0x95, 0x5c, - 0xf6, 0x47, 0x50, 0x9c, 0xcd, 0x6d, 0x40, 0x74, 0x7e, 0x6f, 0xde, 0x78, 0xe6, 0xbd, 0x81, 0x33, - 0x61, 0x24, 0x37, 0x56, 0x7b, 0x4d, 0xa7, 0xcd, 0x42, 0x5c, 0x9b, 0xb5, 0x58, 0x24, 0xaf, 0x2b, - 0xe9, 0xd7, 0xdb, 0x15, 0x2f, 0x74, 0x9d, 0x55, 0xba, 0xd2, 0x59, 0x10, 0xac, 0xb6, 0xdf, 0x02, - 0x0a, 0x20, 0xbc, 0xfa, 0xc6, 0xe4, 0xdd, 0xe6, 0xd2, 0x71, 0xa9, 0x33, 0x61, 0x64, 0x2d, 0x8a, - 0xb5, 0x54, 0x68, 0x77, 0x99, 0xd9, 0x54, 0x1d, 0xe1, 0xb2, 0x1a, 0xbd, 0xc8, 0x9a, 0x45, 0x56, - 0xa1, 0x42, 0x2b, 0x3c, 0x96, 0x7d, 0x57, 0x3a, 0x81, 0x93, 0x8f, 0xb5, 0xf1, 0xbb, 0xf4, 0x2b, - 0xcc, 0x73, 0xac, 0xa4, 0xf3, 0x68, 0x73, 0xfc, 0xbe, 0x45, 0xe7, 0x29, 0x85, 0x27, 0x4a, 0xd4, - 0x18, 0x93, 0x17, 0xe4, 0xe2, 0x2c, 0x0f, 0x6f, 0x1a, 0xc3, 0xa4, 0x41, 0xeb, 0xa4, 0x56, 0xf1, - 0xa3, 0x40, 0x0f, 0x90, 0x26, 0x30, 0x45, 0x55, 0x1a, 0x2d, 0x95, 0x8f, 0x1f, 0x87, 0xd2, 0x03, - 0x4e, 0x7f, 0x12, 0x78, 0xf6, 0x41, 0x2b, 0x85, 0x85, 0x1f, 0x3e, 0x67, 0x00, 0x46, 0x58, 0x51, - 0xa3, 0x47, 0xeb, 0xc2, 0x88, 0x59, 0x3e, 0x62, 0xe8, 0x15, 0x9c, 0xd4, 0xba, 0xc4, 0xeb, 0x30, - 0xe6, 0xe9, 0x92, 0xf3, 0xde, 0x1e, 0x1f, 0xdb, 0xe3, 0x66, 0x53, 0x75, 0x84, 0xe3, 0x9d, 0x3d, - 0xde, 0x2c, 0xf8, 0x97, 0x9d, 0xc1, 0xcf, 0xe8, 0x45, 0xde, 0x37, 0xd3, 0xe7, 0x70, 0x5a, 0x62, - 0x23, 0x0b, 0x0c, 0x2b, 0xcd, 0xf2, 0x03, 0x4a, 0x5f, 0xc1, 0xfc, 0x61, 0x1f, 0x67, 0xb4, 0x72, - 0x38, 0x92, 0x92, 0xb1, 0x74, 0xf9, 0x09, 0x66, 0x7d, 0x30, 0x56, 0xf8, 0xce, 0xe7, 0x25, 0x4c, - 0x87, 0xa0, 0xe8, 0x39, 0x1f, 0xae, 0xc5, 0xff, 0x09, 0x2f, 0x99, 0x1f, 0x4b, 0x7d, 0xc0, 0xd1, - 0x32, 0x07, 0x38, 0x0c, 0xed, 0xfe, 0xb9, 0x82, 0xc9, 0x01, 0xd1, 0xf8, 0xa8, 0xfd, 0x3b, 0xa5, - 0xe4, 0xfc, 0x3f, 0x95, 0x7e, 0xdf, 0x34, 0xba, 0x20, 0x6f, 0xc8, 0xfb, 0x97, 0x37, 0xf7, 0x8c, - 0xdc, 0xdd, 0xb3, 0xe8, 0x47, 0xcb, 0xc8, 0x4d, 0xcb, 0xc8, 0x6d, 0xcb, 0xc8, 0xef, 0x96, 0x91, - 0x5f, 0x7b, 0x16, 0xdd, 0xee, 0x59, 0x74, 0xb7, 0x67, 0xd1, 0xea, 0x34, 0x1c, 0xfb, 0xed, 0x9f, - 0x00, 0x00, 0x00, 0xff, 0xff, 0x72, 0x93, 0x17, 0x4f, 0x68, 0x02, 0x00, 0x00, + // 515 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x53, 0xc1, 0x6e, 0xd3, 0x40, + 0x10, 0xf5, 0x26, 0x4d, 0x93, 0x4e, 0x22, 0x82, 0x56, 0x08, 0xb9, 0x16, 0xb2, 0xaa, 0x08, 0xa1, + 0x70, 0x60, 0x4d, 0x02, 0x87, 0x08, 0x71, 0x82, 0x56, 0x94, 0x03, 0x17, 0x8b, 0x1b, 0xa7, 0x4d, + 0x32, 0x75, 0xac, 0xc4, 0xde, 0x65, 0x77, 0x63, 0x29, 0x37, 0x3e, 0x81, 0x5f, 0xe0, 0x4b, 0xb8, + 0xf6, 0xd8, 0x63, 0x8f, 0x34, 0xf9, 0x11, 0xe4, 0xb5, 0x93, 0xb8, 0xa8, 0x45, 0xbd, 0xcd, 0x9b, + 0xf1, 0x9b, 0xd9, 0x79, 0xf3, 0x0c, 0x47, 0x5c, 0xc6, 0x4c, 0x2a, 0x61, 0x04, 0x6d, 0x65, 0x03, + 0xbe, 0x90, 0x33, 0x3e, 0xf0, 0x5e, 0x45, 0xb1, 0x99, 0x2d, 0xc7, 0x6c, 0x22, 0x92, 0x20, 0x12, + 0x91, 0x08, 0xec, 0x07, 0xe3, 0xe5, 0x85, 0x45, 0x16, 0xd8, 0xa8, 0x20, 0x7a, 0x6f, 0xe7, 0x23, + 0xcd, 0x62, 0x11, 0x70, 0x19, 0x27, 0x7c, 0x32, 0x8b, 0x53, 0x54, 0xab, 0x40, 0xce, 0xa3, 0x3c, + 0xa1, 0x83, 0x04, 0x0d, 0x0f, 0xb2, 0x41, 0x10, 0x61, 0x8a, 0x8a, 0x1b, 0x9c, 0x16, 0xac, 0x5e, + 0x13, 0x1a, 0x67, 0x89, 0x34, 0xab, 0xde, 0x37, 0xe8, 0x86, 0x18, 0xc5, 0xda, 0xa0, 0x0a, 0xf1, + 0xfb, 0x12, 0xb5, 0xa1, 0x14, 0x0e, 0x52, 0x9e, 0xa0, 0x4b, 0x4e, 0x48, 0xff, 0x28, 0xb4, 0x31, + 0x75, 0xa1, 0x99, 0xa1, 0xd2, 0xb1, 0x48, 0xdd, 0x9a, 0x4d, 0x6f, 0x21, 0xf5, 0xa0, 0x85, 0xe9, + 0x54, 0x8a, 0x38, 0x35, 0x6e, 0xdd, 0x96, 0x76, 0xb8, 0xf7, 0x8b, 0xc0, 0xb3, 0x8f, 0x22, 0x4d, + 0x71, 0x62, 0xca, 0xe6, 0x21, 0x5e, 0xa0, 0xc2, 0x74, 0x82, 0x67, 0xa9, 0x51, 0x2b, 0xfa, 0x09, + 0x1a, 0xb1, 0xc1, 0x44, 0xbb, 0xe4, 0xa4, 0xde, 0x6f, 0x0f, 0x07, 0x6c, 0xab, 0x02, 0xfb, 0x1f, + 0x8d, 0x7d, 0xce, 0x39, 0x36, 0x0c, 0x0b, 0xbe, 0x37, 0x02, 0xd8, 0x27, 0xe9, 0x63, 0xa8, 0xcf, + 0x71, 0x55, 0x2e, 0x90, 0x87, 0xf4, 0x09, 0x34, 0x32, 0xbe, 0x58, 0xa2, 0x7d, 0x7d, 0x27, 0x2c, + 0xc0, 0xbb, 0xda, 0x88, 0xf4, 0x7e, 0xd7, 0xe0, 0xd1, 0xed, 0x61, 0xd4, 0x07, 0x90, 0x5c, 0xf1, + 0x04, 0x0d, 0x2a, 0x6d, 0xbb, 0x74, 0xc2, 0x4a, 0x86, 0x9e, 0x42, 0x23, 0x11, 0x53, 0x5c, 0xd8, + 0x66, 0xed, 0x21, 0x63, 0xc5, 0x09, 0x58, 0xf5, 0x04, 0x4c, 0xce, 0xa3, 0x3c, 0xa1, 0x59, 0x7e, + 0x02, 0x96, 0x0d, 0xd8, 0xd7, 0x95, 0xc4, 0x2f, 0x68, 0x78, 0x58, 0x90, 0xe9, 0x53, 0x38, 0x9c, + 0x62, 0x16, 0x4f, 0xd0, 0xca, 0xd6, 0x09, 0x4b, 0x44, 0xcf, 0x01, 0xd4, 0x76, 0x5d, 0xed, 0x1e, + 0x58, 0x61, 0xfa, 0xf7, 0x09, 0xc3, 0x76, 0xca, 0x94, 0x7a, 0x54, 0xb8, 0x1e, 0xe6, 0xb7, 0xbd, + 0x55, 0xbe, 0x43, 0x99, 0xf7, 0x55, 0x65, 0xda, 0xc3, 0x17, 0x0f, 0x3b, 0x41, 0x55, 0xc1, 0x97, + 0xd0, 0xdd, 0x7d, 0xaa, 0xa5, 0x48, 0x35, 0x56, 0x76, 0x23, 0xd5, 0xdd, 0x86, 0xe7, 0xd0, 0x29, + 0xdc, 0xa6, 0xb8, 0xc9, 0xcd, 0x33, 0x82, 0xd6, 0xd6, 0x7d, 0xf4, 0x78, 0x3f, 0xf9, 0x1f, 0x47, + 0x7a, 0xdd, 0x7d, 0xa9, 0x70, 0xad, 0x33, 0x0c, 0x01, 0xca, 0xa1, 0x79, 0x9f, 0x53, 0x68, 0x96, + 0x88, 0xba, 0xf7, 0x2d, 0xe0, 0x1d, 0xdf, 0x51, 0x29, 0xde, 0xdb, 0x73, 0xfa, 0xe4, 0x35, 0xf9, + 0xf0, 0xfc, 0xf2, 0xc6, 0x27, 0xd7, 0x37, 0xbe, 0xf3, 0x63, 0xed, 0x93, 0xcb, 0xb5, 0x4f, 0xae, + 0xd6, 0x3e, 0xf9, 0xb3, 0xf6, 0xc9, 0xcf, 0x8d, 0xef, 0x5c, 0x6d, 0x7c, 0xe7, 0x7a, 0xe3, 0x3b, + 0xe3, 0x43, 0xfb, 0x07, 0xbd, 0xf9, 0x1b, 0x00, 0x00, 0xff, 0xff, 0xcb, 0x96, 0x7e, 0xa6, 0xbd, + 0x03, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -536,6 +600,50 @@ func (m *RegisterRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *ConnectRequestReferenceEntry) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ConnectRequestReferenceEntry) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ConnectRequestReferenceEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Items) > 0 { + for k := range m.Items { + v := m.Items[k] + baseI := i + if len(v) > 0 { + i -= len(v) + copy(dAtA[i:], v) + i = encodeVarintApi(dAtA, i, uint64(len(v))) + i-- + dAtA[i] = 0x12 + } + i -= len(k) + copy(dAtA[i:], k) + i = encodeVarintApi(dAtA, i, uint64(len(k))) + i-- + dAtA[i] = 0xa + i = encodeVarintApi(dAtA, i, uint64(baseI-i)) + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + func (m *ConnectRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -556,6 +664,32 @@ func (m *ConnectRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.References) > 0 { + for k := range m.References { + v := m.References[k] + baseI := i + if v != nil { + { + size, err := v.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintApi(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + i -= len(k) + copy(dAtA[i:], k) + i = encodeVarintApi(dAtA, i, uint64(len(k))) + i-- + dAtA[i] = 0xa + i = encodeVarintApi(dAtA, i, uint64(baseI-i)) + i-- + dAtA[i] = 0x22 + } + } if len(m.Device) > 0 { i -= len(m.Device) copy(dAtA[i:], m.Device) @@ -656,6 +790,27 @@ func (m *RegisterRequest) Size() (n int) { return n } +func (m *ConnectRequestReferenceEntry) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Items) > 0 { + for k, v := range m.Items { + _ = k + _ = v + l = 0 + if len(v) > 0 { + l = 1 + len(v) + sovApi(uint64(len(v))) + } + mapEntrySize := 1 + len(k) + sovApi(uint64(len(k))) + l + n += mapEntrySize + 1 + sovApi(uint64(mapEntrySize)) + } + } + return n +} + func (m *ConnectRequest) Size() (n int) { if m == nil { return 0 @@ -674,6 +829,19 @@ func (m *ConnectRequest) Size() (n int) { if l > 0 { n += 1 + l + sovApi(uint64(l)) } + if len(m.References) > 0 { + for k, v := range m.References { + _ = k + _ = v + l = 0 + if v != nil { + l = v.Size() + l += 1 + sovApi(uint64(l)) + } + mapEntrySize := 1 + len(k) + sovApi(uint64(len(k))) + l + n += mapEntrySize + 1 + sovApi(uint64(mapEntrySize)) + } + } return n } @@ -717,14 +885,45 @@ func (this *RegisterRequest) String() string { }, "") return s } +func (this *ConnectRequestReferenceEntry) String() string { + if this == nil { + return "nil" + } + keysForItems := make([]string, 0, len(this.Items)) + for k, _ := range this.Items { + keysForItems = append(keysForItems, k) + } + github_com_gogo_protobuf_sortkeys.Strings(keysForItems) + mapStringForItems := "map[string][]byte{" + for _, k := range keysForItems { + mapStringForItems += fmt.Sprintf("%v: %v,", k, this.Items[k]) + } + mapStringForItems += "}" + s := strings.Join([]string{`&ConnectRequestReferenceEntry{`, + `Items:` + mapStringForItems + `,`, + `}`, + }, "") + return s +} func (this *ConnectRequest) String() string { if this == nil { return "nil" } + keysForReferences := make([]string, 0, len(this.References)) + for k, _ := range this.References { + keysForReferences = append(keysForReferences, k) + } + github_com_gogo_protobuf_sortkeys.Strings(keysForReferences) + mapStringForReferences := "map[string]*ConnectRequestReferenceEntry{" + for _, k := range keysForReferences { + mapStringForReferences += fmt.Sprintf("%v: %v,", k, this.References[k]) + } + mapStringForReferences += "}" s := strings.Join([]string{`&ConnectRequest{`, `Parameters:` + fmt.Sprintf("%v", this.Parameters) + `,`, `Model:` + strings.Replace(fmt.Sprintf("%v", this.Model), "TypeMeta", "v1.TypeMeta", 1) + `,`, `Device:` + fmt.Sprintf("%v", this.Device) + `,`, + `References:` + mapStringForReferences + `,`, `}`, }, "") return s @@ -949,6 +1148,187 @@ func (m *RegisterRequest) Unmarshal(dAtA []byte) error { } return nil } +func (m *ConnectRequestReferenceEntry) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowApi + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ConnectRequestReferenceEntry: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ConnectRequestReferenceEntry: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Items", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowApi + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthApi + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthApi + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Items == nil { + m.Items = make(map[string][]byte) + } + var mapkey string + mapvalue := []byte{} + for iNdEx < postIndex { + entryPreIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowApi + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + if fieldNum == 1 { + var stringLenmapkey uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowApi + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLenmapkey |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLenmapkey := int(stringLenmapkey) + if intStringLenmapkey < 0 { + return ErrInvalidLengthApi + } + postStringIndexmapkey := iNdEx + intStringLenmapkey + if postStringIndexmapkey < 0 { + return ErrInvalidLengthApi + } + if postStringIndexmapkey > l { + return io.ErrUnexpectedEOF + } + mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) + iNdEx = postStringIndexmapkey + } else if fieldNum == 2 { + var mapbyteLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowApi + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + mapbyteLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intMapbyteLen := int(mapbyteLen) + if intMapbyteLen < 0 { + return ErrInvalidLengthApi + } + postbytesIndex := iNdEx + intMapbyteLen + if postbytesIndex < 0 { + return ErrInvalidLengthApi + } + if postbytesIndex > l { + return io.ErrUnexpectedEOF + } + mapvalue = make([]byte, mapbyteLen) + copy(mapvalue, dAtA[iNdEx:postbytesIndex]) + iNdEx = postbytesIndex + } else { + iNdEx = entryPreIndex + skippy, err := skipApi(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthApi + } + if (iNdEx + skippy) > postIndex { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + m.Items[mapkey] = mapvalue + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipApi(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthApi + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthApi + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *ConnectRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -1082,6 +1462,135 @@ func (m *ConnectRequest) Unmarshal(dAtA []byte) error { m.Device = []byte{} } iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field References", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowApi + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthApi + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthApi + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.References == nil { + m.References = make(map[string]*ConnectRequestReferenceEntry) + } + var mapkey string + var mapvalue *ConnectRequestReferenceEntry + for iNdEx < postIndex { + entryPreIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowApi + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + if fieldNum == 1 { + var stringLenmapkey uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowApi + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLenmapkey |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLenmapkey := int(stringLenmapkey) + if intStringLenmapkey < 0 { + return ErrInvalidLengthApi + } + postStringIndexmapkey := iNdEx + intStringLenmapkey + if postStringIndexmapkey < 0 { + return ErrInvalidLengthApi + } + if postStringIndexmapkey > l { + return io.ErrUnexpectedEOF + } + mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) + iNdEx = postStringIndexmapkey + } else if fieldNum == 2 { + var mapmsglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowApi + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + mapmsglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if mapmsglen < 0 { + return ErrInvalidLengthApi + } + postmsgIndex := iNdEx + mapmsglen + if postmsgIndex < 0 { + return ErrInvalidLengthApi + } + if postmsgIndex > l { + return io.ErrUnexpectedEOF + } + mapvalue = &ConnectRequestReferenceEntry{} + if err := mapvalue.Unmarshal(dAtA[iNdEx:postmsgIndex]); err != nil { + return err + } + iNdEx = postmsgIndex + } else { + iNdEx = entryPreIndex + skippy, err := skipApi(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthApi + } + if (iNdEx + skippy) > postIndex { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + m.References[mapkey] = mapvalue + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipApi(dAtA[iNdEx:]) diff --git a/pkg/adaptor/api/v1alpha1/api.proto b/pkg/adaptor/api/v1alpha1/api.proto index 138e637d..b88f0302 100644 --- a/pkg/adaptor/api/v1alpha1/api.proto +++ b/pkg/adaptor/api/v1alpha1/api.proto @@ -24,7 +24,7 @@ service Registration { } message RegisterRequest { - // Name of the adaptor in the form `adaptor-vendor.com/adaptor-vendor`. + // Name of the adaptor in the form `adaptor-vendor.com/adaptor-name`. string name = 1; // Version of the API the adaptor was built against. string version = 2; @@ -38,13 +38,19 @@ service Connection { } } +message ConnectRequestReferenceEntry { + map items = 1; +} + message ConnectRequest { - // Parameters for the connection, it's in form JSON bytes. + // [Deprecated] Parameters for the connection, it's in form JSON bytes. bytes parameters = 1; // Model for the device. k8s.io.apimachinery.pkg.apis.meta.v1.TypeMeta model = 2; // Desired device, it's in form JSON bytes. bytes device = 3; + // References for the device, i.e: Secret, ConfigMap and Downward API. + map references = 4; } message ConnectResponse { diff --git a/pkg/limb/controller/devicelink.go b/pkg/limb/controller/devicelink.go index 32ba12ce..2674c19b 100644 --- a/pkg/limb/controller/devicelink.go +++ b/pkg/limb/controller/devicelink.go @@ -2,14 +2,18 @@ package controller import ( "context" + "fmt" "reflect" + "time" "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" apierrs "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" k8sruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -21,6 +25,7 @@ import ( "github.com/rancher/octopus/pkg/suctioncup" "github.com/rancher/octopus/pkg/util/collection" "github.com/rancher/octopus/pkg/util/converter" + "github.com/rancher/octopus/pkg/util/fieldpath" "github.com/rancher/octopus/pkg/util/model" "github.com/rancher/octopus/pkg/util/object" ) @@ -44,6 +49,8 @@ type DeviceLinkReconciler struct { // +kubebuilder:rbac:groups=edge.cattle.io,resources=devicelinks,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=edge.cattle.io,resources=devicelinks/status,verbs=get;update;patch // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch +// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch func (r *DeviceLinkReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { var ctx = context.Background() @@ -280,7 +287,14 @@ func (r *DeviceLinkReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) // this status changes maybe can drive by suction cup. return ctrl.Result{}, nil case metav1.ConditionTrue: - if err := r.SuctionCup.Send(&device, &link); err != nil { + // fetches the device references if needed + var references, err = r.fetchReferences(ctx, &link) + if err != nil { + log.Error(err, "Unable to fetch the reference parameters of DeviceLink") + return ctrl.Result{Requeue: true, RequeueAfter: 10 * time.Second}, nil + } + + if err := r.SuctionCup.Send(references, &device, &link); err != nil { devicelink.FailOnDeviceConnected(&link.Status, "cannot send data to adaptor") r.Eventf(&link, "Warning", "FailedSent", "cannot send data to adaptor: %v", err) @@ -328,6 +342,97 @@ func (r *DeviceLinkReconciler) SetupWithManager(ctrlMgr ctrl.Manager, suctionCup Complete(r) } +// fetchReferences fetches the references of deviceLink. +func (r *DeviceLinkReconciler) fetchReferences(ctx context.Context, deviceLink *edgev1alpha1.DeviceLink) (map[string]map[string][]byte, error) { + var references = deviceLink.Spec.References + var namespace = deviceLink.Namespace + + var referencesData map[string]map[string][]byte + if len(references) != 0 { + referencesData = make(map[string]map[string][]byte, len(references)) + + for _, rp := range references { + var name = rp.Name + + // fetches secret references + if rp.Secret != nil { + var desiredName = rp.Secret.Name + var desiredItems = rp.Secret.Items + + var secret corev1.Secret + if err := r.Get(ctx, types.NamespacedName{Namespace: namespace, Name: desiredName}, &secret); err != nil { + return nil, err + } + + var items = secret.Data + if len(desiredItems) != 0 { + items = make(map[string][]byte, len(desiredItems)) + for _, sk := range desiredItems { + var sv, exist = secret.Data[sk] + if !exist { + return nil, apierrs.NewNotFound(corev1.Resource(corev1.ResourceSecrets.String()), fmt.Sprintf("%s.data(%s)", desiredName, sk)) + } + items[sk] = sv + } + } + + referencesData[name] = items + continue + } + + // fetches configMap references + if rp.ConfigMap != nil { + var desiredName = rp.ConfigMap.Name + var desiredItems = rp.ConfigMap.Items + + var configMap corev1.ConfigMap + if err := r.Get(ctx, types.NamespacedName{Namespace: namespace, Name: desiredName}, &configMap); err != nil { + return nil, err + } + + var items map[string][]byte + if len(desiredItems) != 0 { + items = make(map[string][]byte, len(desiredItems)) + for _, cmk := range desiredItems { + var cmv, exist = configMap.Data[cmk] + if !exist { + return nil, apierrs.NewNotFound(corev1.Resource(corev1.ResourceConfigMaps.String()), fmt.Sprintf("%s.data(%s)", desiredName, cmk)) + } + items[cmk] = []byte(cmv) + } + } else { + items = make(map[string][]byte, len(configMap.Data)) + for cmk, cmv := range configMap.Data { + items[cmk] = []byte(cmv) + } + } + + referencesData[name] = items + continue + } + + // fetches downward API references + if rp.DownwardAPI != nil { + var desiredItems = rp.DownwardAPI.Items + + // the length of items should not be less than 1 + var items = make(map[string][]byte, len(desiredItems)) + for _, dk := range desiredItems { + var err error + items[dk.Name], err = fieldpath.ExtractDeviceLinkFieldPathAsBytes(deviceLink, dk.FieldRef.FieldPath) + if err != nil { + return nil, apierrs.NewNotFound(edgev1alpha1.GroupResourceDeviceLink, fmt.Sprintf("%s.downwardapi(%s)", deviceLink.Name, dk.FieldRef.FieldPath)) + } + } + + referencesData[name] = items + } + } + } + + return referencesData, nil +} + // isDeviceSpecChanged returns true if there is any changed from deviceLink's template and applies the changes into device. func isDeviceSpecChanged(deviceLink *edgev1alpha1.DeviceLink, device *unstructured.Unstructured) bool { var deviceTemplate = deviceLink.Spec.Template diff --git a/pkg/limb/limb.go b/pkg/limb/limb.go index a640ab2d..69465fb4 100644 --- a/pkg/limb/limb.go +++ b/pkg/limb/limb.go @@ -7,6 +7,7 @@ import ( "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "golang.org/x/sync/errgroup" + corev1 "k8s.io/api/core/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -100,7 +101,13 @@ func Run(name string, opts *options.Options) error { } func RegisterScheme(scheme *k8sruntime.Scheme) error { - return edgev1alpha1.AddToScheme(scheme) + if err := edgev1alpha1.AddToScheme(scheme); err != nil { + return err + } + if err := corev1.AddToScheme(scheme); err != nil { + return err + } + return nil } func RegisterMetrics(registry prometheus.Registerer) error { diff --git a/pkg/suctioncup/connection/connection.go b/pkg/suctioncup/connection/connection.go index caa31fbc..45c6dc74 100644 --- a/pkg/suctioncup/connection/connection.go +++ b/pkg/suctioncup/connection/connection.go @@ -18,8 +18,8 @@ type Connection interface { // GetName returns the name of connection GetName() types.NamespacedName - // Send sends the parameters, device model and desired data to connection - Send(parameters []byte, model *metav1.TypeMeta, device []byte) error + // Send sends the parameters, device model, desired data and references to connection + Send(parameters []byte, model *metav1.TypeMeta, device []byte, references map[string]*api.ConnectRequestReferenceEntry) error // Stop stops the connection Stop() error @@ -61,11 +61,12 @@ func (c *connection) Stop() error { return c.stop() } -func (c *connection) Send(parameters []byte, model *metav1.TypeMeta, device []byte) error { +func (c *connection) Send(parameters []byte, model *metav1.TypeMeta, device []byte, references map[string]*api.ConnectRequestReferenceEntry) error { return c.conn.Send(&api.ConnectRequest{ Parameters: parameters, Model: model, Device: device, + References: references, }) } diff --git a/pkg/suctioncup/neurons.go b/pkg/suctioncup/neurons.go index cc69c3e2..19f964e2 100644 --- a/pkg/suctioncup/neurons.go +++ b/pkg/suctioncup/neurons.go @@ -1,12 +1,11 @@ package suctioncup import ( - "time" - "github.com/pkg/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" edgev1alpha1 "github.com/rancher/octopus/api/v1alpha1" + api "github.com/rancher/octopus/pkg/adaptor/api/v1alpha1" "github.com/rancher/octopus/pkg/metrics" "github.com/rancher/octopus/pkg/util/object" ) @@ -65,21 +64,12 @@ func (m *manager) Disconnect(by *edgev1alpha1.DeviceLink) (exist bool) { return adaptor.DeleteConnection(name) } -func (m *manager) Send(data *unstructured.Unstructured, by *edgev1alpha1.DeviceLink) (berr error) { +func (m *manager) Send(referencesData map[string]map[string][]byte, device *unstructured.Unstructured, by *edgev1alpha1.DeviceLink) error { var adaptorName = by.Status.AdaptorName if adaptorName == "" { return errors.New("could not find blank name adaptor") } - // records metrics - var sendStartTS = time.Now() - defer func() { - metrics.GetLimbMetricsRecorder().ObserveSendLatency(adaptorName, time.Since(sendStartTS)) - if berr != nil { - metrics.GetLimbMetricsRecorder().IncreaseSendErrors(adaptorName) - } - }() - var adaptor = m.adaptors.Get(adaptorName) if adaptor == nil { return errors.Errorf("could not find adaptor %s", adaptorName) @@ -92,7 +82,7 @@ func (m *manager) Send(data *unstructured.Unstructured, by *edgev1alpha1.DeviceL } // NB(thxCode) the data should never be nil - var sendDevice, err = data.MarshalJSON() + var sendDevice, err = device.MarshalJSON() if err != nil { return errors.Wrapf(err, "could not marshal data as JSON") } @@ -101,6 +91,19 @@ func (m *manager) Send(data *unstructured.Unstructured, by *edgev1alpha1.DeviceL sendParameters = by.Spec.Adaptor.Parameters.Raw } var sendModel = &by.Status.Model + var sendReferences map[string]*api.ConnectRequestReferenceEntry + if len(referencesData) != 0 { + sendReferences = make(map[string]*api.ConnectRequestReferenceEntry, len(referencesData)) + for rpName, rp := range referencesData { + var reference = &api.ConnectRequestReferenceEntry{ + Items: make(map[string][]byte, len(rp)), + } + for ripName, rip := range rp { + reference.Items[ripName] = rip + } + sendReferences[rpName] = reference + } + } - return conn.Send(sendParameters, sendModel, sendDevice) + return conn.Send(sendParameters, sendModel, sendDevice, sendReferences) } diff --git a/pkg/suctioncup/types.go b/pkg/suctioncup/types.go index 6798550a..65a96d21 100644 --- a/pkg/suctioncup/types.go +++ b/pkg/suctioncup/types.go @@ -31,6 +31,6 @@ type Neurons interface { // Disconnect stops a connection by link, the return value represents whether there is a disconnected target. Disconnect(by *edgev1alpha1.DeviceLink) (exist bool) - // Send sends the data by link - Send(data *unstructured.Unstructured, by *edgev1alpha1.DeviceLink) error + // Send sends references and device by link + Send(referencesData map[string]map[string][]byte, device *unstructured.Unstructured, by *edgev1alpha1.DeviceLink) error } diff --git a/pkg/util/collection/string_map.go b/pkg/util/collection/string_map.go index 6a8c3baf..a9d515ef 100644 --- a/pkg/util/collection/string_map.go +++ b/pkg/util/collection/string_map.go @@ -1,5 +1,11 @@ package collection +import ( + "strings" + + "k8s.io/apimachinery/pkg/util/sets" +) + func StringMapCopy(source map[string]string) map[string]string { return StringMapCopyInto(source, make(map[string]string, len(source))) } @@ -28,3 +34,31 @@ func DiffStringMap(left, right map[string]string) bool { } return false } + +func FormatStringMap(m map[string]string, splitter string) string { + if splitter == "" { + splitter = "," + } + + var keySet = sets.NewString() + for k := range m { + keySet.Insert(k) + } + var keysLen = keySet.Len() + var keys = keySet.List() + + var builder strings.Builder + for _, k := range keys { + builder.WriteString(k) + builder.WriteString("=") + builder.WriteString(`"`) + builder.WriteString(m[k]) + builder.WriteString(`"`) + + if keysLen > 1 { + builder.WriteString(splitter) + } + keysLen-- + } + return builder.String() +} diff --git a/pkg/util/collection/string_map_test.go b/pkg/util/collection/string_map_test.go index 136cf68f..757e8e04 100644 --- a/pkg/util/collection/string_map_test.go +++ b/pkg/util/collection/string_map_test.go @@ -144,3 +144,44 @@ func TestDiffStringMap(t *testing.T) { } } } + +func TestFormatStringMap(t *testing.T) { + type given struct { + m map[string]string + splitter string + } + var testCases = []struct { + given given + expect string + }{ + { + given: given{ + m: map[string]string{ + "c": "d", + "x": "z", + "a": "b", + }, + splitter: "", + }, + expect: `a="b",c="d",x="z"`, + }, + { + given: given{ + m: map[string]string{ + "c": "d", + "x": "z", + "a": "b", + }, + splitter: ";", + }, + expect: `a="b";c="d";x="z"`, + }, + } + + for i, tc := range testCases { + var ret = FormatStringMap(tc.given.m, tc.given.splitter) + if ret != tc.expect { + t.Errorf("case %v: expected %v, got %v", i+1, tc.expect, ret) + } + } +} diff --git a/pkg/util/converter/bytes.go b/pkg/util/converter/bytes.go new file mode 100644 index 00000000..7f38c1a8 --- /dev/null +++ b/pkg/util/converter/bytes.go @@ -0,0 +1,19 @@ +package converter + +import ( + "reflect" + "unsafe" +) + +func UnsafeBytesToString(bs []byte) string { + return *(*string)(unsafe.Pointer(&bs)) +} + +func UnsafeStringToBytes(s string) (bytes []byte) { + var slice = (*reflect.SliceHeader)(unsafe.Pointer(&bytes)) + var str = (*reflect.StringHeader)(unsafe.Pointer(&s)) + slice.Len = str.Len + slice.Cap = str.Len + slice.Data = str.Data + return bytes +} diff --git a/pkg/util/converter/bytes_test.go b/pkg/util/converter/bytes_test.go new file mode 100644 index 00000000..a99f9fb5 --- /dev/null +++ b/pkg/util/converter/bytes_test.go @@ -0,0 +1,41 @@ +package converter + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUnsafeStringToBytes(t *testing.T) { + var testCases = []struct { + given string + expect []byte + }{ + { + given: "octopus", + expect: []byte("octopus"), + }, + } + + for i, tc := range testCases { + var ret = UnsafeStringToBytes(tc.given) + assert.Equal(t, tc.expect, ret, "case %v", i+1) + } +} + +func TestUnsafeBytesToString(t *testing.T) { + var testCases = []struct { + given []byte + expect string + }{ + { + given: []byte("octopus"), + expect: "octopus", + }, + } + + for i, tc := range testCases { + var ret = UnsafeBytesToString(tc.given) + assert.Equal(t, tc.expect, ret, "case %v", i+1) + } +} diff --git a/pkg/util/fieldpath/devicelink.go b/pkg/util/fieldpath/devicelink.go new file mode 100644 index 00000000..f856004b --- /dev/null +++ b/pkg/util/fieldpath/devicelink.go @@ -0,0 +1,34 @@ +package fieldpath + +import ( + "github.com/pkg/errors" + + edgev1alpha1 "github.com/rancher/octopus/api/v1alpha1" + "github.com/rancher/octopus/pkg/util/converter" +) + +// ExtractDeviceLinkFieldPathAsBytes is extracts the field from the given DeviceLink +// and returns it as a byte array. +func ExtractDeviceLinkFieldPathAsBytes(link *edgev1alpha1.DeviceLink, fieldPath string) ([]byte, error) { + if link == nil { + return nil, errors.New("link is nil") + } + + var status = link.Status + var str string + switch fieldPath { + case "status.nodeHostName": + str = status.NodeHostName + case "status.nodeInternalIP": + str = status.NodeInternalIP + case "status.nodeInternalDNS": + str = status.NodeInternalDNS + case "status.nodeExternalIP": + str = status.NodeExternalIP + case "status.nodeExternalDNS": + str = status.NodeExternalDNS + default: + return ExtractObjectFieldPathAsBytes(link, fieldPath) + } + return converter.UnsafeStringToBytes(str), nil +} diff --git a/pkg/util/fieldpath/devicelink_test.go b/pkg/util/fieldpath/devicelink_test.go new file mode 100644 index 00000000..c02cd7fe --- /dev/null +++ b/pkg/util/fieldpath/devicelink_test.go @@ -0,0 +1,121 @@ +package fieldpath + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + edgev1alpha1 "github.com/rancher/octopus/api/v1alpha1" +) + +func TestExtractDeviceLinkFieldPathAsBytes(t *testing.T) { + type given struct { + link *edgev1alpha1.DeviceLink + fieldPath string + } + type expect struct { + ret []byte + err error + } + + var targetObject = &edgev1alpha1.DeviceLink{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + UID: "uid1", + Annotations: map[string]string{ + "annotation-key-1": "v1", + "annotation-key-2": "v2", + }, + Labels: map[string]string{ + "lb1": "v1", + "lb2": "v2", + }, + }, + Status: edgev1alpha1.DeviceLinkStatus{ + NodeName: "edge-worker", + NodeHostName: "test-node-1", + NodeInternalIP: "192.168.1.34", + }, + } + + var testCases = []struct { + given given + expect expect + }{ + { + given: given{ + link: targetObject.DeepCopy(), + fieldPath: "metadata.annotations", + }, + expect: expect{ + ret: []byte(`annotation-key-1="v1";annotation-key-2="v2"`), + err: nil, + }, + }, + { + given: given{ + link: targetObject.DeepCopy(), + fieldPath: "metadata.name", + }, + expect: expect{ + ret: []byte(`test`), + err: nil, + }, + }, + { + given: given{ + link: targetObject.DeepCopy(), + fieldPath: "metadata.labels['lb3']", + }, + expect: expect{ + ret: nil, + err: nil, + }, + }, + { + given: given{ + link: targetObject.DeepCopy(), + fieldPath: `status.nodeName`, + }, + expect: expect{ + ret: nil, + err: errors.Errorf("unsupported fieldPath: status.nodeName"), + }, + }, + { + given: given{ + link: targetObject.DeepCopy(), + fieldPath: `status.nodeHostName`, + }, + expect: expect{ + ret: []byte(`test-node-1`), + err: nil, + }, + }, + { + given: given{ + link: targetObject.DeepCopy(), + fieldPath: `status.xxxx`, + }, + expect: expect{ + ret: nil, + err: errors.Errorf("unsupported fieldPath: status.xxxx"), + }, + }, + } + + for i, tc := range testCases { + var actualBytes, actualErr = ExtractDeviceLinkFieldPathAsBytes(tc.given.link, tc.given.fieldPath) + if actualErr != nil { + if tc.expect.err != nil { + assert.EqualError(t, actualErr, tc.expect.err.Error(), "case %v", i+1) + } else { + assert.NoError(t, actualErr, "case %v ", i+1) + } + } + assert.Equal(t, actualBytes, tc.expect.ret, "case %v", i+1) + } +} diff --git a/pkg/util/fieldpath/object.go b/pkg/util/fieldpath/object.go new file mode 100644 index 00000000..17b95005 --- /dev/null +++ b/pkg/util/fieldpath/object.go @@ -0,0 +1,84 @@ +package fieldpath + +// Borrowed from k8s.io/kubernetes/pkg/fieldpath/fieldpath.go + +import ( + "strings" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation" + + "github.com/rancher/octopus/pkg/util/collection" + "github.com/rancher/octopus/pkg/util/converter" +) + +// ExtractObjectFieldPathAsBytes is extracts the field from the given Object +// and returns it as a byte array. +func ExtractObjectFieldPathAsBytes(obj runtime.Object, fieldPath string) ([]byte, error) { + var accessor, err = meta.Accessor(obj) + if err != nil { + return nil, errors.Wrapf(err, "failed to access obj: %v", obj) + } + + var str string + switch fieldPath { + case "metadata.annotations": + str = collection.FormatStringMap(accessor.GetAnnotations(), ";") + case "metadata.labels": + str = collection.FormatStringMap(accessor.GetLabels(), ";") + case "metadata.name": + str = accessor.GetName() + case "metadata.namespace": + str = accessor.GetNamespace() + case "metadata.uid": + str = string(accessor.GetUID()) + default: + var path, subscript, ok = splitMaybeSubscriptedPath(fieldPath) + if !ok { + return nil, errors.Errorf("unsupported fieldPath: %s", fieldPath) + } + switch path { + case "metadata.annotations": + if errs := validation.IsQualifiedName(strings.ToLower(subscript)); len(errs) != 0 { + return nil, errors.Errorf("invalid key subscript in %s: %s", fieldPath, strings.Join(errs, ";")) + } + str = accessor.GetAnnotations()[subscript] + case "metadata.labels": + if errs := validation.IsQualifiedName(subscript); len(errs) != 0 { + return nil, errors.Errorf("invalid key subscript in %s: %s", fieldPath, strings.Join(errs, ";")) + } + str = accessor.GetLabels()[subscript] + default: + return nil, errors.Errorf("fieldPath %s doesn't support subscript", fieldPath) + } + } + return converter.UnsafeStringToBytes(str), nil +} + +// splitMaybeSubscriptedPath checks whether the specified fieldPath is +// subscripted, and +// - if yes, this function splits the fieldPath into path and subscript, and +// returns (path, subscript, true). +// - if no, this function returns (fieldPath, "", false). +// +// Example inputs and outputs: +// - "metadata.annotations['myKey']" --> ("metadata.annotations", "myKey", true) +// - "metadata.annotations['a[b]c']" --> ("metadata.annotations", "a[b]c", true) +// - "metadata.labels['']" --> ("metadata.labels", "", true) +// - "metadata.labels" --> ("metadata.labels", "", false) +func splitMaybeSubscriptedPath(fieldPath string) (string, string, bool) { + if !strings.HasSuffix(fieldPath, "']") { + return fieldPath, "", false + } + s := strings.TrimSuffix(fieldPath, "']") + parts := strings.SplitN(s, "['", 2) + if len(parts) < 2 { + return fieldPath, "", false + } + if len(parts[0]) == 0 { + return fieldPath, "", false + } + return parts[0], parts[1], true +} diff --git a/pkg/util/fieldpath/object_test.go b/pkg/util/fieldpath/object_test.go new file mode 100644 index 00000000..079ff498 --- /dev/null +++ b/pkg/util/fieldpath/object_test.go @@ -0,0 +1,112 @@ +package fieldpath + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + edgev1alpha1 "github.com/rancher/octopus/api/v1alpha1" +) + +func TestExtractObjectFieldPathAsBytes(t *testing.T) { + type given struct { + obj runtime.Object + fieldPath string + } + type expect struct { + ret []byte + err error + } + + var targetObject = &edgev1alpha1.DeviceLink{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + UID: "uid1", + Annotations: map[string]string{ + "annotation-key-1": "v1", + "annotation-key-2": "v2", + }, + Labels: map[string]string{ + "lb1": "v1", + "lb2": "v2", + }, + }, + Status: edgev1alpha1.DeviceLinkStatus{ + NodeName: "edge-worker", + NodeHostName: "test-node-1", + NodeInternalIP: "192.168.1.34", + }, + } + + var testCases = []struct { + given given + expect expect + }{ + { + given: given{ + obj: targetObject.DeepCopy(), + fieldPath: "metadata.labels", + }, + expect: expect{ + ret: []byte(`lb1="v1";lb2="v2"`), + err: nil, + }, + }, + { + given: given{ + obj: targetObject.DeepCopy(), + fieldPath: `metadata.annotations['annotation-key-1']`, + }, + expect: expect{ + ret: []byte(`v1`), + err: nil, + }, + }, + { + given: given{ + obj: targetObject.DeepCopy(), + fieldPath: `metadata.namespace`, + }, + expect: expect{ + ret: []byte(`default`), + err: nil, + }, + }, + { + given: given{ + obj: targetObject.DeepCopy(), + fieldPath: `metadata.annotations['annotation-key-3']`, + }, + expect: expect{ + ret: nil, + err: nil, + }, + }, + { + given: given{ + obj: targetObject.DeepCopy(), + fieldPath: `status.nodeName`, + }, + expect: expect{ + ret: nil, + err: errors.Errorf("unsupported fieldPath: status.nodeName"), + }, + }, + } + + for i, tc := range testCases { + var actualBytes, actualErr = ExtractObjectFieldPathAsBytes(tc.given.obj, tc.given.fieldPath) + if actualErr != nil { + if tc.expect.err != nil { + assert.EqualError(t, actualErr, tc.expect.err.Error(), "case %v", i+1) + } else { + assert.NoError(t, actualErr, "case %v ", i+1) + } + } + assert.Equal(t, actualBytes, tc.expect.ret, "case %v", i+1) + } +} diff --git a/test/integration/limb/devicelink_controller_test.go b/test/integration/limb/devicelink_controller_test.go index d509b238..9a45ef8a 100644 --- a/test/integration/limb/devicelink_controller_test.go +++ b/test/integration/limb/devicelink_controller_test.go @@ -10,6 +10,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" edgev1alpha1 "github.com/rancher/octopus/api/v1alpha1" + api "github.com/rancher/octopus/pkg/adaptor/api/v1alpha1" status "github.com/rancher/octopus/pkg/status/devicelink" "github.com/rancher/octopus/pkg/suctioncup/connection" "github.com/rancher/octopus/pkg/util/model" @@ -274,6 +275,6 @@ func (c fakeDummyConnection) Stop() error { return nil } -func (c fakeDummyConnection) Send(parameters []byte, model *metav1.TypeMeta, device []byte) error { +func (c fakeDummyConnection) Send([]byte, *metav1.TypeMeta, []byte, map[string]*api.ConnectRequestReferenceEntry) error { return nil } diff --git a/test/util/content/raw_extension.go b/test/util/content/raw_extension.go index 8d22e00e..3bb4f816 100644 --- a/test/util/content/raw_extension.go +++ b/test/util/content/raw_extension.go @@ -1,8 +1,9 @@ package content import ( - jsoniter "github.com/json-iterator/go" "k8s.io/apimachinery/pkg/runtime" + + "github.com/rancher/octopus/pkg/util/converter" ) func ToRawExtension(content interface{}) *runtime.RawExtension { @@ -15,7 +16,6 @@ func ToRawExtension(content interface{}) *runtime.RawExtension { case string: return &runtime.RawExtension{Raw: []byte(t)} default: - bs, _ := jsoniter.Marshal(content) - return &runtime.RawExtension{Raw: bs} + return &runtime.RawExtension{Raw: converter.TryMarshalJSON(content)} } }