diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index eb2b54867c6..31dc32bb5d2 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - target: [ yurthub, node-servant, yurt-manager ] + target: [ yurthub, node-servant, yurt-manager, yurt-iot-dock ] steps: - uses: actions/checkout@v3 with: diff --git a/Makefile b/Makefile index 49fe17030f4..fefb7fe0ca0 100644 --- a/Makefile +++ b/Makefile @@ -171,6 +171,8 @@ docker-push-yurt-tunnel-server: docker-buildx-builder docker-push-yurt-tunnel-agent: docker-buildx-builder docker buildx build --no-cache --push ${DOCKER_BUILD_ARGS} --platform ${TARGET_PLATFORMS} -f hack/dockerfiles/release/Dockerfile.yurt-tunnel-agent . -t ${IMAGE_REPO}/yurt-tunnel-agent:${GIT_VERSION} +docker-push-yurt-iot-dock: docker-buildx-builder + docker buildx build --no-cache --push ${DOCKER_BUILD_ARGS} --platform ${TARGET_PLATFORMS} -f hack/dockerfiles/release/Dockerfile.yurt-iot-dock . -t ${IMAGE_REPO}/yurt-iot-dock:${GIT_VERSION} generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. # hack/make-rule/generate_openapi.sh // TODO by kadisi diff --git a/charts/yurt-manager/crds/iot.openyurt.io_deviceprofiles.yaml b/charts/yurt-manager/crds/iot.openyurt.io_deviceprofiles.yaml new file mode 100644 index 00000000000..c1d92ed7a8e --- /dev/null +++ b/charts/yurt-manager/crds/iot.openyurt.io_deviceprofiles.yaml @@ -0,0 +1,172 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + name: deviceprofiles.iot.openyurt.io +spec: + group: iot.openyurt.io + names: + kind: DeviceProfile + listKind: DeviceProfileList + plural: deviceprofiles + shortNames: + - dp + singular: deviceprofile + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The nodepool of deviceProfile + jsonPath: .spec.nodePool + name: NODEPOOL + type: string + - description: The synced status of deviceProfile + jsonPath: .status.synced + name: SYNCED + type: boolean + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: DeviceProfile represents the attributes and operational capabilities + of a device. It is a template for which there can be multiple matching devices + within a given system. NOTE This struct is derived from edgex/go-mod-core-contracts/models/deviceprofile.go + 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: DeviceProfileSpec defines the desired state of DeviceProfile + properties: + description: + type: string + deviceCommands: + items: + properties: + isHidden: + type: boolean + name: + type: string + readWrite: + type: string + resourceOperations: + items: + properties: + defaultValue: + type: string + deviceResource: + type: string + mappings: + additionalProperties: + type: string + type: object + required: + - defaultValue + type: object + type: array + required: + - isHidden + - name + - readWrite + - resourceOperations + type: object + type: array + deviceResources: + items: + properties: + attributes: + additionalProperties: + type: string + type: object + description: + type: string + isHidden: + type: boolean + name: + type: string + properties: + properties: + assertion: + type: string + base: + type: string + defaultValue: + type: string + mask: + type: string + maximum: + type: string + mediaType: + type: string + minimum: + type: string + offset: + type: string + readWrite: + type: string + scale: + type: string + shift: + type: string + units: + type: string + valueType: + type: string + type: object + tag: + type: string + required: + - description + - isHidden + - name + - properties + type: object + type: array + labels: + description: Labels used to search for groups of profiles on EdgeX + Foundry + items: + type: string + type: array + manufacturer: + description: Manufacturer of the device + type: string + model: + description: Model of the device + type: string + nodePool: + description: NodePool specifies which nodePool the deviceProfile belongs + to + type: string + type: object + status: + description: DeviceProfileStatus defines the observed state of DeviceProfile + properties: + id: + type: string + synced: + type: boolean + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/charts/yurt-manager/crds/iot.openyurt.io_devices.yaml b/charts/yurt-manager/crds/iot.openyurt.io_devices.yaml new file mode 100644 index 00000000000..a284a0f5b8f --- /dev/null +++ b/charts/yurt-manager/crds/iot.openyurt.io_devices.yaml @@ -0,0 +1,195 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + name: devices.iot.openyurt.io +spec: + group: iot.openyurt.io + names: + kind: Device + listKind: DeviceList + plural: devices + shortNames: + - dev + singular: device + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The nodepool of device + jsonPath: .spec.nodePool + name: NODEPOOL + type: string + - description: The synced status of device + jsonPath: .status.synced + name: SYNCED + type: boolean + - description: The managed status of device + jsonPath: .spec.managed + name: MANAGED + priority: 1 + type: boolean + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Device is the Schema for the devices 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: DeviceSpec defines the desired state of Device + properties: + adminState: + description: Admin state (locked/unlocked) + type: string + description: + description: Information describing the device + type: string + deviceProperties: + additionalProperties: + properties: + desiredValue: + type: string + name: + type: string + putURL: + type: string + required: + - desiredValue + - name + type: object + description: TODO support the following field A list of auto-generated + events coming from the device AutoEvents []AutoEvent `json:"autoEvents"` + DeviceProperties represents the expected state of the device's properties + type: object + labels: + description: Other labels applied to the device to help with searching + items: + type: string + type: array + location: + description: 'Device service specific location (interface{} is an + empty interface so it can be anything) TODO: location type in edgex + is interface{}' + type: string + managed: + description: True means device is managed by cloud, cloud can update + the related fields False means cloud can't update the fields + type: boolean + nodePool: + description: NodePool indicates which nodePool the device comes from + type: string + notify: + type: boolean + operatingState: + description: Operating state (enabled/disabled) + type: string + profileName: + description: Associated Device Profile - Describes the device + type: string + protocols: + additionalProperties: + additionalProperties: + type: string + type: object + description: A map of supported protocols for the given device + type: object + serviceName: + description: Associated Device Service - One per device + type: string + required: + - notify + - profileName + - serviceName + type: object + status: + description: DeviceStatus defines the observed state of Device + properties: + adminState: + description: Admin state (locked/unlocked) + type: string + conditions: + description: current device state + items: + description: DeviceCondition describes current state of a Device. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of in place set condition. + type: string + type: object + type: array + deviceProperties: + additionalProperties: + properties: + actualValue: + type: string + getURL: + type: string + name: + type: string + required: + - actualValue + - name + type: object + description: it represents the actual state of the device's properties + type: object + edgeId: + type: string + lastConnected: + description: Time (milliseconds) that the device last provided any + feedback or responded to any request + format: int64 + type: integer + lastReported: + description: Time (milliseconds) that the device reported data to + the core microservice + format: int64 + type: integer + operatingState: + description: Operating state (up/down/unknown) + type: string + synced: + description: Synced indicates whether the device already exists on + both OpenYurt and edge platform + type: boolean + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/charts/yurt-manager/crds/iot.openyurt.io_deviceservices.yaml b/charts/yurt-manager/crds/iot.openyurt.io_deviceservices.yaml new file mode 100644 index 00000000000..d4722a55546 --- /dev/null +++ b/charts/yurt-manager/crds/iot.openyurt.io_deviceservices.yaml @@ -0,0 +1,141 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + name: deviceservices.iot.openyurt.io +spec: + group: iot.openyurt.io + names: + kind: DeviceService + listKind: DeviceServiceList + plural: deviceservices + shortNames: + - dsvc + singular: deviceservice + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The nodepool of deviceService + jsonPath: .spec.nodePool + name: NODEPOOL + type: string + - description: The synced status of deviceService + jsonPath: .status.synced + name: SYNCED + type: boolean + - description: The managed status of deviceService + jsonPath: .spec.managed + name: MANAGED + priority: 1 + type: boolean + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: DeviceService is the Schema for the deviceservices 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: DeviceServiceSpec defines the desired state of DeviceService + properties: + adminState: + description: Device Service Admin State + type: string + baseAddress: + type: string + description: + description: Information describing the device + type: string + labels: + description: tags or other labels applied to the device service for + search or other identification needs on the EdgeX Foundry + items: + type: string + type: array + managed: + description: True means deviceService is managed by cloud, cloud can + update the related fields False means cloud can't update the fields + type: boolean + nodePool: + description: NodePool indicates which nodePool the deviceService comes + from + type: string + required: + - baseAddress + type: object + status: + description: DeviceServiceStatus defines the observed state of DeviceService + properties: + adminState: + description: Device Service Admin State + type: string + conditions: + description: current deviceService state + items: + description: DeviceServiceCondition describes current state of a + Device. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of in place set condition. + type: string + type: object + type: array + edgeId: + description: the Id assigned by the edge platform + type: string + lastConnected: + description: time in milliseconds that the device last reported data + to the core + format: int64 + type: integer + lastReported: + description: time in milliseconds that the device last reported data + to the core + format: int64 + type: integer + synced: + description: Synced indicates whether the device already exists on + both OpenYurt and edge platform + type: boolean + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml index 3dda7a3d1b4..72e35edad21 100644 --- a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml +++ b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml @@ -382,6 +382,84 @@ rules: - list - patch - watch +- apiGroups: + - iot.openyurt.io + resources: + - deviceprofiles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - iot.openyurt.io + resources: + - deviceprofiles/finalizers + verbs: + - update +- apiGroups: + - iot.openyurt.io + resources: + - deviceprofiles/status + verbs: + - get + - patch + - update +- apiGroups: + - iot.openyurt.io + resources: + - devices + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - iot.openyurt.io + resources: + - devices/finalizers + verbs: + - update +- apiGroups: + - iot.openyurt.io + resources: + - devices/status + verbs: + - get + - patch + - update +- apiGroups: + - iot.openyurt.io + resources: + - deviceservices + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - iot.openyurt.io + resources: + - deviceservices/finalizers + verbs: + - update +- apiGroups: + - iot.openyurt.io + resources: + - deviceservices/status + verbs: + - get + - patch + - update - apiGroups: - iot.openyurt.io resources: diff --git a/charts/yurt-manager/templates/yurt-manager-iot.yaml b/charts/yurt-manager/templates/yurt-manager-iot.yaml new file mode 100644 index 00000000000..ec415db5a72 --- /dev/null +++ b/charts/yurt-manager/templates/yurt-manager-iot.yaml @@ -0,0 +1,88 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: yurt-iot-dock +rules: + - apiGroups: + - "" + resources: + - events + verbs: + - get + - apiGroups: + - iot.openyurt.io + resources: + - devices + - deviceservices + - deviceprofiles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - iot.openyurt.io + resources: + - devices/status + - deviceprofiles/status + - deviceservices/status + verbs: + - get + - patch + - update + - apiGroups: + - iot.openyurt.io + resources: + - devices/finalizers + - deviceprofiles/finalizers + - deviceservices/finalizers + verbs: + - update + - apiGroups: + - apps.openyurt.io + resources: + - nodepools + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - nodes + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: yurt-iot-dock +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: yurt-iot-dock +subjects: + - apiGroup: rbac.authorization.k8s.io + kind: Group + name: system:authenticated \ No newline at end of file diff --git a/cmd/yurt-iot-dock/app/core.go b/cmd/yurt-iot-dock/app/core.go new file mode 100644 index 00000000000..39e30bcaf30 --- /dev/null +++ b/cmd/yurt-iot-dock/app/core.go @@ -0,0 +1,199 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package app + +import ( + "context" + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/klog/v2" + "k8s.io/klog/v2/klogr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + + "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" + "github.com/openyurtio/openyurt/pkg/apis" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + _ = clientgoscheme.AddToScheme(scheme) + + _ = apis.AddToScheme(clientgoscheme.Scheme) + _ = apis.AddToScheme(scheme) + + // +kubebuilder:scaffold:scheme +} + +func NewCmdYurtIoTDock(stopCh <-chan struct{}) *cobra.Command { + yurtIoTDockOptions := options.NewYurtIoTDockOptions() + cmd := &cobra.Command{ + Use: "yurt-iot-dock", + Short: "Launch yurt-iot-dock", + Long: "Launch yurt-iot-dock", + Run: func(cmd *cobra.Command, args []string) { + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + klog.V(1).Infof("FLAG: --%s=%q", flag.Name, flag.Value) + }) + if err := options.ValidateOptions(yurtIoTDockOptions); err != nil { + klog.Fatalf("validate options: %v", err) + } + Run(yurtIoTDockOptions, stopCh) + }, + } + + yurtIoTDockOptions.AddFlags(cmd.Flags()) + return cmd +} + +func Run(opts *options.YurtIoTDockOptions, stopCh <-chan struct{}) { + ctrl.SetLogger(klogr.New()) + cfg := ctrl.GetConfigOrDie() + + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + MetricsBindAddress: opts.MetricsAddr, + HealthProbeBindAddress: opts.ProbeAddr, + LeaderElection: opts.EnableLeaderElection, + LeaderElectionID: "yurt-iot-dock", + Namespace: opts.Namespace, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + // perform preflight check + setupLog.Info("[preflight] Running pre-flight checks") + if err := preflightCheck(mgr, opts); err != nil { + setupLog.Error(err, "failed to run pre-flight checks") + os.Exit(1) + } + + // register the field indexers + setupLog.Info("[preflight] Registering the field indexers") + if err := util.RegisterFieldIndexers(mgr.GetFieldIndexer()); err != nil { + setupLog.Error(err, "failed to register field indexers") + os.Exit(1) + } + + // get nodepool where yurt-iot-dock run + if opts.Nodepool == "" { + opts.Nodepool, err = util.GetNodePool(mgr.GetConfig()) + if err != nil { + setupLog.Error(err, "failed to get the nodepool where yurt-iot-dock run") + os.Exit(1) + } + } + + // setup the DeviceProfile Reconciler and Syncer + if err = (&controllers.DeviceProfileReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr, opts); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "DeviceProfile") + os.Exit(1) + } + dfs, err := controllers.NewDeviceProfileSyncer(mgr.GetClient(), opts) + if err != nil { + setupLog.Error(err, "unable to create syncer", "syncer", "DeviceProfile") + os.Exit(1) + } + err = mgr.Add(dfs.NewDeviceProfileSyncerRunnable()) + if err != nil { + setupLog.Error(err, "unable to create syncer runnable", "syncer", "DeviceProfile") + os.Exit(1) + } + + // setup the Device Reconciler and Syncer + if err = (&controllers.DeviceReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr, opts); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Device") + os.Exit(1) + } + ds, err := controllers.NewDeviceSyncer(mgr.GetClient(), opts) + if err != nil { + setupLog.Error(err, "unable to create syncer", "controller", "Device") + os.Exit(1) + } + err = mgr.Add(ds.NewDeviceSyncerRunnable()) + if err != nil { + setupLog.Error(err, "unable to create syncer runnable", "syncer", "Device") + os.Exit(1) + } + + // setup the DeviceService Reconciler and Syncer + if err = (&controllers.DeviceServiceReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr, opts); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "DeviceService") + os.Exit(1) + } + dss, err := controllers.NewDeviceServiceSyncer(mgr.GetClient(), opts) + if err != nil { + setupLog.Error(err, "unable to create syncer", "syncer", "DeviceService") + os.Exit(1) + } + err = mgr.Add(dss.NewDeviceServiceSyncerRunnable()) + if err != nil { + setupLog.Error(err, "unable to create syncer runnable", "syncer", "DeviceService") + os.Exit(1) + } + //+kubebuilder:scaffold:builder + + if err := mgr.AddHealthzCheck("health", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("check", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("[run controllers] Starting manager, acting on " + fmt.Sprintf("[NodePool: %s, Namespace: %s]", opts.Nodepool, opts.Namespace)) + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "failed to running manager") + os.Exit(1) + } +} + +func preflightCheck(mgr ctrl.Manager, opts *options.YurtIoTDockOptions) error { + client, err := kubernetes.NewForConfig(mgr.GetConfig()) + if err != nil { + return err + } + if _, err := client.CoreV1().Namespaces().Get(context.TODO(), opts.Namespace, metav1.GetOptions{}); err != nil { + return err + } + return nil +} diff --git a/cmd/yurt-iot-dock/app/options/options.go b/cmd/yurt-iot-dock/app/options/options.go new file mode 100644 index 00000000000..08d9e37a920 --- /dev/null +++ b/cmd/yurt-iot-dock/app/options/options.go @@ -0,0 +1,82 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package options + +import ( + "fmt" + "net" + + "github.com/spf13/pflag" +) + +// YurtIoTDockOptions is the main settings for the yurt-iot-dock +type YurtIoTDockOptions struct { + MetricsAddr string + ProbeAddr string + EnableLeaderElection bool + Nodepool string + Namespace string + CoreDataAddr string + CoreMetadataAddr string + CoreCommandAddr string + EdgeSyncPeriod uint +} + +func NewYurtIoTDockOptions() *YurtIoTDockOptions { + return &YurtIoTDockOptions{ + MetricsAddr: ":8080", + ProbeAddr: ":8080", + EnableLeaderElection: false, + Nodepool: "", + Namespace: "default", + CoreDataAddr: "edgex-core-data:59880", + CoreMetadataAddr: "edgex-core-metadata:59881", + CoreCommandAddr: "edgex-core-command:59882", + EdgeSyncPeriod: 5, + } +} + +func ValidateOptions(options *YurtIoTDockOptions) error { + if err := ValidateEdgePlatformAddress(options); err != nil { + return err + } + return nil +} + +func (o *YurtIoTDockOptions) AddFlags(fs *pflag.FlagSet) { + fs.StringVar(&o.MetricsAddr, "metrics-bind-address", o.MetricsAddr, "The address the metric endpoint binds to.") + fs.StringVar(&o.ProbeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + fs.BoolVar(&o.EnableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+"Enabling this will ensure there is only one active controller manager.") + fs.StringVar(&o.Nodepool, "nodepool", "", "The nodePool deviceController is deployed in.(just for debugging)") + fs.StringVar(&o.Namespace, "namespace", "default", "The cluster namespace for edge resources synchronization.") + fs.StringVar(&o.CoreDataAddr, "core-data-address", "edgex-core-data:59880", "The address of edge core-data service.") + fs.StringVar(&o.CoreMetadataAddr, "core-metadata-address", "edgex-core-metadata:59881", "The address of edge core-metadata service.") + fs.StringVar(&o.CoreCommandAddr, "core-command-address", "edgex-core-command:59882", "The address of edge core-command service.") + fs.UintVar(&o.EdgeSyncPeriod, "edge-sync-period", 5, "The period of the device management platform synchronizing the device status to the cloud.(in seconds,not less than 5 seconds)") +} + +func ValidateEdgePlatformAddress(options *YurtIoTDockOptions) error { + addrs := []string{options.CoreDataAddr, options.CoreMetadataAddr, options.CoreCommandAddr} + for _, addr := range addrs { + if addr != "" { + if _, _, err := net.SplitHostPort(addr); err != nil { + return fmt.Errorf("invalid address: %s", err) + } + } + } + return nil +} diff --git a/cmd/yurt-iot-dock/yurt-iot-dock.go b/cmd/yurt-iot-dock/yurt-iot-dock.go new file mode 100644 index 00000000000..c647e5241c0 --- /dev/null +++ b/cmd/yurt-iot-dock/yurt-iot-dock.go @@ -0,0 +1,43 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "math/rand" + "time" + + "k8s.io/apimachinery/pkg/util/wait" + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" + "k8s.io/klog/v2" + + "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app" +) + +func main() { + rand.Seed(time.Now().UnixNano()) + klog.InitFlags(nil) + defer klog.Flush() + + cmd := app.NewCmdYurtIoTDock(wait.NeverStop) + cmd.Flags().AddGoFlagSet(flag.CommandLine) + if err := cmd.Execute(); err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod index f79da3cf042..53dcd2e2ad0 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,13 @@ go 1.18 require ( github.com/aliyun/alibaba-cloud-sdk-go v1.62.156 github.com/davecgh/go-spew v1.1.1 + github.com/edgexfoundry/go-mod-core-contracts/v2 v2.3.0 + github.com/go-resty/resty/v2 v2.4.0 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 github.com/hashicorp/go-version v1.6.0 + github.com/jarcoal/httpmock v1.3.0 github.com/onsi/ginkgo/v2 v2.1.4 github.com/onsi/gomega v1.19.0 github.com/opencontainers/selinux v1.11.0 @@ -24,6 +27,7 @@ require ( go.etcd.io/etcd/api/v3 v3.5.0 go.etcd.io/etcd/client/pkg/v3 v3.5.0 go.etcd.io/etcd/client/v3 v3.5.0 + golang.org/x/net v0.9.0 golang.org/x/sys v0.10.0 google.golang.org/grpc v1.56.2 gopkg.in/cheggaaa/pb.v1 v1.0.28 @@ -86,10 +90,14 @@ require ( github.com/felixge/httpsnoop v1.0.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fvbommel/sortorder v1.0.1 // indirect + github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-logr/logr v0.4.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/validator/v10 v10.11.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect @@ -103,6 +111,7 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/leodido/go-urn v1.2.1 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.7 // indirect @@ -124,6 +133,7 @@ require ( github.com/spf13/afero v1.6.0 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect + github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/contrib v0.20.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0 // indirect @@ -139,7 +149,6 @@ require ( go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.5.0 // indirect - golang.org/x/net v0.9.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/term v0.7.0 // indirect diff --git a/go.sum b/go.sum index 4c71d04330b..c77bd68f7d3 100644 --- a/go.sum +++ b/go.sum @@ -175,6 +175,8 @@ github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/edgexfoundry/go-mod-core-contracts/v2 v2.3.0 h1:8Svk1HTehXEgwxgyA4muVhSkP3D9n1q+oSHI3B1Ac90= +github.com/edgexfoundry/go-mod-core-contracts/v2 v2.3.0/go.mod h1:4/e61acxVkhQWCTjQ4XcHVJDnrMDloFsZZB1B6STCRw= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -214,6 +216,8 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE= github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= +github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -245,6 +249,16 @@ github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfT github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-ozzo/ozzo-validation v3.5.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= +github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-resty/resty/v2 v2.4.0 h1:s6TItTLejEI+2mn98oijC5w/Rk2YU+OA6x0mnZN6r6k= +github.com/go-resty/resty/v2 v2.4.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -385,6 +399,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ishidawataru/sctp v0.0.0-20190723014705-7c296d48a2b5/go.mod h1:DM4VvS+hD/kDi1U1QsX2fnZowwBhqD0Dk3bRPKF/Oc8= +github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= +github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -422,6 +438,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -429,6 +446,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kubernetes/kubernetes v1.22.3 h1:0gYnqsr5nZiAO+iDkEU7RJ6Ne2CMyoinJXVm5qVSTiE= github.com/kubernetes/kubernetes v1.22.3/go.mod h1:Snea7fgIObGgHmLbUJ3OgjGEr5bjj16iEdp5oHS6eS8= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/libopenstorage/openstorage v1.0.0/go.mod h1:Sp1sIObHjat1BeXhfMqLZ14wnOzEhNx2YQedreMcUyc= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= @@ -452,6 +471,7 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mindprince/gonvml v0.0.0-20190828220739-9ebdce4bb989/go.mod h1:2eu9pRWp8mo84xCg6KswZ+USQHjwgRhNp06sozOdsTY= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= @@ -543,6 +563,7 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -591,6 +612,8 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rubiojr/go-vhd v0.0.0-20200706105327-02e210299021/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= @@ -676,6 +699,8 @@ github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3C github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca h1:1CFlNzQhALwjS9mBAUkycX616GzgsuYUOCHA5+HSlXI= @@ -756,6 +781,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -837,6 +863,7 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -914,8 +941,10 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -936,6 +965,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1086,6 +1116,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= diff --git a/hack/dockerfiles/build/Dockerfile.yurt-iot-dock b/hack/dockerfiles/build/Dockerfile.yurt-iot-dock new file mode 100644 index 00000000000..3e684bb6866 --- /dev/null +++ b/hack/dockerfiles/build/Dockerfile.yurt-iot-dock @@ -0,0 +1,8 @@ +# multi-arch image building for yurt-iot-dock + +FROM --platform=${TARGETPLATFORM} alpine:3.17 +ARG TARGETOS TARGETARCH MIRROR_REPO +RUN if [ ! -z "${MIRROR_REPO+x}" ]; then sed -i "s/dl-cdn.alpinelinux.org/${MIRROR_REPO}/g" /etc/apk/repositories; fi && \ + apk add ca-certificates bash libc6-compat iptables ip6tables && update-ca-certificates && rm /var/cache/apk/* +COPY ./_output/local/bin/${TARGETOS}/${TARGETARCH}/yurt-iot-dock /usr/local/bin/yurt-iot-dock +ENTRYPOINT ["/usr/local/bin/yurt-iot-dock"] \ No newline at end of file diff --git a/hack/dockerfiles/release/Dockerfile.yurt-iot-dock b/hack/dockerfiles/release/Dockerfile.yurt-iot-dock new file mode 100644 index 00000000000..d73a35cb790 --- /dev/null +++ b/hack/dockerfiles/release/Dockerfile.yurt-iot-dock @@ -0,0 +1,14 @@ +# multi-arch image building for yurt-iot-dock + +FROM --platform=${BUILDPLATFORM} golang:1.18 as builder +ADD . /build +ARG TARGETOS TARGETARCH GIT_VERSION GOPROXY MIRROR_REPO +WORKDIR /build/ +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} GIT_VERSION=${GIT_VERSION} make build WHAT=cmd/yurt-iot-dock + +FROM --platform=${TARGETPLATFORM} alpine:3.17 +ARG TARGETOS TARGETARCH MIRROR_REPO +RUN if [ ! -z "${MIRROR_REPO+x}" ]; then sed -i "s/dl-cdn.alpinelinux.org/${MIRROR_REPO}/g" /etc/apk/repositories; fi && \ + apk add ca-certificates bash libc6-compat iptables ip6tables && update-ca-certificates && rm /var/cache/apk/* +COPY --from=builder /build/_output/local/bin/${TARGETOS}/${TARGETARCH}/yurt-iot-dock /usr/local/bin/yurt-iot-dock +ENTRYPOINT ["/usr/local/bin/yurt-iot-dock"] \ No newline at end of file diff --git a/hack/make-rules/build.sh b/hack/make-rules/build.sh index 1422dd5df7d..5e28026b658 100755 --- a/hack/make-rules/build.sh +++ b/hack/make-rules/build.sh @@ -25,6 +25,7 @@ readonly YURT_ALL_TARGETS=( yurt-node-servant yurthub yurt-manager + yurt-iot-dock ) # clean old binaries at GOOS and GOARCH diff --git a/hack/make-rules/image_build.sh b/hack/make-rules/image_build.sh index b069cfcbc0b..1f4afa71d07 100755 --- a/hack/make-rules/image_build.sh +++ b/hack/make-rules/image_build.sh @@ -25,6 +25,7 @@ readonly IMAGE_TARGETS=( yurt-node-servant yurthub yurt-manager + yurt-iot-dock ) http_proxy=${http_proxy:-} diff --git a/hack/make-rules/kustomize_to_chart.sh b/hack/make-rules/kustomize_to_chart.sh index a9442b31b2a..f8e0f98eff8 100755 --- a/hack/make-rules/kustomize_to_chart.sh +++ b/hack/make-rules/kustomize_to_chart.sh @@ -192,6 +192,10 @@ EOF mv ${crd_dir}/apiextensions.k8s.io_v1_customresourcedefinition_yurtappdaemons.apps.openyurt.io.yaml ${crd_dir}/apps.openyurt.io_yurtappdaemons.yaml mv ${crd_dir}/apiextensions.k8s.io_v1_customresourcedefinition_yurtappsets.apps.openyurt.io.yaml ${crd_dir}/apps.openyurt.io_yurtappsets.yaml mv ${crd_dir}/apiextensions.k8s.io_v1_customresourcedefinition_gateways.raven.openyurt.io.yaml ${crd_dir}/raven.openyurt.io_gateways.yaml + mv ${crd_dir}/apiextensions.k8s.io_v1_customresourcedefinition_platformadmins.iot.openyurt.io.yaml ${crd_dir}/iot.openyurt.io_platformadmins.yaml + mv ${crd_dir}/apiextensions.k8s.io_v1_customresourcedefinition_devices.iot.openyurt.io.yaml ${crd_dir}/iot.openyurt.io_devices.yaml + mv ${crd_dir}/apiextensions.k8s.io_v1_customresourcedefinition_deviceservices.iot.openyurt.io.yaml ${crd_dir}/iot.openyurt.io_deviceservices.yaml + mv ${crd_dir}/apiextensions.k8s.io_v1_customresourcedefinition_deviceprofiles.iot.openyurt.io.yaml ${crd_dir}/iot.openyurt.io_deviceprofiles.yaml # rbac dir local rbac_kustomization_resources="" diff --git a/hack/make-rules/local-up-openyurt.sh b/hack/make-rules/local-up-openyurt.sh index 5181eec46f8..88c709b0b4f 100755 --- a/hack/make-rules/local-up-openyurt.sh +++ b/hack/make-rules/local-up-openyurt.sh @@ -56,6 +56,7 @@ readonly REQUIRED_IMAGES=( openyurt/node-servant openyurt/yurt-manager openyurt/yurthub + openyurt/yurt-iot-dock ) readonly LOCAL_ARCH=$(go env GOHOSTARCH) diff --git a/pkg/apis/iot/v1alpha1/condition_const.go b/pkg/apis/iot/v1alpha1/condition_const.go index 7f1c8b5e84e..498c9c3e87e 100644 --- a/pkg/apis/iot/v1alpha1/condition_const.go +++ b/pkg/apis/iot/v1alpha1/condition_const.go @@ -35,4 +35,30 @@ const ( DeploymentProvisioningReason = "DeploymentProvisioning" DeploymentProvisioningFailedReason = "DeploymentProvisioningFailed" + + // DeviceSyncedCondition indicates that the device exists in both OpenYurt and edge platform + DeviceSyncedCondition DeviceConditionType = "DeviceSynced" + + DeviceManagingReason = "This device is not managed by openyurt" + + DeviceCreateSyncedReason = "Failed to create device on edge platform" + + // DeviceManagingCondition indicates that the device is being managed by cloud and its properties are being reconciled + DeviceManagingCondition DeviceConditionType = "DeviceManaging" + + DeviceVistedCoreMetadataSyncedReason = "Failed to visit the EdgeX core-metadata-service" + + DeviceUpdateStateReason = "Failed to update AdminState or OperatingState of device on edge platform" + + // DeviceServiceSyncedCondition indicates that the deviceService exists in both OpenYurt and edge platform + DeviceServiceSyncedCondition DeviceServiceConditionType = "DeviceServiceSynced" + + DeviceServiceManagingReason = "This deviceService is not managed by openyurt" + + // DeviceServiceManagingCondition indicates that the deviceService is being managed by cloud and its field are being reconciled + DeviceServiceManagingCondition DeviceServiceConditionType = "DeviceServiceManaging" + + DeviceServiceCreateSyncedReason = "Failed to add DeviceService to EdgeX" + + DeviceServiceUpdateStatusSyncedReason = "Failed to update DeviceService status" ) diff --git a/pkg/apis/iot/v1alpha1/device_types.go b/pkg/apis/iot/v1alpha1/device_types.go new file mode 100644 index 00000000000..9a1bafd257f --- /dev/null +++ b/pkg/apis/iot/v1alpha1/device_types.go @@ -0,0 +1,175 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + DeviceFinalizer = "iot.openyurt.io/device" +) + +// DeviceConditionType indicates valid conditions type of a Device. +type DeviceConditionType string + +type AdminState string + +const ( + Locked AdminState = "LOCKED" + UnLocked AdminState = "UNLOCKED" +) + +type OperatingState string + +const ( + Unknown OperatingState = "UNKNOWN" + Up OperatingState = "UP" + Down OperatingState = "DOWN" +) + +type ProtocolProperties map[string]string + +// DeviceSpec defines the desired state of Device +type DeviceSpec struct { + // Information describing the device + Description string `json:"description,omitempty"` + // Admin state (locked/unlocked) + AdminState AdminState `json:"adminState,omitempty"` + // Operating state (enabled/disabled) + OperatingState OperatingState `json:"operatingState,omitempty"` + // A map of supported protocols for the given device + Protocols map[string]ProtocolProperties `json:"protocols,omitempty"` + // Other labels applied to the device to help with searching + Labels []string `json:"labels,omitempty"` + // Device service specific location (interface{} is an empty interface so + // it can be anything) + // TODO: location type in edgex is interface{} + Location string `json:"location,omitempty"` + // Associated Device Service - One per device + Service string `json:"serviceName"` + // Associated Device Profile - Describes the device + Profile string `json:"profileName"` + Notify bool `json:"notify"` + // True means device is managed by cloud, cloud can update the related fields + // False means cloud can't update the fields + Managed bool `json:"managed,omitempty"` + // NodePool indicates which nodePool the device comes from + NodePool string `json:"nodePool,omitempty"` + // TODO support the following field + // A list of auto-generated events coming from the device + // AutoEvents []AutoEvent `json:"autoEvents"` + // DeviceProperties represents the expected state of the device's properties + DeviceProperties map[string]DesiredPropertyState `json:"deviceProperties,omitempty"` +} + +type DesiredPropertyState struct { + Name string `json:"name"` + PutURL string `json:"putURL,omitempty"` + DesiredValue string `json:"desiredValue"` +} + +type ActualPropertyState struct { + Name string `json:"name"` + GetURL string `json:"getURL,omitempty"` + ActualValue string `json:"actualValue"` +} + +// DeviceStatus defines the observed state of Device +type DeviceStatus struct { + // Time (milliseconds) that the device last provided any feedback or + // responded to any request + LastConnected int64 `json:"lastConnected,omitempty"` + // Time (milliseconds) that the device reported data to the core + // microservice + LastReported int64 `json:"lastReported,omitempty"` + // Synced indicates whether the device already exists on both OpenYurt and edge platform + Synced bool `json:"synced,omitempty"` + // it represents the actual state of the device's properties + DeviceProperties map[string]ActualPropertyState `json:"deviceProperties,omitempty"` + EdgeId string `json:"edgeId,omitempty"` + // Admin state (locked/unlocked) + AdminState AdminState `json:"adminState,omitempty"` + // Operating state (up/down/unknown) + OperatingState OperatingState `json:"operatingState,omitempty"` + // current device state + // +optional + Conditions []DeviceCondition `json:"conditions,omitempty"` +} + +// DeviceCondition describes current state of a Device. +type DeviceCondition struct { + // Type of in place set condition. + Type DeviceConditionType `json:"type,omitempty"` + + // Status of the condition, one of True, False, Unknown. + Status corev1.ConditionStatus `json:"status,omitempty"` + + // Last time the condition transitioned from one status to another. + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` + + // The reason for the condition's last transition. + Reason string `json:"reason,omitempty"` + + // A human readable message indicating details about the transition. + Message string `json:"message,omitempty"` +} + +// +genclient +// +k8s:openapi-gen=true +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:shortName=dev +// +kubebuilder:printcolumn:name="NODEPOOL",type="string",JSONPath=".spec.nodePool",description="The nodepool of device" +// +kubebuilder:printcolumn:name="SYNCED",type="boolean",JSONPath=".status.synced",description="The synced status of device" +// +kubebuilder:printcolumn:name="MANAGED",type="boolean",priority=1,JSONPath=".spec.managed",description="The managed status of device" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" + +// Device is the Schema for the devices API +type Device struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DeviceSpec `json:"spec,omitempty"` + Status DeviceStatus `json:"status,omitempty"` +} + +func (d *Device) SetConditions(conditions []DeviceCondition) { + d.Status.Conditions = conditions +} + +func (d *Device) GetConditions() []DeviceCondition { + return d.Status.Conditions +} + +func (d *Device) IsAddedToEdgeX() bool { + return d.Status.Synced +} + +//+kubebuilder:object:root=true + +// DeviceList contains a list of Device +type DeviceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Device `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Device{}, &DeviceList{}) +} diff --git a/pkg/apis/iot/v1alpha1/deviceprofile_types.go b/pkg/apis/iot/v1alpha1/deviceprofile_types.go new file mode 100644 index 00000000000..2d25605bea9 --- /dev/null +++ b/pkg/apis/iot/v1alpha1/deviceprofile_types.go @@ -0,0 +1,122 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + DeviceProfileFinalizer = "iot.openyurt.io/deviceprofile" +) + +type DeviceResource struct { + Description string `json:"description"` + Name string `json:"name"` + Tag string `json:"tag,omitempty"` + IsHidden bool `json:"isHidden"` + Properties ResourceProperties `json:"properties"` + Attributes map[string]string `json:"attributes,omitempty"` +} + +type ResourceProperties struct { + ReadWrite string `json:"readWrite,omitempty"` // Read/Write Permissions set for this property + Minimum string `json:"minimum,omitempty"` // Minimum value that can be get/set from this property + Maximum string `json:"maximum,omitempty"` // Maximum value that can be get/set from this property + DefaultValue string `json:"defaultValue,omitempty"` // Default value set to this property if no argument is passed + Mask string `json:"mask,omitempty"` // Mask to be applied prior to get/set of property + Shift string `json:"shift,omitempty"` // Shift to be applied after masking, prior to get/set of property + Scale string `json:"scale,omitempty"` // Multiplicative factor to be applied after shifting, prior to get/set of property + Offset string `json:"offset,omitempty"` // Additive factor to be applied after multiplying, prior to get/set of property + Base string `json:"base,omitempty"` // Base for property to be applied to, leave 0 for no power operation (i.e. base ^ property: 2 ^ 10) + Assertion string `json:"assertion,omitempty"` + MediaType string `json:"mediaType,omitempty"` + Units string `json:"units,omitempty"` + ValueType string `json:"valueType,omitempty"` +} + +type DeviceCommand struct { + Name string `json:"name"` + IsHidden bool `json:"isHidden"` + ReadWrite string `json:"readWrite"` + ResourceOperations []ResourceOperation `json:"resourceOperations"` +} + +type ResourceOperation struct { + DeviceResource string `json:"deviceResource,omitempty"` + Mappings map[string]string `json:"mappings,omitempty"` + DefaultValue string `json:"defaultValue"` +} + +// DeviceProfileSpec defines the desired state of DeviceProfile +type DeviceProfileSpec struct { + // NodePool specifies which nodePool the deviceProfile belongs to + NodePool string `json:"nodePool,omitempty"` + Description string `json:"description,omitempty"` + // Manufacturer of the device + Manufacturer string `json:"manufacturer,omitempty"` + // Model of the device + Model string `json:"model,omitempty"` + // Labels used to search for groups of profiles on EdgeX Foundry + Labels []string `json:"labels,omitempty"` + DeviceResources []DeviceResource `json:"deviceResources,omitempty"` + DeviceCommands []DeviceCommand `json:"deviceCommands,omitempty"` +} + +// DeviceProfileStatus defines the observed state of DeviceProfile +type DeviceProfileStatus struct { + EdgeId string `json:"id,omitempty"` + Synced bool `json:"synced,omitempty"` +} + +// +genclient +// +k8s:openapi-gen=true +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:shortName=dp +// +kubebuilder:printcolumn:name="NODEPOOL",type="string",JSONPath=".spec.nodePool",description="The nodepool of deviceProfile" +// +kubebuilder:printcolumn:name="SYNCED",type="boolean",JSONPath=".status.synced",description="The synced status of deviceProfile" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" + +// DeviceProfile represents the attributes and operational capabilities of a device. +// It is a template for which there can be multiple matching devices within a given system. +// NOTE This struct is derived from +// edgex/go-mod-core-contracts/models/deviceprofile.go +type DeviceProfile struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DeviceProfileSpec `json:"spec,omitempty"` + Status DeviceProfileStatus `json:"status,omitempty"` +} + +func (dp *DeviceProfile) IsAddedToEdgeX() bool { + return dp.Status.Synced +} + +//+kubebuilder:object:root=true + +// DeviceProfileList contains a list of DeviceProfile +type DeviceProfileList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DeviceProfile `json:"items"` +} + +func init() { + SchemeBuilder.Register(&DeviceProfile{}, &DeviceProfileList{}) +} diff --git a/pkg/apis/iot/v1alpha1/deviceservice_types.go b/pkg/apis/iot/v1alpha1/deviceservice_types.go new file mode 100644 index 00000000000..4720b4e46bf --- /dev/null +++ b/pkg/apis/iot/v1alpha1/deviceservice_types.go @@ -0,0 +1,121 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + DeviceServiceFinalizer = "iot.openyurt.io/deviceservice" +) + +// DeviceServiceConditionType indicates valid conditions type of a Device Service. +type DeviceServiceConditionType string + +// DeviceServiceSpec defines the desired state of DeviceService +type DeviceServiceSpec struct { + BaseAddress string `json:"baseAddress"` + // Information describing the device + Description string `json:"description,omitempty"` + // tags or other labels applied to the device service for search or other + // identification needs on the EdgeX Foundry + Labels []string `json:"labels,omitempty"` + // Device Service Admin State + AdminState AdminState `json:"adminState,omitempty"` + // True means deviceService is managed by cloud, cloud can update the related fields + // False means cloud can't update the fields + Managed bool `json:"managed,omitempty"` + // NodePool indicates which nodePool the deviceService comes from + NodePool string `json:"nodePool,omitempty"` +} + +// DeviceServiceStatus defines the observed state of DeviceService +type DeviceServiceStatus struct { + // Synced indicates whether the device already exists on both OpenYurt and edge platform + Synced bool `json:"synced,omitempty"` + // the Id assigned by the edge platform + EdgeId string `json:"edgeId,omitempty"` + // time in milliseconds that the device last reported data to the core + LastConnected int64 `json:"lastConnected,omitempty"` + // time in milliseconds that the device last reported data to the core + LastReported int64 `json:"lastReported,omitempty"` + // Device Service Admin State + AdminState AdminState `json:"adminState,omitempty"` + // current deviceService state + // +optional + Conditions []DeviceServiceCondition `json:"conditions,omitempty"` +} + +// DeviceServiceCondition describes current state of a Device. +type DeviceServiceCondition struct { + // Type of in place set condition. + Type DeviceServiceConditionType `json:"type,omitempty"` + + // Status of the condition, one of True, False, Unknown. + Status corev1.ConditionStatus `json:"status,omitempty"` + + // Last time the condition transitioned from one status to another. + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` + + // The reason for the condition's last transition. + Reason string `json:"reason,omitempty"` + + // A human readable message indicating details about the transition. + Message string `json:"message,omitempty"` +} + +// +genclient +// +k8s:openapi-gen=true +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:shortName=dsvc +// +kubebuilder:printcolumn:name="NODEPOOL",type="string",JSONPath=".spec.nodePool",description="The nodepool of deviceService" +// +kubebuilder:printcolumn:name="SYNCED",type="boolean",JSONPath=".status.synced",description="The synced status of deviceService" +// +kubebuilder:printcolumn:name="MANAGED",type="boolean",priority=1,JSONPath=".spec.managed",description="The managed status of deviceService" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" + +// DeviceService is the Schema for the deviceservices API +type DeviceService struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DeviceServiceSpec `json:"spec,omitempty"` + Status DeviceServiceStatus `json:"status,omitempty"` +} + +func (ds *DeviceService) SetConditions(conditions []DeviceServiceCondition) { + ds.Status.Conditions = conditions +} + +func (ds *DeviceService) GetConditions() []DeviceServiceCondition { + return ds.Status.Conditions +} + +//+kubebuilder:object:root=true + +// DeviceServiceList contains a list of DeviceService +type DeviceServiceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DeviceService `json:"items"` +} + +func init() { + SchemeBuilder.Register(&DeviceService{}, &DeviceServiceList{}) +} diff --git a/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go index 62aaa062a52..3162ed46c6e 100644 --- a/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go @@ -25,6 +25,21 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActualPropertyState) DeepCopyInto(out *ActualPropertyState) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActualPropertyState. +func (in *ActualPropertyState) DeepCopy() *ActualPropertyState { + if in == nil { + return nil + } + out := new(ActualPropertyState) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DeploymentTemplateSpec) DeepCopyInto(out *DeploymentTemplateSpec) { *out = *in @@ -42,6 +57,439 @@ func (in *DeploymentTemplateSpec) DeepCopy() *DeploymentTemplateSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DesiredPropertyState) DeepCopyInto(out *DesiredPropertyState) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DesiredPropertyState. +func (in *DesiredPropertyState) DeepCopy() *DesiredPropertyState { + if in == nil { + return nil + } + out := new(DesiredPropertyState) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Device) DeepCopyInto(out *Device) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Device. +func (in *Device) DeepCopy() *Device { + if in == nil { + return nil + } + out := new(Device) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Device) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceCommand) DeepCopyInto(out *DeviceCommand) { + *out = *in + if in.ResourceOperations != nil { + in, out := &in.ResourceOperations, &out.ResourceOperations + *out = make([]ResourceOperation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceCommand. +func (in *DeviceCommand) DeepCopy() *DeviceCommand { + if in == nil { + return nil + } + out := new(DeviceCommand) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceCondition) DeepCopyInto(out *DeviceCondition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceCondition. +func (in *DeviceCondition) DeepCopy() *DeviceCondition { + if in == nil { + return nil + } + out := new(DeviceCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceList) DeepCopyInto(out *DeviceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Device, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceList. +func (in *DeviceList) DeepCopy() *DeviceList { + if in == nil { + return nil + } + out := new(DeviceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeviceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceProfile) DeepCopyInto(out *DeviceProfile) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceProfile. +func (in *DeviceProfile) DeepCopy() *DeviceProfile { + if in == nil { + return nil + } + out := new(DeviceProfile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeviceProfile) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceProfileList) DeepCopyInto(out *DeviceProfileList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DeviceProfile, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceProfileList. +func (in *DeviceProfileList) DeepCopy() *DeviceProfileList { + if in == nil { + return nil + } + out := new(DeviceProfileList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeviceProfileList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceProfileSpec) DeepCopyInto(out *DeviceProfileSpec) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.DeviceResources != nil { + in, out := &in.DeviceResources, &out.DeviceResources + *out = make([]DeviceResource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.DeviceCommands != nil { + in, out := &in.DeviceCommands, &out.DeviceCommands + *out = make([]DeviceCommand, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceProfileSpec. +func (in *DeviceProfileSpec) DeepCopy() *DeviceProfileSpec { + if in == nil { + return nil + } + out := new(DeviceProfileSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceProfileStatus) DeepCopyInto(out *DeviceProfileStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceProfileStatus. +func (in *DeviceProfileStatus) DeepCopy() *DeviceProfileStatus { + if in == nil { + return nil + } + out := new(DeviceProfileStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceResource) DeepCopyInto(out *DeviceResource) { + *out = *in + out.Properties = in.Properties + if in.Attributes != nil { + in, out := &in.Attributes, &out.Attributes + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceResource. +func (in *DeviceResource) DeepCopy() *DeviceResource { + if in == nil { + return nil + } + out := new(DeviceResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceService) DeepCopyInto(out *DeviceService) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceService. +func (in *DeviceService) DeepCopy() *DeviceService { + if in == nil { + return nil + } + out := new(DeviceService) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeviceService) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceServiceCondition) DeepCopyInto(out *DeviceServiceCondition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceServiceCondition. +func (in *DeviceServiceCondition) DeepCopy() *DeviceServiceCondition { + if in == nil { + return nil + } + out := new(DeviceServiceCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceServiceList) DeepCopyInto(out *DeviceServiceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DeviceService, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceServiceList. +func (in *DeviceServiceList) DeepCopy() *DeviceServiceList { + if in == nil { + return nil + } + out := new(DeviceServiceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeviceServiceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceServiceSpec) DeepCopyInto(out *DeviceServiceSpec) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceServiceSpec. +func (in *DeviceServiceSpec) DeepCopy() *DeviceServiceSpec { + if in == nil { + return nil + } + out := new(DeviceServiceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceServiceStatus) DeepCopyInto(out *DeviceServiceStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]DeviceServiceCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceServiceStatus. +func (in *DeviceServiceStatus) DeepCopy() *DeviceServiceStatus { + if in == nil { + return nil + } + out := new(DeviceServiceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceSpec) DeepCopyInto(out *DeviceSpec) { + *out = *in + if in.Protocols != nil { + in, out := &in.Protocols, &out.Protocols + *out = make(map[string]ProtocolProperties, len(*in)) + for key, val := range *in { + var outVal map[string]string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make(ProtocolProperties, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + (*out)[key] = outVal + } + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.DeviceProperties != nil { + in, out := &in.DeviceProperties, &out.DeviceProperties + *out = make(map[string]DesiredPropertyState, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceSpec. +func (in *DeviceSpec) DeepCopy() *DeviceSpec { + if in == nil { + return nil + } + out := new(DeviceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceStatus) DeepCopyInto(out *DeviceStatus) { + *out = *in + if in.DeviceProperties != nil { + in, out := &in.DeviceProperties, &out.DeviceProperties + *out = make(map[string]ActualPropertyState, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]DeviceCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceStatus. +func (in *DeviceStatus) DeepCopy() *DeviceStatus { + if in == nil { + return nil + } + out := new(DeviceStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PlatformAdmin) DeepCopyInto(out *PlatformAdmin) { *out = *in @@ -168,6 +616,64 @@ func (in *PlatformAdminStatus) DeepCopy() *PlatformAdminStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ProtocolProperties) DeepCopyInto(out *ProtocolProperties) { + { + in := &in + *out = make(ProtocolProperties, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProtocolProperties. +func (in ProtocolProperties) DeepCopy() ProtocolProperties { + if in == nil { + return nil + } + out := new(ProtocolProperties) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceOperation) DeepCopyInto(out *ResourceOperation) { + *out = *in + if in.Mappings != nil { + in, out := &in.Mappings, &out.Mappings + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceOperation. +func (in *ResourceOperation) DeepCopy() *ResourceOperation { + if in == nil { + return nil + } + out := new(ResourceOperation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceProperties) DeepCopyInto(out *ResourceProperties) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceProperties. +func (in *ResourceProperties) DeepCopy() *ResourceProperties { + if in == nil { + return nil + } + out := new(ResourceProperties) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceTemplateSpec) DeepCopyInto(out *ServiceTemplateSpec) { *out = *in diff --git a/pkg/controller/platformadmin/platformadmin_controller.go b/pkg/controller/platformadmin/platformadmin_controller.go index 542c147c0f2..dddb4178a9e 100644 --- a/pkg/controller/platformadmin/platformadmin_controller.go +++ b/pkg/controller/platformadmin/platformadmin_controller.go @@ -729,6 +729,12 @@ func (r *ReconcilePlatformAdmin) initFramework(ctx context.Context, platformAdmi platformAdminFramework.Components = r.Configration.NoSectyComponents[platformAdmin.Spec.Version] } + yurtIotDock, err := NewYurtIoTDockComponent(platformAdmin, platformAdminFramework) + if err != nil { + return err + } + platformAdminFramework.Components = append(platformAdminFramework.Components, yurtIotDock) + // For better serialization, the serialization method of the Kubernetes runtime library is used data, err := runtime.Encode(r.yamlSerializer, platformAdminFramework) if err != nil { diff --git a/pkg/controller/platformadmin/util.go b/pkg/controller/platformadmin/util.go new file mode 100644 index 00000000000..6abc2b5e780 --- /dev/null +++ b/pkg/controller/platformadmin/util.go @@ -0,0 +1,126 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package platformadmin + +import ( + "fmt" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/pointer" + + iotv1alpha2 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" + "github.com/openyurtio/openyurt/pkg/controller/platformadmin/config" + utils "github.com/openyurtio/openyurt/pkg/controller/platformadmin/utils" +) + +// NewYurtIoTDockComponent initialize the configuration of yurt-iot-dock component +func NewYurtIoTDockComponent(platformAdmin *iotv1alpha2.PlatformAdmin, platformAdminFramework *PlatformAdminFramework) (*config.Component, error) { + var yurtIotDockComponent config.Component + + // If the configuration of the yurt-iot-dock component that customized in the platformAdminFramework + for _, cp := range platformAdminFramework.Components { + if cp.Name != utils.IotDockName { + continue + } + return cp, nil + } + + // Otherwise, the default configuration is used to start + ver, ns, err := utils.DefaultVersion(platformAdmin) + if err != nil { + return nil, err + } + + yurtIotDockComponent.Name = utils.IotDockName + yurtIotDockComponent.Deployment = &appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": utils.IotDockName, + "control-plane": utils.IotDockControlPlane, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": utils.IotDockName, + "control-plane": utils.IotDockControlPlane, + }, + Namespace: ns, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: utils.IotDockName, + Image: fmt.Sprintf("%s:%s", utils.IotDockImage, ver), + ImagePullPolicy: corev1.PullIfNotPresent, + Args: []string{ + "--health-probe-bind-address=:8081", + "--metrics-bind-address=127.0.0.1:8080", + "--leader-elect=false", + fmt.Sprintf("--namespace=%s", ns), + }, + LivenessProbe: &corev1.Probe{ + InitialDelaySeconds: 15, + PeriodSeconds: 20, + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/healthz", + Port: intstr.FromInt(8081), + }, + }, + }, + ReadinessProbe: &corev1.Probe{ + InitialDelaySeconds: 5, + PeriodSeconds: 10, + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/readyz", + Port: intstr.FromInt(8081), + }, + }, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("512m"), + corev1.ResourceMemory: resource.MustParse("256Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1024m"), + corev1.ResourceMemory: resource.MustParse("512Mi"), + }, + }, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: pointer.Bool(false), + }, + }, + }, + TerminationGracePeriodSeconds: pointer.Int64(10), + SecurityContext: &corev1.PodSecurityContext{ + RunAsUser: pointer.Int64(65532), + }, + }, + }, + } + // YurtIoTDock doesn't need a service yet + yurtIotDockComponent.Service = nil + + return &yurtIotDockComponent, nil +} diff --git a/pkg/controller/platformadmin/utils/util.go b/pkg/controller/platformadmin/utils/conditions.go similarity index 100% rename from pkg/controller/platformadmin/utils/util.go rename to pkg/controller/platformadmin/utils/conditions.go diff --git a/pkg/controller/platformadmin/utils/version.go b/pkg/controller/platformadmin/utils/version.go new file mode 100644 index 00000000000..08542f4c97c --- /dev/null +++ b/pkg/controller/platformadmin/utils/version.go @@ -0,0 +1,31 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + iotv1alpha2 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" +) + +const IotDockName = "yurt-iot-dock" +const IotDockImage = "openyurt/yurt-iot-dock" +const IotDockControlPlane = "platformadmin-controller" + +func DefaultVersion(platformAdmin *iotv1alpha2.PlatformAdmin) (string, string, error) { + version := "latest" + ns := platformAdmin.Namespace + return version, ns, nil +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/device_client.go b/pkg/yurtiotdock/clients/edgex-foundry/device_client.go new file mode 100644 index 00000000000..72ef381d8fa --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/device_client.go @@ -0,0 +1,371 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package edgex_foundry + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/cookiejar" + "strings" + "time" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/common" + edgex_resp "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses" + "github.com/go-resty/resty/v2" + "golang.org/x/net/publicsuffix" + "k8s.io/klog/v2" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +type EdgexDeviceClient struct { + *resty.Client + CoreMetaAddr string + CoreCommandAddr string +} + +func NewEdgexDeviceClient(coreMetaAddr, coreCommandAddr string) *EdgexDeviceClient { + cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + instance := resty.NewWithClient(&http.Client{ + Jar: cookieJar, + Timeout: 10 * time.Second, + }) + return &EdgexDeviceClient{ + Client: instance, + CoreMetaAddr: coreMetaAddr, + CoreCommandAddr: coreCommandAddr, + } +} + +// Create function sends a POST request to EdgeX to add a new device +func (efc *EdgexDeviceClient) Create(ctx context.Context, device *iotv1alpha1.Device, options clients.CreateOptions) (*iotv1alpha1.Device, error) { + devs := []*iotv1alpha1.Device{device} + req := makeEdgeXDeviceRequest(devs) + klog.V(5).Infof("will add the Device: %s", device.Name) + reqBody, err := json.Marshal(req) + if err != nil { + return nil, err + } + postPath := fmt.Sprintf("http://%s%s", efc.CoreMetaAddr, DevicePath) + resp, err := efc.R(). + SetBody(reqBody).Post(postPath) + if err != nil { + return nil, err + } else if resp.StatusCode() != http.StatusMultiStatus { + return nil, fmt.Errorf("create device on edgex foundry failed, the response is : %s", resp.Body()) + } + + var edgexResps []*common.BaseWithIdResponse + if err = json.Unmarshal(resp.Body(), &edgexResps); err != nil { + return nil, err + } + createdDevice := device.DeepCopy() + if len(edgexResps) == 1 { + if edgexResps[0].StatusCode == http.StatusCreated { + createdDevice.Status.EdgeId = edgexResps[0].Id + createdDevice.Status.Synced = true + } else { + return nil, fmt.Errorf("create device on edgex foundry failed, the response is : %s", resp.Body()) + } + } else { + return nil, fmt.Errorf("edgex BaseWithIdResponse count mismatch device cound, the response is : %s", resp.Body()) + } + return createdDevice, err +} + +// Delete function sends a request to EdgeX to delete a device +func (efc *EdgexDeviceClient) Delete(ctx context.Context, name string, options clients.DeleteOptions) error { + klog.V(5).Infof("will delete the Device: %s", name) + delURL := fmt.Sprintf("http://%s%s/name/%s", efc.CoreMetaAddr, DevicePath, name) + resp, err := efc.R().Delete(delURL) + if err != nil { + return err + } + if resp.StatusCode() != http.StatusOK { + return errors.New(string(resp.Body())) + } + return nil +} + +// Update is used to set the admin or operating state of the device by unique name of the device. +// TODO support to update other fields +func (efc *EdgexDeviceClient) Update(ctx context.Context, device *iotv1alpha1.Device, options clients.UpdateOptions) (*iotv1alpha1.Device, error) { + actualDeviceName := getEdgeXName(device) + patchURL := fmt.Sprintf("http://%s%s", efc.CoreMetaAddr, DevicePath) + if device == nil { + return nil, nil + } + devs := []*iotv1alpha1.Device{device} + req := makeEdgeXDeviceUpdateRequest(devs) + reqBody, err := json.Marshal(req) + if err != nil { + return nil, err + } + rep, err := efc.R(). + SetHeader("Content-Type", "application/json"). + SetBody(reqBody). + Patch(patchURL) + if err != nil { + return nil, err + } else if rep.StatusCode() != http.StatusMultiStatus { + return nil, fmt.Errorf("failed to update device: %s, get response: %s", actualDeviceName, string(rep.Body())) + } + return device, nil +} + +// Get is used to query the device information corresponding to the device name +func (efc *EdgexDeviceClient) Get(ctx context.Context, deviceName string, options clients.GetOptions) (*iotv1alpha1.Device, error) { + klog.V(5).Infof("will get Devices: %s", deviceName) + var dResp edgex_resp.DeviceResponse + getURL := fmt.Sprintf("http://%s%s/name/%s", efc.CoreMetaAddr, DevicePath, deviceName) + resp, err := efc.R().Get(getURL) + if err != nil { + return nil, err + } + if resp.StatusCode() == http.StatusNotFound { + return nil, fmt.Errorf("Device %s not found", deviceName) + } + err = json.Unmarshal(resp.Body(), &dResp) + if err != nil { + return nil, err + } + device := toKubeDevice(dResp.Device, options.Namespace) + return &device, err +} + +// List is used to get all device objects on edge platform +// TODO:support label filtering according to options +func (efc *EdgexDeviceClient) List(ctx context.Context, options clients.ListOptions) ([]iotv1alpha1.Device, error) { + lp := fmt.Sprintf("http://%s%s/all?limit=-1", efc.CoreMetaAddr, DevicePath) + resp, err := efc.R().EnableTrace().Get(lp) + if err != nil { + return nil, err + } + var mdResp edgex_resp.MultiDevicesResponse + if err := json.Unmarshal(resp.Body(), &mdResp); err != nil { + return nil, err + } + var res []iotv1alpha1.Device + for _, dp := range mdResp.Devices { + res = append(res, toKubeDevice(dp, options.Namespace)) + } + return res, nil +} + +func (efc *EdgexDeviceClient) GetPropertyState(ctx context.Context, propertyName string, d *iotv1alpha1.Device, options clients.GetOptions) (*iotv1alpha1.ActualPropertyState, error) { + actualDeviceName := getEdgeXName(d) + // get the old property from status + oldAps, exist := d.Status.DeviceProperties[propertyName] + propertyGetURL := "" + // 1. query the Get URL of a property + if !exist || (exist && oldAps.GetURL == "") { + coreCommands, err := efc.GetCommandResponseByName(actualDeviceName) + if err != nil { + return &iotv1alpha1.ActualPropertyState{}, err + } + for _, c := range coreCommands { + if c.Name == propertyName && c.Get { + propertyGetURL = fmt.Sprintf("%s%s", c.Url, c.Path) + break + } + } + if propertyGetURL == "" { + return nil, &clients.NotFoundError{} + } + } else { + propertyGetURL = oldAps.GetURL + } + // 2. get the actual property value by the getURL + actualPropertyState := iotv1alpha1.ActualPropertyState{ + Name: propertyName, + GetURL: propertyGetURL, + } + if resp, err := efc.getPropertyState(propertyGetURL); err != nil { + return nil, err + } else { + var eResp edgex_resp.EventResponse + if err := json.Unmarshal(resp.Body(), &eResp); err != nil { + return nil, err + } + actualPropertyState.ActualValue = getPropertyValueFromEvent(propertyName, eResp.Event) + } + return &actualPropertyState, nil +} + +// getPropertyState returns different error messages according to the status code +func (efc *EdgexDeviceClient) getPropertyState(getURL string) (*resty.Response, error) { + resp, err := efc.R().Get(getURL) + if err != nil { + return resp, err + } + if resp.StatusCode() == 400 { + err = errors.New("request is in an invalid state") + } else if resp.StatusCode() == 404 { + err = errors.New("the requested resource does not exist") + } else if resp.StatusCode() == 423 { + err = errors.New("the device is locked (AdminState) or down (OperatingState)") + } else if resp.StatusCode() == 500 { + err = errors.New("an unexpected error occurred on the server") + } + return resp, err +} + +func (efc *EdgexDeviceClient) UpdatePropertyState(ctx context.Context, propertyName string, d *iotv1alpha1.Device, options clients.UpdateOptions) error { + // Get the actual device name + acturalDeviceName := getEdgeXName(d) + + dps := d.Spec.DeviceProperties[propertyName] + parameterName := dps.Name + if dps.PutURL == "" { + putCmd, err := efc.getPropertyPut(acturalDeviceName, dps.Name) + if err != nil { + return err + } + dps.PutURL = fmt.Sprintf("%s%s", putCmd.Url, putCmd.Path) + if len(putCmd.Parameters) == 1 { + parameterName = putCmd.Parameters[0].ResourceName + } + } + // set the device property to desired state + bodyMap := make(map[string]string) + bodyMap[parameterName] = dps.DesiredValue + body, _ := json.Marshal(bodyMap) + klog.V(5).Infof("setting the property to desired value", "propertyName", parameterName, "desiredValue", string(body)) + rep, err := efc.R(). + SetHeader("Content-Type", "application/json"). + SetBody(body). + Put(dps.PutURL) + if err != nil { + return err + } else if rep.StatusCode() != http.StatusOK { + return fmt.Errorf("failed to set property: %s, get response: %s", dps.Name, string(rep.Body())) + } else if rep.Body() != nil { + // If the parameters are illegal, such as out of range, the 200 status code is also returned, but the description appears in the body + a := string(rep.Body()) + if strings.Contains(a, "execWriteCmd") { + return fmt.Errorf("failed to set property: %s, get response: %s", dps.Name, string(rep.Body())) + } + } + return nil +} + +// Gets the models.Put from edgex foundry which is used to set the device property's value +func (efc *EdgexDeviceClient) getPropertyPut(deviceName, cmdName string) (dtos.CoreCommand, error) { + coreCommands, err := efc.GetCommandResponseByName(deviceName) + if err != nil { + return dtos.CoreCommand{}, err + } + for _, c := range coreCommands { + if cmdName == c.Name && c.Set { + return c, nil + } + } + return dtos.CoreCommand{}, errors.New("corresponding command is not found") +} + +// ListPropertiesState gets all the actual property information about a device +func (efc *EdgexDeviceClient) ListPropertiesState(ctx context.Context, device *iotv1alpha1.Device, options clients.ListOptions) (map[string]iotv1alpha1.DesiredPropertyState, map[string]iotv1alpha1.ActualPropertyState, error) { + actualDeviceName := getEdgeXName(device) + + dpsm := map[string]iotv1alpha1.DesiredPropertyState{} + apsm := map[string]iotv1alpha1.ActualPropertyState{} + coreCommands, err := efc.GetCommandResponseByName(actualDeviceName) + if err != nil { + return dpsm, apsm, err + } + + for _, c := range coreCommands { + // DesiredPropertyState only store the basic information and does not set DesiredValue + if c.Get { + getURL := fmt.Sprintf("%s%s", c.Url, c.Path) + aps, ok := apsm[c.Name] + if ok { + aps.GetURL = getURL + } else { + aps = iotv1alpha1.ActualPropertyState{Name: c.Name, GetURL: getURL} + } + apsm[c.Name] = aps + resp, err := efc.getPropertyState(getURL) + if err != nil { + klog.V(5).ErrorS(err, "getPropertyState failed", "propertyName", c.Name, "deviceName", actualDeviceName) + } else { + var eResp edgex_resp.EventResponse + if err := json.Unmarshal(resp.Body(), &eResp); err != nil { + klog.V(5).ErrorS(err, "failed to decode the response ", "response", resp) + continue + } + event := eResp.Event + readingName := c.Name + expectParams := c.Parameters + if len(expectParams) == 1 { + readingName = expectParams[0].ResourceName + } + klog.V(5).Infof("get reading name %s for command %s of device %s", readingName, c.Name, device.Name) + actualValue := getPropertyValueFromEvent(readingName, event) + aps.ActualValue = actualValue + apsm[c.Name] = aps + } + } + } + return dpsm, apsm, nil +} + +// The actual property value is resolved from the returned event +func getPropertyValueFromEvent(resName string, event dtos.Event) string { + actualValue := "" + for _, r := range event.Readings { + if resName == r.ResourceName { + if r.SimpleReading.Value != "" { + actualValue = r.SimpleReading.Value + } else if len(r.BinaryReading.BinaryValue) != 0 { + // TODO: how to demonstrate binary data + actualValue = fmt.Sprintf("%s:%s", r.BinaryReading.MediaType, "blob value") + } else if r.ObjectReading.ObjectValue != nil { + serializedBytes, _ := json.Marshal(r.ObjectReading.ObjectValue) + actualValue = string(serializedBytes) + } + break + } + } + return actualValue +} + +// GetCommandResponseByName gets all commands supported by the device +func (efc *EdgexDeviceClient) GetCommandResponseByName(deviceName string) ([]dtos.CoreCommand, error) { + klog.V(5).Infof("will get CommandResponses of device: %s", deviceName) + + var dcr edgex_resp.DeviceCoreCommandResponse + getURL := fmt.Sprintf("http://%s%s/name/%s", efc.CoreCommandAddr, CommandResponsePath, deviceName) + + resp, err := efc.R().Get(getURL) + if err != nil { + return nil, err + } + if resp.StatusCode() == http.StatusNotFound { + return nil, errors.New("Item not found") + } + err = json.Unmarshal(resp.Body(), &dcr) + if err != nil { + return nil, err + } + return dcr.DeviceCoreCommand.CoreCommands, nil +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/device_client_test.go b/pkg/yurtiotdock/clients/edgex-foundry/device_client_test.go new file mode 100644 index 00000000000..5873a540d74 --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/device_client_test.go @@ -0,0 +1,202 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package edgex_foundry + +import ( + "context" + "encoding/json" + "testing" + + edgex_resp "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +const ( + DeviceListMetadata = `{"apiVersion":"v2","statusCode":200,"totalCount":5,"devices":[{"created":1661829206505,"modified":1661829206505,"id":"f6255845-f4b2-4182-bd3c-abc9eac4a649","name":"Random-Float-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-Float-Device","autoEvents":[{"interval":"30s","onChange":false,"sourceName":"Float32"},{"interval":"30s","onChange":false,"sourceName":"Float64"}],"protocols":{"other":{"Address":"device-virtual-float-01","Protocol":"300"}}},{"created":1661829206506,"modified":1661829206506,"id":"d29efe20-fdec-4aeb-90e5-99528cb6ca28","name":"Random-Binary-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-Binary-Device","protocols":{"other":{"Address":"device-virtual-binary-01","Port":"300"}}},{"created":1661829206504,"modified":1661829206504,"id":"6a7f00a4-9536-48b2-9380-a9fc202ac517","name":"Random-Integer-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-Integer-Device","autoEvents":[{"interval":"15s","onChange":false,"sourceName":"Int8"},{"interval":"15s","onChange":false,"sourceName":"Int16"},{"interval":"15s","onChange":false,"sourceName":"Int32"},{"interval":"15s","onChange":false,"sourceName":"Int64"}],"protocols":{"other":{"Address":"device-virtual-int-01","Protocol":"300"}}},{"created":1661829206503,"modified":1661829206503,"id":"439d47a2-fa72-4c27-9f47-c19356cc0c3b","name":"Random-Boolean-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-Boolean-Device","autoEvents":[{"interval":"10s","onChange":false,"sourceName":"Bool"}],"protocols":{"other":{"Address":"device-virtual-bool-01","Port":"300"}}},{"created":1661829206505,"modified":1661829206505,"id":"2890ab86-3ae4-4b5e-98ab-aad85fc540e6","name":"Random-UnsignedInteger-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-UnsignedInteger-Device","autoEvents":[{"interval":"20s","onChange":false,"sourceName":"Uint8"},{"interval":"20s","onChange":false,"sourceName":"Uint16"},{"interval":"20s","onChange":false,"sourceName":"Uint32"},{"interval":"20s","onChange":false,"sourceName":"Uint64"}],"protocols":{"other":{"Address":"device-virtual-uint-01","Protocol":"300"}}}]}` + DeviceMetadata = `{"apiVersion":"v2","statusCode":200,"device":{"created":1661829206505,"modified":1661829206505,"id":"f6255845-f4b2-4182-bd3c-abc9eac4a649","name":"Random-Float-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-Float-Device","autoEvents":[{"interval":"30s","onChange":false,"sourceName":"Float32"},{"interval":"30s","onChange":false,"sourceName":"Float64"}],"protocols":{"other":{"Address":"device-virtual-float-01","Protocol":"300"}}}}` + + DeviceCreateSuccess = `[{"apiVersion":"v2","statusCode":201,"id":"2fff4f1a-7110-442f-b347-9f896338ba57"}]` + DeviceCreateFail = `[{"apiVersion":"v2","message":"device name test-Random-Float-Device already exists","statusCode":409}]` + + DeviceDeleteSuccess = `{"apiVersion":"v2","statusCode":200}` + DeviceDeleteFail = `{"apiVersion":"v2","message":"fail to query device by name test-Random-Float-Device","statusCode":404}` + + DeviceCoreCommands = `{"apiVersion":"v2","statusCode":200,"deviceCoreCommand":{"deviceName":"Random-Float-Device","profileName":"Random-Float-Device","coreCommands":[{"name":"WriteFloat32ArrayValue","set":true,"path":"/api/v2/device/name/Random-Float-Device/WriteFloat32ArrayValue","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float32Array","valueType":"Float32Array"},{"resourceName":"EnableRandomization_Float32Array","valueType":"Bool"}]},{"name":"WriteFloat64ArrayValue","set":true,"path":"/api/v2/device/name/Random-Float-Device/WriteFloat64ArrayValue","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float64Array","valueType":"Float64Array"},{"resourceName":"EnableRandomization_Float64Array","valueType":"Bool"}]},{"name":"Float32","get":true,"set":true,"path":"/api/v2/device/name/Random-Float-Device/Float32","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float32","valueType":"Float32"}]},{"name":"Float64","get":true,"set":true,"path":"/api/v2/device/name/Random-Float-Device/Float64","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float64","valueType":"Float64"}]},{"name":"Float32Array","get":true,"set":true,"path":"/api/v2/device/name/Random-Float-Device/Float32Array","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float32Array","valueType":"Float32Array"}]},{"name":"Float64Array","get":true,"set":true,"path":"/api/v2/device/name/Random-Float-Device/Float64Array","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float64Array","valueType":"Float64Array"}]},{"name":"WriteFloat32Value","set":true,"path":"/api/v2/device/name/Random-Float-Device/WriteFloat32Value","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float32","valueType":"Float32"},{"resourceName":"EnableRandomization_Float32","valueType":"Bool"}]},{"name":"WriteFloat64Value","set":true,"path":"/api/v2/device/name/Random-Float-Device/WriteFloat64Value","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float64","valueType":"Float64"},{"resourceName":"EnableRandomization_Float64","valueType":"Bool"}]}]}}` + DeviceCommandResp = `{"apiVersion":"v2","statusCode":200,"event":{"apiVersion":"v2","id":"095090e4-de39-45a1-a0fa-18bc340104e6","deviceName":"Random-Float-Device","profileName":"Random-Float-Device","sourceName":"Float32","origin":1661851070562067780,"readings":[{"id":"972bf6be-3b01-49fc-b211-a43ed51d207d","origin":1661851070562067780,"deviceName":"Random-Float-Device","resourceName":"Float32","profileName":"Random-Float-Device","valueType":"Float32","value":"-2.038811e+38"}]}}` + + DeviceUpdateSuccess = `[{"apiVersion":"v2","statusCode":200}] ` + + DeviceUpdateProperty = `{"apiVersion":"v2","statusCode":200}` +) + +var deviceClient = NewEdgexDeviceClient("edgex-core-metadata:59881", "edgex-core-command:59882") + +func Test_Get(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v2/device/name/Random-Float-Device", + httpmock.NewStringResponder(200, DeviceMetadata)) + + device, err := deviceClient.Get(context.TODO(), "Random-Float-Device", clients.GetOptions{Namespace: "default"}) + assert.Nil(t, err) + + assert.Equal(t, "Random-Float-Device", device.Spec.Profile) +} + +func Test_List(t *testing.T) { + + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v2/device/all?limit=-1", + httpmock.NewStringResponder(200, DeviceListMetadata)) + + devices, err := deviceClient.List(context.TODO(), clients.ListOptions{Namespace: "default"}) + assert.Nil(t, err) + + assert.Equal(t, len(devices), 5) +} + +func Test_Create(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v2/device", + httpmock.NewStringResponder(207, DeviceCreateSuccess)) + + var resp edgex_resp.DeviceResponse + + err := json.Unmarshal([]byte(DeviceMetadata), &resp) + assert.Nil(t, err) + + device := toKubeDevice(resp.Device, "default") + device.Name = "test-Random-Float-Device" + + create, err := deviceClient.Create(context.TODO(), &device, clients.CreateOptions{}) + assert.Nil(t, err) + + assert.Equal(t, "test-Random-Float-Device", create.Name) + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v2/device", + httpmock.NewStringResponder(207, DeviceCreateFail)) + + create, err = deviceClient.Create(context.TODO(), &device, clients.CreateOptions{}) + assert.NotNil(t, err) + assert.Nil(t, create) +} + +func Test_Delete(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v2/device/name/test-Random-Float-Device", + httpmock.NewStringResponder(200, DeviceDeleteSuccess)) + + err := deviceClient.Delete(context.TODO(), "test-Random-Float-Device", clients.DeleteOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v2/device/name/test-Random-Float-Device", + httpmock.NewStringResponder(404, DeviceDeleteFail)) + + err = deviceClient.Delete(context.TODO(), "test-Random-Float-Device", clients.DeleteOptions{}) + assert.NotNil(t, err) +} + +func Test_GetPropertyState(t *testing.T) { + + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-command:59882/api/v2/device/name/Random-Float-Device", + httpmock.NewStringResponder(200, DeviceCoreCommands)) + httpmock.RegisterResponder("GET", "http://edgex-core-command:59882/api/v2/device/name/Random-Float-Device/Float32", + httpmock.NewStringResponder(200, DeviceCommandResp)) + + var resp edgex_resp.DeviceResponse + + err := json.Unmarshal([]byte(DeviceMetadata), &resp) + assert.Nil(t, err) + + device := toKubeDevice(resp.Device, "default") + + _, err = deviceClient.GetPropertyState(context.TODO(), "Float32", &device, clients.GetOptions{}) + assert.Nil(t, err) +} + +func Test_ListPropertiesState(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-command:59882/api/v2/device/name/Random-Float-Device", + httpmock.NewStringResponder(200, DeviceCoreCommands)) + + var resp edgex_resp.DeviceResponse + + err := json.Unmarshal([]byte(DeviceMetadata), &resp) + assert.Nil(t, err) + + device := toKubeDevice(resp.Device, "default") + + _, _, err = deviceClient.ListPropertiesState(context.TODO(), &device, clients.ListOptions{}) + assert.Nil(t, err) +} + +func Test_UpdateDevice(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("PATCH", "http://edgex-core-metadata:59881/api/v2/device", + httpmock.NewStringResponder(207, DeviceUpdateSuccess)) + + var resp edgex_resp.DeviceResponse + + err := json.Unmarshal([]byte(DeviceMetadata), &resp) + assert.Nil(t, err) + + device := toKubeDevice(resp.Device, "default") + device.Spec.AdminState = "LOCKED" + + _, err = deviceClient.Update(context.TODO(), &device, clients.UpdateOptions{}) + assert.Nil(t, err) +} + +func Test_UpdatePropertyState(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-command:59882/api/v2/device/name/Random-Float-Device", + httpmock.NewStringResponder(200, DeviceCoreCommands)) + + httpmock.RegisterResponder("PUT", "http://edgex-core-command:59882/api/v2/device/name/Random-Float-Device/Float32", + httpmock.NewStringResponder(200, DeviceUpdateSuccess)) + var resp edgex_resp.DeviceResponse + err := json.Unmarshal([]byte(DeviceMetadata), &resp) + assert.Nil(t, err) + + device := toKubeDevice(resp.Device, "default") + device.Spec.DeviceProperties = map[string]iotv1alpha1.DesiredPropertyState{ + "Float32": { + Name: "Float32", + DesiredValue: "66.66", + }, + } + + err = deviceClient.UpdatePropertyState(context.TODO(), "Float32", &device, clients.UpdateOptions{}) + assert.Nil(t, err) +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client.go b/pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client.go new file mode 100644 index 00000000000..2cd9c9d40c6 --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client.go @@ -0,0 +1,141 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package edgex_foundry + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses" + "github.com/go-resty/resty/v2" + "k8s.io/klog/v2" + + "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + devcli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +type EdgexDeviceProfile struct { + *resty.Client + CoreMetaAddr string +} + +func NewEdgexDeviceProfile(coreMetaAddr string) *EdgexDeviceProfile { + return &EdgexDeviceProfile{ + Client: resty.New(), + CoreMetaAddr: coreMetaAddr, + } +} + +// TODO: support label filtering +func getListDeviceProfileURL(address string, opts devcli.ListOptions) (string, error) { + url := fmt.Sprintf("http://%s%s/all?limit=-1", address, DeviceProfilePath) + return url, nil +} + +func (cdc *EdgexDeviceProfile) List(ctx context.Context, opts devcli.ListOptions) ([]v1alpha1.DeviceProfile, error) { + klog.V(5).Info("will list DeviceProfiles") + lp, err := getListDeviceProfileURL(cdc.CoreMetaAddr, opts) + if err != nil { + return nil, err + } + resp, err := cdc.R().EnableTrace().Get(lp) + if err != nil { + return nil, err + } + var mdpResp responses.MultiDeviceProfilesResponse + if err := json.Unmarshal(resp.Body(), &mdpResp); err != nil { + return nil, err + } + var deviceProfiles []v1alpha1.DeviceProfile + for _, dp := range mdpResp.Profiles { + deviceProfiles = append(deviceProfiles, toKubeDeviceProfile(&dp, opts.Namespace)) + } + return deviceProfiles, nil +} + +func (cdc *EdgexDeviceProfile) Get(ctx context.Context, name string, opts devcli.GetOptions) (*v1alpha1.DeviceProfile, error) { + klog.V(5).Infof("will get DeviceProfiles: %s", name) + var dpResp responses.DeviceProfileResponse + getURL := fmt.Sprintf("http://%s%s/name/%s", cdc.CoreMetaAddr, DeviceProfilePath, name) + resp, err := cdc.R().Get(getURL) + if err != nil { + return nil, err + } + if resp.StatusCode() == http.StatusNotFound { + return nil, fmt.Errorf("DeviceProfile %s not found", name) + } + if err = json.Unmarshal(resp.Body(), &dpResp); err != nil { + return nil, err + } + kubedp := toKubeDeviceProfile(&dpResp.Profile, opts.Namespace) + return &kubedp, nil +} + +func (cdc *EdgexDeviceProfile) Create(ctx context.Context, deviceProfile *v1alpha1.DeviceProfile, opts devcli.CreateOptions) (*v1alpha1.DeviceProfile, error) { + dps := []*v1alpha1.DeviceProfile{deviceProfile} + req := makeEdgeXDeviceProfilesRequest(dps) + klog.V(5).Infof("will add the DeviceProfile: %s", deviceProfile.Name) + reqBody, err := json.Marshal(req) + if err != nil { + return nil, err + } + postURL := fmt.Sprintf("http://%s%s", cdc.CoreMetaAddr, DeviceProfilePath) + resp, err := cdc.R().SetBody(reqBody).Post(postURL) + if err != nil { + return nil, err + } + if resp.StatusCode() != http.StatusMultiStatus { + return nil, fmt.Errorf("create edgex deviceProfile err: %s", string(resp.Body())) // 假定 resp.Body() 存了 msg 信息 + } + var edgexResps []*common.BaseWithIdResponse + if err = json.Unmarshal(resp.Body(), &edgexResps); err != nil { + return nil, err + } + createdDeviceProfile := deviceProfile.DeepCopy() + if len(edgexResps) == 1 { + if edgexResps[0].StatusCode == http.StatusCreated { + createdDeviceProfile.Status.EdgeId = edgexResps[0].Id + createdDeviceProfile.Status.Synced = true + } else { + return nil, fmt.Errorf("create deviceprofile on edgex foundry failed, the response is : %s", resp.Body()) + } + } else { + return nil, fmt.Errorf("edgex BaseWithIdResponse count mismatch DeviceProfile count, the response is : %s", resp.Body()) + } + return createdDeviceProfile, err +} + +// TODO: edgex does not support update DeviceProfile +func (cdc *EdgexDeviceProfile) Update(ctx context.Context, deviceProfile *v1alpha1.DeviceProfile, opts devcli.UpdateOptions) (*v1alpha1.DeviceProfile, error) { + return nil, nil +} + +func (cdc *EdgexDeviceProfile) Delete(ctx context.Context, name string, opts devcli.DeleteOptions) error { + klog.V(5).Infof("will delete the DeviceProfile: %s", name) + delURL := fmt.Sprintf("http://%s%s/name/%s", cdc.CoreMetaAddr, DeviceProfilePath, name) + resp, err := cdc.R().Delete(delURL) + if err != nil { + return err + } + if resp.StatusCode() != http.StatusOK { + return fmt.Errorf("delete edgex deviceProfile err: %s", string(resp.Body())) // 假定 resp.Body() 存了 msg 信息 + } + return nil +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client_test.go b/pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client_test.go new file mode 100644 index 00000000000..0f32eecf687 --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client_test.go @@ -0,0 +1,107 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package edgex_foundry + +import ( + "context" + "encoding/json" + "testing" + + edgex_resp "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +const ( + DeviceProfileListMetaData = `{"apiVersion":"v2","statusCode":200,"totalCount":5,"profiles":[{"created":1661829206499,"modified":1661829206499,"id":"cf624c1f-c93a-48c0-b327-b00c7dc171f1","name":"Random-Binary-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"Generate random binary value","name":"Binary","isHidden":false,"tag":"","properties":{"valueType":"Binary","readWrite":"R","units":"","minimum":"","maximum":"","defaultValue":"","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":"random"},"attributes":null}],"deviceCommands":[]},{"created":1661829206501,"modified":1661829206501,"id":"adeafefa-2d11-4eee-8fe9-a4742f85f7fb","name":"Random-UnsignedInteger-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint8","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint16","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint32","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint64","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint8 value","name":"Uint8","isHidden":false,"tag":"","properties":{"valueType":"Uint8","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint16 value","name":"Uint16","isHidden":false,"tag":"","properties":{"valueType":"Uint16","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint32 value","name":"Uint32","isHidden":false,"tag":"","properties":{"valueType":"Uint32","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint64 value","name":"Uint64","isHidden":false,"tag":"","properties":{"valueType":"Uint64","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint8Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint16Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint32Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint64Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint8 array value","name":"Uint8Array","isHidden":false,"tag":"","properties":{"valueType":"Uint8Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint16 array value","name":"Uint16Array","isHidden":false,"tag":"","properties":{"valueType":"Uint16Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint32 array value","name":"Uint32Array","isHidden":false,"tag":"","properties":{"valueType":"Uint32Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint64 array value","name":"Uint64Array","isHidden":false,"tag":"","properties":{"valueType":"Uint64Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null}],"deviceCommands":[{"name":"WriteUint8Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint8","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint8","defaultValue":"false","mappings":null}]},{"name":"WriteUint16Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint16","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint16","defaultValue":"false","mappings":null}]},{"name":"WriteUint32Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint32","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint32","defaultValue":"false","mappings":null}]},{"name":"WriteUint64Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint64","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint64","defaultValue":"false","mappings":null}]},{"name":"WriteUint8ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint8Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint8Array","defaultValue":"false","mappings":null}]},{"name":"WriteUint16ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint16Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint16Array","defaultValue":"false","mappings":null}]},{"name":"WriteUint32ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint32Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint32Array","defaultValue":"false","mappings":null}]},{"name":"WriteUint64ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint64Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint64Array","defaultValue":"false","mappings":null}]}]},{"created":1661829206500,"modified":1661829206500,"id":"67f4a5a1-06e6-4051-b71d-655ec5dd4eb2","name":"Random-Integer-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int8","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int16","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int32","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int64","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int8 value","name":"Int8","isHidden":false,"tag":"","properties":{"valueType":"Int8","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int16 value","name":"Int16","isHidden":false,"tag":"","properties":{"valueType":"Int16","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int32 value","name":"Int32","isHidden":false,"tag":"","properties":{"valueType":"Int32","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int64 value","name":"Int64","isHidden":false,"tag":"","properties":{"valueType":"Int64","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int8Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int16Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int32Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int64Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int8 array value","name":"Int8Array","isHidden":false,"tag":"","properties":{"valueType":"Int8Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int16 array value","name":"Int16Array","isHidden":false,"tag":"","properties":{"valueType":"Int16Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int32 array value","name":"Int32Array","isHidden":false,"tag":"","properties":{"valueType":"Int32Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int64 array value","name":"Int64Array","isHidden":false,"tag":"","properties":{"valueType":"Int64Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null}],"deviceCommands":[{"name":"WriteInt8Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int8","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int8","defaultValue":"false","mappings":null}]},{"name":"WriteInt16Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int16","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int16","defaultValue":"false","mappings":null}]},{"name":"WriteInt32Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int32","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int32","defaultValue":"false","mappings":null}]},{"name":"WriteInt64Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int64","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int64","defaultValue":"false","mappings":null}]},{"name":"WriteInt8ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int8Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int8Array","defaultValue":"false","mappings":null}]},{"name":"WriteInt16ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int16Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int16Array","defaultValue":"false","mappings":null}]},{"name":"WriteInt32ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int32Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int32Array","defaultValue":"false","mappings":null}]},{"name":"WriteInt64ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int64Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int64Array","defaultValue":"false","mappings":null}]}]},{"created":1661829206500,"modified":1661829206500,"id":"30b8448f-0532-44fb-aed7-5fe4bca16f9a","name":"Random-Float-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Float32","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Float64","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random float32 value","name":"Float32","isHidden":false,"tag":"","properties":{"valueType":"Float32","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random float64 value","name":"Float64","isHidden":false,"tag":"","properties":{"valueType":"Float64","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Float32Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Float64Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random float32 array value","name":"Float32Array","isHidden":false,"tag":"","properties":{"valueType":"Float32Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random float64 array value","name":"Float64Array","isHidden":false,"tag":"","properties":{"valueType":"Float64Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null}],"deviceCommands":[{"name":"WriteFloat32Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Float32","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Float32","defaultValue":"false","mappings":null}]},{"name":"WriteFloat64Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Float64","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Float64","defaultValue":"false","mappings":null}]},{"name":"WriteFloat32ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Float32Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Float32Array","defaultValue":"false","mappings":null}]},{"name":"WriteFloat64ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Float64Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Float64Array","defaultValue":"false","mappings":null}]}]},{"created":1661829206499,"modified":1661829206499,"id":"01dfe04d-f361-41fd-b1c4-7ca0718f461a","name":"Random-Boolean-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Bool","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random boolean value","name":"Bool","isHidden":false,"tag":"","properties":{"valueType":"Bool","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_BoolArray","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random boolean array value","name":"BoolArray","isHidden":false,"tag":"","properties":{"valueType":"BoolArray","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[true]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null}],"deviceCommands":[{"name":"WriteBoolValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Bool","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Bool","defaultValue":"false","mappings":null}]},{"name":"WriteBoolArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"BoolArray","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_BoolArray","defaultValue":"false","mappings":null}]}]}]}` + DeviceProfileMetaData = `{"apiVersion":"v2","statusCode":200,"profile":{"created":1661829206499,"modified":1661829206499,"id":"01dfe04d-f361-41fd-b1c4-7ca0718f461a","name":"Random-Boolean-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Bool","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random boolean value","name":"Bool","isHidden":false,"tag":"","properties":{"valueType":"Bool","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_BoolArray","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random boolean array value","name":"BoolArray","isHidden":false,"tag":"","properties":{"valueType":"BoolArray","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[true]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null}],"deviceCommands":[{"name":"WriteBoolValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Bool","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Bool","defaultValue":"false","mappings":null}]},{"name":"WriteBoolArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"BoolArray","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_BoolArray","defaultValue":"false","mappings":null}]}]}}` + + ProfileCreateSuccess = `[{"apiVersion":"v2","statusCode":201,"id":"a583b97d-7c4d-4b7c-8b93-51da9e68518c"}]` + ProfileCreateFail = `[{"apiVersion":"v2","message":"device profile name test-Random-Boolean-Device exists","statusCode":409}]` + + ProfileDeleteSuccess = `{"apiVersion":"v2","statusCode":200}` + ProfileDeleteFail = `{"apiVersion":"v2","message":"fail to delete the device profile with name test-Random-Boolean-Device","statusCode":404}` +) + +var profileClient = NewEdgexDeviceProfile("edgex-core-metadata:59881") + +func Test_ListProfile(t *testing.T) { + + httpmock.ActivateNonDefault(profileClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v2/deviceprofile/all?limit=-1", + httpmock.NewStringResponder(200, DeviceProfileListMetaData)) + profiles, err := profileClient.List(context.TODO(), clients.ListOptions{Namespace: "default"}) + assert.Nil(t, err) + + assert.Equal(t, 5, len(profiles)) +} + +func Test_GetProfile(T *testing.T) { + httpmock.ActivateNonDefault(profileClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v2/deviceprofile/name/Random-Boolean-Device", + httpmock.NewStringResponder(200, DeviceProfileMetaData)) + + _, err := profileClient.Get(context.TODO(), "Random-Boolean-Device", clients.GetOptions{Namespace: "default"}) + assert.Nil(T, err) +} + +func Test_CreateProfile(t *testing.T) { + httpmock.ActivateNonDefault(profileClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v2/deviceprofile", + httpmock.NewStringResponder(207, ProfileCreateSuccess)) + + var resp edgex_resp.DeviceProfileResponse + + err := json.Unmarshal([]byte(DeviceProfileMetaData), &resp) + assert.Nil(t, err) + + profile := toKubeDeviceProfile(&resp.Profile, "default") + profile.Name = "test-Random-Boolean-Device" + + _, err = profileClient.Create(context.TODO(), &profile, clients.CreateOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v2/deviceprofile", + httpmock.NewStringResponder(207, ProfileCreateFail)) + + _, err = profileClient.Create(context.TODO(), &profile, clients.CreateOptions{}) + assert.NotNil(t, err) +} + +func Test_DeleteProfile(t *testing.T) { + httpmock.ActivateNonDefault(profileClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v2/deviceprofile/name/test-Random-Boolean-Device", + httpmock.NewStringResponder(200, ProfileDeleteSuccess)) + + err := profileClient.Delete(context.TODO(), "test-Random-Boolean-Device", clients.DeleteOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v2/deviceprofile/name/test-Random-Boolean-Device", + httpmock.NewStringResponder(404, ProfileDeleteFail)) + + err = profileClient.Delete(context.TODO(), "test-Random-Boolean-Device", clients.DeleteOptions{}) + assert.NotNil(t, err) +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client.go b/pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client.go new file mode 100644 index 00000000000..c7cd9f70f24 --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client.go @@ -0,0 +1,166 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package edgex_foundry + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses" + "github.com/go-resty/resty/v2" + "k8s.io/klog/v2" + + "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + edgeCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +type EdgexDeviceServiceClient struct { + *resty.Client + CoreMetaAddr string +} + +func NewEdgexDeviceServiceClient(coreMetaAddr string) *EdgexDeviceServiceClient { + return &EdgexDeviceServiceClient{ + Client: resty.New(), + CoreMetaAddr: coreMetaAddr, + } +} + +// Create function sends a POST request to EdgeX to add a new deviceService +func (eds *EdgexDeviceServiceClient) Create(ctx context.Context, deviceService *v1alpha1.DeviceService, options edgeCli.CreateOptions) (*v1alpha1.DeviceService, error) { + dss := []*v1alpha1.DeviceService{deviceService} + req := makeEdgeXDeviceService(dss) + klog.V(5).InfoS("will add the DeviceServices", "DeviceService", deviceService.Name) + jsonBody, err := json.Marshal(req) + if err != nil { + return nil, err + } + postPath := fmt.Sprintf("http://%s%s", eds.CoreMetaAddr, DeviceServicePath) + resp, err := eds.R(). + SetBody(jsonBody).Post(postPath) + if err != nil { + return nil, err + } else if resp.StatusCode() != http.StatusMultiStatus { + return nil, fmt.Errorf("create DeviceService on edgex foundry failed, the response is : %s", resp.Body()) + } + + var edgexResps []*common.BaseWithIdResponse + if err = json.Unmarshal(resp.Body(), &edgexResps); err != nil { + return nil, err + } + createdDeviceService := deviceService.DeepCopy() + if len(edgexResps) == 1 { + if edgexResps[0].StatusCode == http.StatusCreated { + createdDeviceService.Status.EdgeId = edgexResps[0].Id + createdDeviceService.Status.Synced = true + } else { + return nil, fmt.Errorf("create DeviceService on edgex foundry failed, the response is : %s", resp.Body()) + } + } else { + return nil, fmt.Errorf("edgex BaseWithIdResponse count mismatch DeviceService count, the response is : %s", resp.Body()) + } + return createdDeviceService, err +} + +// Delete function sends a request to EdgeX to delete a deviceService +func (eds *EdgexDeviceServiceClient) Delete(ctx context.Context, name string, option edgeCli.DeleteOptions) error { + klog.V(5).InfoS("will delete the DeviceService", "DeviceService", name) + delURL := fmt.Sprintf("http://%s%s/name/%s", eds.CoreMetaAddr, DeviceServicePath, name) + resp, err := eds.R().Delete(delURL) + if err != nil { + return err + } + if resp.StatusCode() != http.StatusOK { + return fmt.Errorf("delete edgex deviceservice err: %s", string(resp.Body())) + } + return nil +} + +// Update is used to set the admin or operating state of the deviceService by unique name of the deviceService. +// TODO support to update other fields +func (eds *EdgexDeviceServiceClient) Update(ctx context.Context, ds *v1alpha1.DeviceService, options edgeCli.UpdateOptions) (*v1alpha1.DeviceService, error) { + patchURL := fmt.Sprintf("http://%s%s", eds.CoreMetaAddr, DeviceServicePath) + if ds == nil { + return nil, nil + } + + if ds.Status.EdgeId == "" { + return nil, fmt.Errorf("failed to update deviceservice %s with empty edgex id", ds.Name) + } + edgeDs := toEdgexDeviceService(ds) + edgeDs.Id = ds.Status.EdgeId + dsJson, err := json.Marshal(&edgeDs) + if err != nil { + return nil, err + } + resp, err := eds.R(). + SetBody(dsJson).Patch(patchURL) + if err != nil { + return nil, err + } + + if resp.StatusCode() == http.StatusOK || resp.StatusCode() == http.StatusMultiStatus { + return ds, nil + } else { + return nil, fmt.Errorf("request to patch deviceservice failed, errcode:%d", resp.StatusCode()) + } +} + +// Get is used to query the deviceService information corresponding to the deviceService name +func (eds *EdgexDeviceServiceClient) Get(ctx context.Context, name string, options edgeCli.GetOptions) (*v1alpha1.DeviceService, error) { + klog.V(5).InfoS("will get DeviceServices", "DeviceService", name) + var dsResp responses.DeviceServiceResponse + getURL := fmt.Sprintf("http://%s%s/name/%s", eds.CoreMetaAddr, DeviceServicePath, name) + resp, err := eds.R().Get(getURL) + if err != nil { + return nil, err + } + if resp.StatusCode() == http.StatusNotFound { + return nil, fmt.Errorf("deviceservice %s not found", name) + } + err = json.Unmarshal(resp.Body(), &dsResp) + if err != nil { + return nil, err + } + ds := toKubeDeviceService(dsResp.Service, options.Namespace) + return &ds, nil +} + +// List is used to get all deviceService objects on edge platform +// The Hanoi version currently supports only a single label and does not support other filters +func (eds *EdgexDeviceServiceClient) List(ctx context.Context, options edgeCli.ListOptions) ([]v1alpha1.DeviceService, error) { + klog.V(5).Info("will list DeviceServices") + lp := fmt.Sprintf("http://%s%s/all?limit=-1", eds.CoreMetaAddr, DeviceServicePath) + resp, err := eds.R(). + EnableTrace(). + Get(lp) + if err != nil { + return nil, err + } + var mdsResponse responses.MultiDeviceServicesResponse + if err := json.Unmarshal(resp.Body(), &mdsResponse); err != nil { + return nil, err + } + var res []v1alpha1.DeviceService + for _, ds := range mdsResponse.Services { + res = append(res, toKubeDeviceService(ds, options.Namespace)) + } + return res, nil +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client_test.go b/pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client_test.go new file mode 100644 index 00000000000..c00d7b9118e --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client_test.go @@ -0,0 +1,126 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package edgex_foundry + +import ( + "context" + "encoding/json" + "testing" + + edgex_resp "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +const ( + DeviceServiceListMetaData = `{"apiVersion":"v2","statusCode":200,"totalCount":1,"services":[{"created":1661829206490,"modified":1661850999190,"id":"74516e96-973d-4cad-bad1-afd4b3a8ea46","name":"device-virtual","baseAddress":"http://edgex-device-virtual:59900","adminState":"UNLOCKED"}]}` + DeviceServiceMetaData = `{"apiVersion":"v2","statusCode":200,"service":{"created":1661829206490,"modified":1661850999190,"id":"74516e96-973d-4cad-bad1-afd4b3a8ea46","name":"device-virtual","baseAddress":"http://edgex-device-virtual:59900","adminState":"UNLOCKED"}}` + ServiceCreateSuccess = `[{"apiVersion":"v2","statusCode":201,"id":"a583b97d-7c4d-4b7c-8b93-51da9e68518c"}]` + ServiceCreateFail = `[{"apiVersion":"v2","message":"device service name test-device-virtual exists","statusCode":409}]` + + ServiceDeleteSuccess = `{"apiVersion":"v2","statusCode":200}` + ServiceDeleteFail = `{"apiVersion":"v2","message":"fail to delete the device profile with name test-Random-Boolean-Device","statusCode":404}` + + ServiceUpdateSuccess = `[{"apiVersion":"v2","statusCode":200}]` + ServiceUpdateFail = `[{"apiVersion":"v2","message":"fail to query object *models.DeviceService, because id: md|ds:01dfe04d-f361-41fd-b1c4-7ca0718f461a doesn't exist in the database","statusCode":404}]` +) + +var serviceClient = NewEdgexDeviceServiceClient("edgex-core-metadata:59881") + +func Test_GetService(t *testing.T) { + httpmock.ActivateNonDefault(serviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v2/deviceservice/name/device-virtual", + httpmock.NewStringResponder(200, DeviceServiceMetaData)) + + _, err := serviceClient.Get(context.TODO(), "device-virtual", clients.GetOptions{Namespace: "default"}) + assert.Nil(t, err) +} + +func Test_ListService(t *testing.T) { + httpmock.ActivateNonDefault(serviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v2/deviceservice/all?limit=-1", + httpmock.NewStringResponder(200, DeviceServiceListMetaData)) + + services, err := serviceClient.List(context.TODO(), clients.ListOptions{}) + assert.Nil(t, err) + assert.Equal(t, 1, len(services)) +} + +func Test_CreateService(t *testing.T) { + httpmock.ActivateNonDefault(serviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v2/deviceservice", + httpmock.NewStringResponder(207, ServiceCreateSuccess)) + + var resp edgex_resp.DeviceServiceResponse + + err := json.Unmarshal([]byte(DeviceServiceMetaData), &resp) + assert.Nil(t, err) + + service := toKubeDeviceService(resp.Service, "default") + service.Name = "test-device-virtual" + + _, err = serviceClient.Create(context.TODO(), &service, clients.CreateOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v2/deviceservice", + httpmock.NewStringResponder(207, ServiceCreateFail)) +} + +func Test_DeleteService(t *testing.T) { + httpmock.ActivateNonDefault(serviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v2/deviceservice/name/test-device-virtual", + httpmock.NewStringResponder(200, ServiceDeleteSuccess)) + + err := serviceClient.Delete(context.TODO(), "test-device-virtual", clients.DeleteOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v2/deviceservice/name/test-device-virtual", + httpmock.NewStringResponder(404, ServiceDeleteFail)) + + err = serviceClient.Delete(context.TODO(), "test-device-virtual", clients.DeleteOptions{}) + assert.NotNil(t, err) +} + +func Test_UpdateService(t *testing.T) { + httpmock.ActivateNonDefault(serviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("PATCH", "http://edgex-core-metadata:59881/api/v2/deviceservice", + httpmock.NewStringResponder(200, ServiceUpdateSuccess)) + var resp edgex_resp.DeviceServiceResponse + + err := json.Unmarshal([]byte(DeviceServiceMetaData), &resp) + assert.Nil(t, err) + + service := toKubeDeviceService(resp.Service, "default") + _, err = serviceClient.Update(context.TODO(), &service, clients.UpdateOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("PATCH", "http://edgex-core-metadata:59881/api/v2/deviceservice", + httpmock.NewStringResponder(404, ServiceUpdateFail)) + + _, err = serviceClient.Update(context.TODO(), &service, clients.UpdateOptions{}) + assert.NotNil(t, err) +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/edgexobject.go b/pkg/yurtiotdock/clients/edgex-foundry/edgexobject.go new file mode 100644 index 00000000000..4a093bf9834 --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/edgexobject.go @@ -0,0 +1,21 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package edgex_foundry + +type EdgeXObject interface { + IsAddedToEdgeX() bool +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/util.go b/pkg/yurtiotdock/clients/edgex-foundry/util.go new file mode 100644 index 00000000000..8fbf276ab30 --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/util.go @@ -0,0 +1,456 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package edgex_foundry + +import ( + "fmt" + "strings" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/requests" + "github.com/edgexfoundry/go-mod-core-contracts/v2/models" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" +) + +const ( + EdgeXObjectName = "yurt-iot-dock/edgex-object.name" + DeviceServicePath = "/api/v2/deviceservice" + DeviceProfilePath = "/api/v2/deviceprofile" + DevicePath = "/api/v2/device" + CommandResponsePath = "/api/v2/device" + + APIVersionV2 = "v2" +) + +type ClientURL struct { + Host string + Port int +} + +func getEdgeXName(provider metav1.Object) string { + var actualDeviceName string + if _, ok := provider.GetLabels()[EdgeXObjectName]; ok { + actualDeviceName = provider.GetLabels()[EdgeXObjectName] + } else { + actualDeviceName = provider.GetName() + } + return actualDeviceName +} + +func toEdgexDeviceService(ds *iotv1alpha1.DeviceService) dtos.DeviceService { + return dtos.DeviceService{ + Description: ds.Spec.Description, + Name: getEdgeXName(ds), + LastConnected: ds.Status.LastConnected, + LastReported: ds.Status.LastReported, + Labels: ds.Spec.Labels, + AdminState: string(ds.Spec.AdminState), + BaseAddress: ds.Spec.BaseAddress, + } +} + +func toEdgeXDeviceResourceSlice(drs []iotv1alpha1.DeviceResource) []dtos.DeviceResource { + var ret []dtos.DeviceResource + for _, dr := range drs { + ret = append(ret, toEdgeXDeviceResource(dr)) + } + return ret +} + +func toEdgeXDeviceResource(dr iotv1alpha1.DeviceResource) dtos.DeviceResource { + genericAttrs := make(map[string]interface{}) + for k, v := range dr.Attributes { + genericAttrs[k] = v + } + + return dtos.DeviceResource{ + Description: dr.Description, + Name: dr.Name, + Tag: dr.Tag, + Properties: toEdgeXProfileProperty(dr.Properties), + Attributes: genericAttrs, + } +} + +func toEdgeXProfileProperty(pp iotv1alpha1.ResourceProperties) dtos.ResourceProperties { + return dtos.ResourceProperties{ + ReadWrite: pp.ReadWrite, + Minimum: pp.Minimum, + Maximum: pp.Maximum, + DefaultValue: pp.DefaultValue, + Mask: pp.Mask, + Shift: pp.Shift, + Scale: pp.Scale, + Offset: pp.Offset, + Base: pp.Base, + Assertion: pp.Assertion, + MediaType: pp.MediaType, + Units: pp.Units, + ValueType: pp.ValueType, + } +} + +func toKubeDeviceService(ds dtos.DeviceService, namespace string) iotv1alpha1.DeviceService { + return iotv1alpha1.DeviceService{ + ObjectMeta: metav1.ObjectMeta{ + Name: toKubeName(ds.Name), + Namespace: namespace, + Labels: map[string]string{ + EdgeXObjectName: ds.Name, + }, + }, + Spec: iotv1alpha1.DeviceServiceSpec{ + Description: ds.Description, + Labels: ds.Labels, + AdminState: iotv1alpha1.AdminState(ds.AdminState), + BaseAddress: ds.BaseAddress, + }, + Status: iotv1alpha1.DeviceServiceStatus{ + EdgeId: ds.Id, + LastConnected: ds.LastConnected, + LastReported: ds.LastReported, + AdminState: iotv1alpha1.AdminState(ds.AdminState), + }, + } +} + +func toEdgeXDevice(d *iotv1alpha1.Device) dtos.Device { + md := dtos.Device{ + Description: d.Spec.Description, + Name: getEdgeXName(d), + AdminState: string(toEdgeXAdminState(d.Spec.AdminState)), + OperatingState: string(toEdgeXOperatingState(d.Spec.OperatingState)), + Protocols: toEdgeXProtocols(d.Spec.Protocols), + LastConnected: d.Status.LastConnected, + LastReported: d.Status.LastReported, + Labels: d.Spec.Labels, + Location: d.Spec.Location, + ServiceName: d.Spec.Service, + ProfileName: d.Spec.Profile, + } + if d.Status.EdgeId != "" { + md.Id = d.Status.EdgeId + } + return md +} + +func toEdgeXUpdateDevice(d *iotv1alpha1.Device) dtos.UpdateDevice { + adminState := string(toEdgeXAdminState(d.Spec.AdminState)) + operationState := string(toEdgeXOperatingState(d.Spec.OperatingState)) + md := dtos.UpdateDevice{ + Description: &d.Spec.Description, + Name: &d.Name, + AdminState: &adminState, + OperatingState: &operationState, + Protocols: toEdgeXProtocols(d.Spec.Protocols), + LastConnected: &d.Status.LastConnected, + LastReported: &d.Status.LastReported, + Labels: d.Spec.Labels, + Location: d.Spec.Location, + ServiceName: &d.Spec.Service, + ProfileName: &d.Spec.Profile, + Notify: &d.Spec.Notify, + } + if d.Status.EdgeId != "" { + md.Id = &d.Status.EdgeId + } + return md +} + +func toEdgeXProtocols( + pps map[string]iotv1alpha1.ProtocolProperties) map[string]dtos.ProtocolProperties { + ret := map[string]dtos.ProtocolProperties{} + for k, v := range pps { + ret[k] = dtos.ProtocolProperties(v) + } + return ret +} + +func toEdgeXAdminState(as iotv1alpha1.AdminState) models.AdminState { + if as == iotv1alpha1.Locked { + return models.Locked + } + return models.Unlocked +} + +func toEdgeXOperatingState(os iotv1alpha1.OperatingState) models.OperatingState { + if os == iotv1alpha1.Up { + return models.Up + } else if os == iotv1alpha1.Down { + return models.Down + } + return models.Unknown +} + +// toKubeDevice serialize the EdgeX Device to the corresponding Kubernetes Device +func toKubeDevice(ed dtos.Device, namespace string) iotv1alpha1.Device { + var loc string + if ed.Location != nil { + loc = ed.Location.(string) + } + return iotv1alpha1.Device{ + ObjectMeta: metav1.ObjectMeta{ + Name: toKubeName(ed.Name), + Namespace: namespace, + Labels: map[string]string{ + EdgeXObjectName: ed.Name, + }, + }, + Spec: iotv1alpha1.DeviceSpec{ + Description: ed.Description, + AdminState: iotv1alpha1.AdminState(ed.AdminState), + OperatingState: iotv1alpha1.OperatingState(ed.OperatingState), + Protocols: toKubeProtocols(ed.Protocols), + Labels: ed.Labels, + Location: loc, + Service: ed.ServiceName, + Profile: ed.ProfileName, + // TODO: Notify + }, + Status: iotv1alpha1.DeviceStatus{ + LastConnected: ed.LastConnected, + LastReported: ed.LastReported, + Synced: true, + EdgeId: ed.Id, + AdminState: iotv1alpha1.AdminState(ed.AdminState), + OperatingState: iotv1alpha1.OperatingState(ed.OperatingState), + }, + } +} + +// toKubeProtocols serialize the EdgeX ProtocolProperties to the corresponding +// Kubernetes OperatingState +func toKubeProtocols( + eps map[string]dtos.ProtocolProperties) map[string]iotv1alpha1.ProtocolProperties { + ret := map[string]iotv1alpha1.ProtocolProperties{} + for k, v := range eps { + ret[k] = iotv1alpha1.ProtocolProperties(v) + } + return ret +} + +// toKubeDeviceProfile create DeviceProfile in cloud according to devicProfile in edge +func toKubeDeviceProfile(dp *dtos.DeviceProfile, namespace string) iotv1alpha1.DeviceProfile { + return iotv1alpha1.DeviceProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: toKubeName(dp.Name), + Namespace: namespace, + Labels: map[string]string{ + EdgeXObjectName: dp.Name, + }, + }, + Spec: iotv1alpha1.DeviceProfileSpec{ + Description: dp.Description, + Manufacturer: dp.Manufacturer, + Model: dp.Model, + Labels: dp.Labels, + DeviceResources: toKubeDeviceResources(dp.DeviceResources), + DeviceCommands: toKubeDeviceCommand(dp.DeviceCommands), + }, + Status: iotv1alpha1.DeviceProfileStatus{ + EdgeId: dp.Id, + Synced: true, + }, + } +} + +func toKubeDeviceCommand(dcs []dtos.DeviceCommand) []iotv1alpha1.DeviceCommand { + var ret []iotv1alpha1.DeviceCommand + for _, dc := range dcs { + ret = append(ret, iotv1alpha1.DeviceCommand{ + Name: dc.Name, + ReadWrite: dc.ReadWrite, + IsHidden: dc.IsHidden, + ResourceOperations: toKubeResourceOperations(dc.ResourceOperations), + }) + } + return ret +} + +func toEdgeXDeviceCommand(dcs []iotv1alpha1.DeviceCommand) []dtos.DeviceCommand { + var ret []dtos.DeviceCommand + for _, dc := range dcs { + ret = append(ret, dtos.DeviceCommand{ + Name: dc.Name, + ReadWrite: dc.ReadWrite, + IsHidden: dc.IsHidden, + ResourceOperations: toEdgeXResourceOperations(dc.ResourceOperations), + }) + } + return ret +} + +func toKubeResourceOperations(ros []dtos.ResourceOperation) []iotv1alpha1.ResourceOperation { + var ret []iotv1alpha1.ResourceOperation + for _, ro := range ros { + ret = append(ret, iotv1alpha1.ResourceOperation{ + DeviceResource: ro.DeviceResource, + Mappings: ro.Mappings, + DefaultValue: ro.DefaultValue, + }) + } + return ret +} + +func toEdgeXResourceOperations(ros []iotv1alpha1.ResourceOperation) []dtos.ResourceOperation { + var ret []dtos.ResourceOperation + for _, ro := range ros { + ret = append(ret, dtos.ResourceOperation{ + DeviceResource: ro.DeviceResource, + Mappings: ro.Mappings, + DefaultValue: ro.DefaultValue, + }) + } + return ret +} + +func toKubeDeviceResources(drs []dtos.DeviceResource) []iotv1alpha1.DeviceResource { + var ret []iotv1alpha1.DeviceResource + for _, dr := range drs { + ret = append(ret, toKubeDeviceResource(dr)) + } + return ret +} + +func toKubeDeviceResource(dr dtos.DeviceResource) iotv1alpha1.DeviceResource { + concreteAttrs := make(map[string]string) + for k, v := range dr.Attributes { + switch asserted := v.(type) { + case string: + concreteAttrs[k] = asserted + continue + case int: + concreteAttrs[k] = fmt.Sprintf("%d", asserted) + continue + case float64: + concreteAttrs[k] = fmt.Sprintf("%f", asserted) + continue + case fmt.Stringer: + concreteAttrs[k] = asserted.String() + continue + } + } + + return iotv1alpha1.DeviceResource{ + Description: dr.Description, + Name: dr.Name, + Tag: dr.Tag, + IsHidden: dr.IsHidden, + Properties: toKubeProfileProperty(dr.Properties), + Attributes: concreteAttrs, + } +} + +func toKubeProfileProperty(rp dtos.ResourceProperties) iotv1alpha1.ResourceProperties { + return iotv1alpha1.ResourceProperties{ + ValueType: rp.ValueType, + ReadWrite: rp.ReadWrite, + Minimum: rp.Minimum, + Maximum: rp.Maximum, + DefaultValue: rp.DefaultValue, + Mask: rp.Mask, + Shift: rp.Shift, + Scale: rp.Scale, + Offset: rp.Offset, + Base: rp.Base, + Assertion: rp.Assertion, + MediaType: rp.MediaType, + Units: rp.Units, + } +} + +// toEdgeXDeviceProfile create DeviceProfile in edge according to devicProfile in cloud +func toEdgeXDeviceProfile(dp *iotv1alpha1.DeviceProfile) dtos.DeviceProfile { + return dtos.DeviceProfile{ + DeviceProfileBasicInfo: dtos.DeviceProfileBasicInfo{ + Description: dp.Spec.Description, + Name: getEdgeXName(dp), + Manufacturer: dp.Spec.Manufacturer, + Model: dp.Spec.Model, + Labels: dp.Spec.Labels, + }, + DeviceResources: toEdgeXDeviceResourceSlice(dp.Spec.DeviceResources), + DeviceCommands: toEdgeXDeviceCommand(dp.Spec.DeviceCommands), + } +} + +func makeEdgeXDeviceProfilesRequest(dps []*iotv1alpha1.DeviceProfile) []*requests.DeviceProfileRequest { + var req []*requests.DeviceProfileRequest + for _, dp := range dps { + req = append(req, &requests.DeviceProfileRequest{ + BaseRequest: common.BaseRequest{ + Versionable: common.Versionable{ + ApiVersion: APIVersionV2, + }, + }, + Profile: toEdgeXDeviceProfile(dp), + }) + } + return req +} + +func makeEdgeXDeviceUpdateRequest(devs []*iotv1alpha1.Device) []*requests.UpdateDeviceRequest { + var req []*requests.UpdateDeviceRequest + for _, dev := range devs { + req = append(req, &requests.UpdateDeviceRequest{ + BaseRequest: common.BaseRequest{ + Versionable: common.Versionable{ + ApiVersion: APIVersionV2, + }, + }, + Device: toEdgeXUpdateDevice(dev), + }) + } + return req +} + +func makeEdgeXDeviceRequest(devs []*iotv1alpha1.Device) []*requests.AddDeviceRequest { + var req []*requests.AddDeviceRequest + for _, dev := range devs { + req = append(req, &requests.AddDeviceRequest{ + BaseRequest: common.BaseRequest{ + Versionable: common.Versionable{ + ApiVersion: APIVersionV2, + }, + }, + Device: toEdgeXDevice(dev), + }) + } + return req +} + +func makeEdgeXDeviceService(dss []*iotv1alpha1.DeviceService) []*requests.AddDeviceServiceRequest { + var req []*requests.AddDeviceServiceRequest + for _, ds := range dss { + req = append(req, &requests.AddDeviceServiceRequest{ + BaseRequest: common.BaseRequest{ + Versionable: common.Versionable{ + ApiVersion: APIVersionV2, + }, + }, + Service: toEdgexDeviceService(ds), + }) + } + return req +} + +func toKubeName(edgexName string) string { + return strings.ReplaceAll(strings.ToLower(edgexName), "_", "-") +} diff --git a/pkg/yurtiotdock/clients/errors.go b/pkg/yurtiotdock/clients/errors.go new file mode 100644 index 00000000000..b5d6d5b6a2b --- /dev/null +++ b/pkg/yurtiotdock/clients/errors.go @@ -0,0 +1,29 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package clients + +import "strings" + +type NotFoundError struct{} + +func (e *NotFoundError) Error() string { return "Item not found" } + +func IsNotFoundErr(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), "not found") || strings.HasPrefix(err.Error(), "no item found") +} diff --git a/pkg/yurtiotdock/clients/interface.go b/pkg/yurtiotdock/clients/interface.go new file mode 100644 index 00000000000..93ffc3574bd --- /dev/null +++ b/pkg/yurtiotdock/clients/interface.go @@ -0,0 +1,93 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clients + +import ( + "context" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" +) + +// CreateOptions defines additional options when creating an object +// Additional general field definitions can be added +type CreateOptions struct{} + +// DeleteOptions defines additional options when deleting an object +// Additional general field definitions can be added +type DeleteOptions struct{} + +// UpdateOptions defines additional options when updating an object +// Additional general field definitions can be added +type UpdateOptions struct{} + +// GetOptions defines additional options when getting an object +// Additional general field definitions can be added +type GetOptions struct { + // Namespace represents the namespace to list for, or empty for + // non-namespaced objects, or to list across all namespaces. + Namespace string +} + +// ListOptions defines additional options when listing an object +type ListOptions struct { + // A selector to restrict the list of returned objects by their labels. + // Defaults to everything. + // +optional + LabelSelector map[string]string + // A selector to restrict the list of returned objects by their fields. + // Defaults to everything. + // +optional + FieldSelector map[string]string + // Namespace represents the namespace to list for, or empty for + // non-namespaced objects, or to list across all namespaces. + Namespace string +} + +// DeviceInterface defines the interfaces which used to create, delete, update, get and list Device objects on edge-side platform +type DeviceInterface interface { + DevicePropertyInterface + Create(ctx context.Context, device *iotv1alpha1.Device, options CreateOptions) (*iotv1alpha1.Device, error) + Delete(ctx context.Context, name string, options DeleteOptions) error + Update(ctx context.Context, device *iotv1alpha1.Device, options UpdateOptions) (*iotv1alpha1.Device, error) + Get(ctx context.Context, name string, options GetOptions) (*iotv1alpha1.Device, error) + List(ctx context.Context, options ListOptions) ([]iotv1alpha1.Device, error) +} + +// DevicePropertyInterface defines the interfaces which used to get, list and set the actual status value of the device properties +type DevicePropertyInterface interface { + GetPropertyState(ctx context.Context, propertyName string, device *iotv1alpha1.Device, options GetOptions) (*iotv1alpha1.ActualPropertyState, error) + UpdatePropertyState(ctx context.Context, propertyName string, device *iotv1alpha1.Device, options UpdateOptions) error + ListPropertiesState(ctx context.Context, device *iotv1alpha1.Device, options ListOptions) (map[string]iotv1alpha1.DesiredPropertyState, map[string]iotv1alpha1.ActualPropertyState, error) +} + +// DeviceServiceInterface defines the interfaces which used to create, delete, update, get and list DeviceService objects on edge-side platform +type DeviceServiceInterface interface { + Create(ctx context.Context, deviceService *iotv1alpha1.DeviceService, options CreateOptions) (*iotv1alpha1.DeviceService, error) + Delete(ctx context.Context, name string, options DeleteOptions) error + Update(ctx context.Context, deviceService *iotv1alpha1.DeviceService, options UpdateOptions) (*iotv1alpha1.DeviceService, error) + Get(ctx context.Context, name string, options GetOptions) (*iotv1alpha1.DeviceService, error) + List(ctx context.Context, options ListOptions) ([]iotv1alpha1.DeviceService, error) +} + +// DeviceProfileInterface defines the interfaces which used to create, delete, update, get and list DeviceProfile objects on edge-side platform +type DeviceProfileInterface interface { + Create(ctx context.Context, deviceProfile *iotv1alpha1.DeviceProfile, options CreateOptions) (*iotv1alpha1.DeviceProfile, error) + Delete(ctx context.Context, name string, options DeleteOptions) error + Update(ctx context.Context, deviceProfile *iotv1alpha1.DeviceProfile, options UpdateOptions) (*iotv1alpha1.DeviceProfile, error) + Get(ctx context.Context, name string, options GetOptions) (*iotv1alpha1.DeviceProfile, error) + List(ctx context.Context, options ListOptions) ([]iotv1alpha1.DeviceProfile, error) +} diff --git a/pkg/yurtiotdock/controllers/device_controller.go b/pkg/yurtiotdock/controllers/device_controller.go new file mode 100644 index 00000000000..dd61d5a6a96 --- /dev/null +++ b/pkg/yurtiotdock/controllers/device_controller.go @@ -0,0 +1,295 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "encoding/json" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" + edgexCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + util "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" +) + +// DeviceReconciler reconciles a Device object +type DeviceReconciler struct { + client.Client + Scheme *runtime.Scheme + deviceCli clients.DeviceInterface + // which nodePool deviceController is deployed in + NodePool string + Namespace string +} + +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=devices,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=devices/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=devices/finalizers,verbs=update + +func (r *DeviceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var d iotv1alpha1.Device + if err := r.Get(ctx, req.NamespacedName, &d); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // If objects doesn't belong to the Edge platform to which the controller is connected, the controller does not handle events for that object + if d.Spec.NodePool != r.NodePool { + return ctrl.Result{}, nil + } + klog.V(3).Infof("Reconciling the Device: %s", d.GetName()) + + deviceStatus := d.Status.DeepCopy() + // Update the conditions for device + defer func() { + if !d.Spec.Managed { + util.SetDeviceCondition(deviceStatus, util.NewDeviceCondition(iotv1alpha1.DeviceManagingCondition, corev1.ConditionFalse, iotv1alpha1.DeviceManagingReason, "")) + } + + err := r.Status().Update(ctx, &d) + if client.IgnoreNotFound(err) != nil { + if !apierrors.IsConflict(err) { + klog.V(4).ErrorS(err, "update device conditions failed", "DeviceName", d.GetName()) + } + } + }() + + // 1. Handle the device deletion event + if err := r.reconcileDeleteDevice(ctx, &d); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } else if !d.ObjectMeta.DeletionTimestamp.IsZero() { + return ctrl.Result{}, nil + } + + if !d.Status.Synced { + // 2. Synchronize OpenYurt device objects to edge platform + if err := r.reconcileCreateDevice(ctx, &d, deviceStatus); err != nil { + if apierrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } else { + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil + } else if d.Spec.Managed { + // 3. If the device has been synchronized and is managed by the cloud, reconcile the device properties + if err := r.reconcileUpdateDevice(ctx, &d, deviceStatus); err != nil { + if apierrors.IsConflict(err) { + return ctrl.Result{RequeueAfter: time.Second * 2}, nil + } + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *DeviceReconciler) SetupWithManager(mgr ctrl.Manager, opts *options.YurtIoTDockOptions) error { + r.deviceCli = edgexCli.NewEdgexDeviceClient(opts.CoreMetadataAddr, opts.CoreCommandAddr) + r.NodePool = opts.Nodepool + r.Namespace = opts.Namespace + + return ctrl.NewControllerManagedBy(mgr). + For(&iotv1alpha1.Device{}). + WithEventFilter(genFirstUpdateFilter("device")). + Complete(r) +} + +func (r *DeviceReconciler) reconcileDeleteDevice(ctx context.Context, d *iotv1alpha1.Device) error { + // gets the actual name of the device on the Edge platform from the Label of the device + edgeDeviceName := util.GetEdgeDeviceName(d, EdgeXObjectName) + if d.ObjectMeta.DeletionTimestamp.IsZero() { + if len(d.GetFinalizers()) == 0 { + patchData, _ := json.Marshal(map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": []string{iotv1alpha1.DeviceFinalizer}, + }, + }) + if err := r.Patch(ctx, d, client.RawPatch(types.MergePatchType, patchData)); err != nil { + return err + } + } + } else { + // delete the device object on the edge platform + err := r.deviceCli.Delete(context.TODO(), edgeDeviceName, clients.DeleteOptions{}) + if err != nil && !clients.IsNotFoundErr(err) { + return err + } + + // delete the device in OpenYurt + patchData, _ := json.Marshal(map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": []string{}, + }, + }) + if err = r.Patch(ctx, d, client.RawPatch(types.MergePatchType, patchData)); err != nil { + return err + } + } + return nil +} + +func (r *DeviceReconciler) reconcileCreateDevice(ctx context.Context, d *iotv1alpha1.Device, deviceStatus *iotv1alpha1.DeviceStatus) error { + // get the actual name of the device on the Edge platform from the Label of the device + edgeDeviceName := util.GetEdgeDeviceName(d, EdgeXObjectName) + newDeviceStatus := d.Status.DeepCopy() + klog.V(4).Infof("Checking if device already exist on the edge platform: %s", d.GetName()) + // Checking if device already exist on the edge platform + edgeDevice, err := r.deviceCli.Get(context.TODO(), edgeDeviceName, clients.GetOptions{Namespace: r.Namespace}) + if err == nil { + // a. If object exists, the status of the device on OpenYurt is updated + klog.V(4).Infof("Device already exists on edge platform: %s", d.GetName()) + newDeviceStatus.EdgeId = edgeDevice.Status.EdgeId + newDeviceStatus.Synced = true + } else if clients.IsNotFoundErr(err) { + // b. If the object does not exist, a request is sent to the edge platform to create a new device + klog.V(4).Infof("Adding device to the edge platform: %s", d.GetName()) + createdEdgeObj, err := r.deviceCli.Create(context.TODO(), d, clients.CreateOptions{}) + if err != nil { + util.SetDeviceCondition(deviceStatus, util.NewDeviceCondition(iotv1alpha1.DeviceSyncedCondition, corev1.ConditionFalse, iotv1alpha1.DeviceCreateSyncedReason, err.Error())) + return fmt.Errorf("fail to add Device to edge platform: %v", err) + } else { + klog.V(4).Infof("Successfully add Device to edge platform, Name: %s, EdgeId: %s", edgeDeviceName, createdEdgeObj.Status.EdgeId) + newDeviceStatus.EdgeId = createdEdgeObj.Status.EdgeId + newDeviceStatus.Synced = true + } + } else { + klog.V(4).ErrorS(err, "failed to visit the edge platform") + util.SetDeviceCondition(deviceStatus, util.NewDeviceCondition(iotv1alpha1.DeviceSyncedCondition, corev1.ConditionFalse, iotv1alpha1.DeviceVistedCoreMetadataSyncedReason, "")) + return nil + } + d.Status = *newDeviceStatus + util.SetDeviceCondition(deviceStatus, util.NewDeviceCondition(iotv1alpha1.DeviceSyncedCondition, corev1.ConditionTrue, "", "")) + + return r.Status().Update(ctx, d) +} + +func (r *DeviceReconciler) reconcileUpdateDevice(ctx context.Context, d *iotv1alpha1.Device, deviceStatus *iotv1alpha1.DeviceStatus) error { + // the device has been added to the edge platform, check if each device property are in the desired state + newDeviceStatus := d.Status.DeepCopy() + // This list is used to hold the names of properties that failed to reconcile + var failedPropertyNames []string + + // 1. reconciling the AdminState and OperatingState field of device + klog.V(3).Infof("DeviceName: %s, reconciling the AdminState and OperatingState field of device", d.GetName()) + updateDevice := d.DeepCopy() + if d.Spec.AdminState != "" && d.Spec.AdminState != d.Status.AdminState { + newDeviceStatus.AdminState = d.Spec.AdminState + } else { + updateDevice.Spec.AdminState = "" + } + + if d.Spec.OperatingState != "" && d.Spec.OperatingState != d.Status.OperatingState { + newDeviceStatus.OperatingState = d.Spec.OperatingState + } else { + updateDevice.Spec.OperatingState = "" + } + _, err := r.deviceCli.Update(context.TODO(), updateDevice, clients.UpdateOptions{}) + if err != nil { + util.SetDeviceCondition(deviceStatus, util.NewDeviceCondition(iotv1alpha1.DeviceManagingCondition, corev1.ConditionFalse, iotv1alpha1.DeviceUpdateStateReason, err.Error())) + return err + } + + // 2. reconciling the device properties' value + klog.V(3).Infof("DeviceName: %s, reconciling the device properties", d.GetName()) + // property updates are made only when the device is up and unlocked + if newDeviceStatus.OperatingState == iotv1alpha1.Up && newDeviceStatus.AdminState == iotv1alpha1.UnLocked { + newDeviceStatus, failedPropertyNames = r.reconcileDeviceProperties(d, newDeviceStatus) + } + + d.Status = *newDeviceStatus + + // 3. update the device status on OpenYurt + klog.V(3).Infof("DeviceName: %s, update the device status", d.GetName()) + if err := r.Status().Update(ctx, d); err != nil { + util.SetDeviceCondition(deviceStatus, util.NewDeviceCondition(iotv1alpha1.DeviceManagingCondition, corev1.ConditionFalse, iotv1alpha1.DeviceUpdateStateReason, err.Error())) + return err + } else if len(failedPropertyNames) != 0 { + err = fmt.Errorf("the following device properties failed to reconcile: %v", failedPropertyNames) + util.SetDeviceCondition(deviceStatus, util.NewDeviceCondition(iotv1alpha1.DeviceManagingCondition, corev1.ConditionFalse, err.Error(), "")) + return nil + } + + util.SetDeviceCondition(deviceStatus, util.NewDeviceCondition(iotv1alpha1.DeviceManagingCondition, corev1.ConditionTrue, "", "")) + return nil +} + +// Update the actual property value of the device on edge platform, +// return the latest status and the names of the property that failed to update +func (r *DeviceReconciler) reconcileDeviceProperties(d *iotv1alpha1.Device, deviceStatus *iotv1alpha1.DeviceStatus) (*iotv1alpha1.DeviceStatus, []string) { + newDeviceStatus := deviceStatus.DeepCopy() + // This list is used to hold the names of properties that failed to reconcile + var failedPropertyNames []string + // 2. reconciling the device properties' value + klog.V(3).Infof("DeviceName: %s, reconciling the value of device properties", d.GetName()) + for _, desiredProperty := range d.Spec.DeviceProperties { + if desiredProperty.DesiredValue == "" { + continue + } + propertyName := desiredProperty.Name + // 1.1. gets the actual property value of the current device from edge platform + klog.V(4).Infof("DeviceName: %s, getting the actual value of property: %s", d.GetName(), propertyName) + actualProperty, err := r.deviceCli.GetPropertyState(context.TODO(), propertyName, d, clients.GetOptions{}) + if err != nil { + if !clients.IsNotFoundErr(err) { + klog.Errorf("DeviceName: %s, failed to get actual property value of %s, err:%v", d.GetName(), propertyName, err) + failedPropertyNames = append(failedPropertyNames, propertyName) + continue + } + klog.Errorf("DeviceName: %s, property read command not found", d.GetName()) + } else { + klog.V(4).Infof("DeviceName: %s, got the actual property state, {Name: %s, GetURL: %s, ActualValue: %s}", + d.GetName(), propertyName, actualProperty.GetURL, actualProperty.ActualValue) + } + + if newDeviceStatus.DeviceProperties == nil { + newDeviceStatus.DeviceProperties = map[string]iotv1alpha1.ActualPropertyState{} + } else { + newDeviceStatus.DeviceProperties[propertyName] = *actualProperty + } + + // 1.2. set the device attribute in the edge platform to the expected value + if actualProperty == nil || desiredProperty.DesiredValue != actualProperty.ActualValue { + klog.V(4).Infof("DeviceName: %s, the desired value and the actual value are different, desired: %s, actual: %s", + d.GetName(), desiredProperty.DesiredValue, actualProperty.ActualValue) + if err := r.deviceCli.UpdatePropertyState(context.TODO(), propertyName, d, clients.UpdateOptions{}); err != nil { + klog.ErrorS(err, "failed to update property", "DeviceName", d.GetName(), "propertyName", propertyName) + failedPropertyNames = append(failedPropertyNames, propertyName) + continue + } + + klog.V(4).Infof("DeviceName: %s, successfully set the property %s to desired value", d.GetName(), propertyName) + newActualProperty := iotv1alpha1.ActualPropertyState{ + Name: propertyName, + GetURL: actualProperty.GetURL, + ActualValue: desiredProperty.DesiredValue, + } + newDeviceStatus.DeviceProperties[propertyName] = newActualProperty + } + } + return newDeviceStatus, failedPropertyNames +} diff --git a/pkg/yurtiotdock/controllers/device_syncer.go b/pkg/yurtiotdock/controllers/device_syncer.go new file mode 100644 index 00000000000..501ba6101dc --- /dev/null +++ b/pkg/yurtiotdock/controllers/device_syncer.go @@ -0,0 +1,237 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "strings" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlmgr "sigs.k8s.io/controller-runtime/pkg/manager" + + "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + edgeCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" + efCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" +) + +type DeviceSyncer struct { + // kubernetes client + client.Client + // which nodePool deviceController is deployed in + NodePool string + // edge platform's client + deviceCli edgeCli.DeviceInterface + // syncing period in seconds + syncPeriod time.Duration + Namespace string +} + +// NewDeviceSyncer initialize a New DeviceSyncer +func NewDeviceSyncer(client client.Client, opts *options.YurtIoTDockOptions) (DeviceSyncer, error) { + return DeviceSyncer{ + syncPeriod: time.Duration(opts.EdgeSyncPeriod) * time.Second, + deviceCli: efCli.NewEdgexDeviceClient(opts.CoreMetadataAddr, opts.CoreCommandAddr), + Client: client, + NodePool: opts.Nodepool, + Namespace: opts.Namespace, + }, nil +} + +// NewDeviceSyncerRunnable initialize a controller-runtime manager runnable +func (ds *DeviceSyncer) NewDeviceSyncerRunnable() ctrlmgr.RunnableFunc { + return func(ctx context.Context) error { + ds.Run(ctx.Done()) + return nil + } +} + +func (ds *DeviceSyncer) Run(stop <-chan struct{}) { + klog.V(1).Info("[Device] Starting the syncer...") + go func() { + for { + <-time.After(ds.syncPeriod) + klog.V(2).Info("[Device] Start a round of synchronization.") + // 1. get device on edge platform and OpenYurt + edgeDevices, kubeDevices, err := ds.getAllDevices() + if err != nil { + klog.V(3).ErrorS(err, "fail to list the devices") + continue + } + + // 2. find the device that need to be synchronized + redundantEdgeDevices, redundantKubeDevices, syncedDevices := ds.findDiffDevice(edgeDevices, kubeDevices) + klog.V(2).Infof("[Device] The number of objects waiting for synchronization { %s:%d, %s:%d, %s:%d }", + "Edge device should be added to OpenYurt", len(redundantEdgeDevices), + "OpenYurt device that should be deleted", len(redundantKubeDevices), + "Devices that should be synchronized", len(syncedDevices)) + + // 3. create device on OpenYurt which are exists in edge platform but not in OpenYurt + if err := ds.syncEdgeToKube(redundantEdgeDevices); err != nil { + klog.V(3).ErrorS(err, "fail to create devices on OpenYurt") + } + + // 4. delete redundant device on OpenYurt + if err := ds.deleteDevices(redundantKubeDevices); err != nil { + klog.V(3).ErrorS(err, "fail to delete redundant devices on OpenYurt") + } + + // 5. update device status on OpenYurt + if err := ds.updateDevices(syncedDevices); err != nil { + klog.V(3).ErrorS(err, "fail to update devices status") + } + klog.V(2).Info("[Device] One round of synchronization is complete") + } + }() + + <-stop + klog.V(1).Info("[Device] Stopping the syncer") +} + +// Get the existing Device on the Edge platform, as well as OpenYurt existing Device +// edgeDevice:map[actualName]device +// kubeDevice:map[actualName]device +func (ds *DeviceSyncer) getAllDevices() (map[string]iotv1alpha1.Device, map[string]iotv1alpha1.Device, error) { + edgeDevice := map[string]iotv1alpha1.Device{} + kubeDevice := map[string]iotv1alpha1.Device{} + // 1. list devices on edge platform + eDevs, err := ds.deviceCli.List(context.TODO(), edgeCli.ListOptions{Namespace: ds.Namespace}) + if err != nil { + klog.V(4).ErrorS(err, "fail to list the devices object on the Edge Platform") + return edgeDevice, kubeDevice, err + } + // 2. list devices on OpenYurt (filter objects belonging to edgeServer) + var kDevs iotv1alpha1.DeviceList + listOptions := client.MatchingFields{util.IndexerPathForNodepool: ds.NodePool} + if err = ds.List(context.TODO(), &kDevs, listOptions, client.InNamespace(ds.Namespace)); err != nil { + klog.V(4).ErrorS(err, "fail to list the devices object on the OpenYurt") + return edgeDevice, kubeDevice, err + } + for i := range eDevs { + deviceName := util.GetEdgeDeviceName(&eDevs[i], EdgeXObjectName) + edgeDevice[deviceName] = eDevs[i] + } + + for i := range kDevs.Items { + deviceName := util.GetEdgeDeviceName(&kDevs.Items[i], EdgeXObjectName) + kubeDevice[deviceName] = kDevs.Items[i] + } + return edgeDevice, kubeDevice, nil +} + +// Get the list of devices that need to be added, deleted and updated +func (ds *DeviceSyncer) findDiffDevice( + edgeDevices map[string]iotv1alpha1.Device, kubeDevices map[string]iotv1alpha1.Device) ( + redundantEdgeDevices map[string]*iotv1alpha1.Device, redundantKubeDevices map[string]*iotv1alpha1.Device, syncedDevices map[string]*iotv1alpha1.Device) { + + redundantEdgeDevices = map[string]*iotv1alpha1.Device{} + redundantKubeDevices = map[string]*iotv1alpha1.Device{} + syncedDevices = map[string]*iotv1alpha1.Device{} + + for i := range edgeDevices { + ed := edgeDevices[i] + edName := util.GetEdgeDeviceName(&ed, EdgeXObjectName) + if _, exists := kubeDevices[edName]; !exists { + klog.V(5).Infof("found redundant edge device %s", edName) + redundantEdgeDevices[edName] = ds.completeCreateContent(&ed) + } else { + klog.V(5).Infof("found device %s to be synced", edName) + kd := kubeDevices[edName] + syncedDevices[edName] = ds.completeUpdateContent(&kd, &ed) + } + } + + for i := range kubeDevices { + kd := kubeDevices[i] + if !kd.Status.Synced { + continue + } + kdName := util.GetEdgeDeviceName(&kd, EdgeXObjectName) + if _, exists := edgeDevices[kdName]; !exists { + redundantKubeDevices[kdName] = &kd + } + } + return +} + +// syncEdgeToKube creates device on OpenYurt which are exists in edge platform but not in OpenYurt +func (ds *DeviceSyncer) syncEdgeToKube(edgeDevs map[string]*iotv1alpha1.Device) error { + for _, ed := range edgeDevs { + if err := ds.Client.Create(context.TODO(), ed); err != nil { + if apierrors.IsAlreadyExists(err) { + continue + } + klog.V(5).ErrorS(err, "fail to create device on OpenYurt", "DeviceName", strings.ToLower(ed.Name)) + return err + } + } + return nil +} + +// deleteDevices deletes redundant device on OpenYurt +func (ds *DeviceSyncer) deleteDevices(redundantKubeDevices map[string]*iotv1alpha1.Device) error { + for _, kd := range redundantKubeDevices { + if err := ds.Client.Delete(context.TODO(), kd); err != nil { + klog.V(5).ErrorS(err, "fail to delete the device on OpenYurt", + "DeviceName", kd.Name) + return err + } + } + return nil +} + +// updateDevicesStatus updates device status on OpenYurt +func (ds *DeviceSyncer) updateDevices(syncedDevices map[string]*iotv1alpha1.Device) error { + for n := range syncedDevices { + if err := ds.Client.Status().Update(context.TODO(), syncedDevices[n]); err != nil { + if apierrors.IsConflict(err) { + klog.V(5).InfoS("update Conflicts", "Device", syncedDevices[n].Name) + continue + } + return err + } + } + return nil +} + +// completeCreateContent completes the content of the device which will be created on OpenYurt +func (ds *DeviceSyncer) completeCreateContent(edgeDevice *iotv1alpha1.Device) *iotv1alpha1.Device { + createDevice := edgeDevice.DeepCopy() + createDevice.Spec.NodePool = ds.NodePool + createDevice.Name = strings.Join([]string{ds.NodePool, createDevice.Name}, "-") + createDevice.Namespace = ds.Namespace + createDevice.Spec.Managed = false + + return createDevice +} + +// completeUpdateContent completes the content of the device which will be updated on OpenYurt +func (ds *DeviceSyncer) completeUpdateContent(kubeDevice *iotv1alpha1.Device, edgeDevice *iotv1alpha1.Device) *iotv1alpha1.Device { + updatedDevice := kubeDevice.DeepCopy() + _, aps, _ := ds.deviceCli.ListPropertiesState(context.TODO(), updatedDevice, edgeCli.ListOptions{}) + // update device status + updatedDevice.Status.LastConnected = edgeDevice.Status.LastConnected + updatedDevice.Status.LastReported = edgeDevice.Status.LastReported + updatedDevice.Status.AdminState = edgeDevice.Status.AdminState + updatedDevice.Status.OperatingState = edgeDevice.Status.OperatingState + updatedDevice.Status.DeviceProperties = aps + return updatedDevice +} diff --git a/pkg/yurtiotdock/controllers/deviceprofile_controller.go b/pkg/yurtiotdock/controllers/deviceprofile_controller.go new file mode 100644 index 00000000000..d174cc2ebb6 --- /dev/null +++ b/pkg/yurtiotdock/controllers/deviceprofile_controller.go @@ -0,0 +1,165 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "encoding/json" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" + edgexclis "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" +) + +// DeviceProfileReconciler reconciles a DeviceProfile object +type DeviceProfileReconciler struct { + client.Client + Scheme *runtime.Scheme + edgeClient clients.DeviceProfileInterface + NodePool string + Namespace string +} + +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=deviceprofiles,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=deviceprofiles/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=deviceprofiles/finalizers,verbs=update + +// Reconcile make changes to a deviceprofile object in EdgeX based on it in Kubernetes +func (r *DeviceProfileReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var dp iotv1alpha1.DeviceProfile + if err := r.Get(ctx, req.NamespacedName, &dp); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + if dp.Spec.NodePool != r.NodePool { + return ctrl.Result{}, nil + } + klog.V(3).Infof("Reconciling the DeviceProfile: %s", dp.GetName()) + + // gets the actual name of deviceProfile on the edge platform from the Label of the deviceProfile + dpActualName := util.GetEdgeDeviceProfileName(&dp, EdgeXObjectName) + + // 1. Handle the deviceProfile deletion event + if err := r.reconcileDeleteDeviceProfile(ctx, &dp, dpActualName); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } else if !dp.ObjectMeta.DeletionTimestamp.IsZero() { + return ctrl.Result{}, nil + } + + if !dp.Status.Synced { + // 2. Synchronize OpenYurt deviceProfile to edge platform + if err := r.reconcileCreateDeviceProfile(ctx, &dp, dpActualName); err != nil { + if apierrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } else { + return ctrl.Result{}, err + } + } + } + // 3. Handle the deviceProfile update event + // TODO + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *DeviceProfileReconciler) SetupWithManager(mgr ctrl.Manager, opts *options.YurtIoTDockOptions) error { + r.edgeClient = edgexclis.NewEdgexDeviceProfile(opts.CoreMetadataAddr) + r.NodePool = opts.Nodepool + r.Namespace = opts.Namespace + + return ctrl.NewControllerManagedBy(mgr). + For(&iotv1alpha1.DeviceProfile{}). + WithEventFilter(genFirstUpdateFilter("deviceprofile")). + Complete(r) +} + +func (r *DeviceProfileReconciler) reconcileDeleteDeviceProfile(ctx context.Context, dp *iotv1alpha1.DeviceProfile, actualName string) error { + if dp.ObjectMeta.DeletionTimestamp.IsZero() { + if len(dp.GetFinalizers()) == 0 { + patchString := map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": []string{iotv1alpha1.DeviceProfileFinalizer}, + }, + } + if patchData, err := json.Marshal(patchString); err != nil { + return err + } else { + if err = r.Patch(ctx, dp, client.RawPatch(types.MergePatchType, patchData)); err != nil { + return err + } + } + } + } else { + patchString := map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": []string{}, + }, + } + // delete the deviceProfile in OpenYurt + if patchData, err := json.Marshal(patchString); err != nil { + return err + } else { + if err = r.Patch(ctx, dp, client.RawPatch(types.MergePatchType, patchData)); err != nil { + return err + } + } + + // delete the deviceProfile object on edge platform + err := r.edgeClient.Delete(context.TODO(), actualName, clients.DeleteOptions{}) + if err != nil && !clients.IsNotFoundErr(err) { + return err + } + } + return nil +} + +func (r *DeviceProfileReconciler) reconcileCreateDeviceProfile(ctx context.Context, dp *iotv1alpha1.DeviceProfile, actualName string) error { + klog.V(4).Infof("Checking if deviceProfile already exist on the edge platform: %s", dp.GetName()) + if edgeDp, err := r.edgeClient.Get(context.TODO(), actualName, clients.GetOptions{Namespace: r.Namespace}); err != nil { + if !clients.IsNotFoundErr(err) { + klog.V(4).ErrorS(err, "fail to visit the edge platform") + return nil + } + } else { + // a. If object exists, the status of the deviceProfile on OpenYurt is updated + klog.V(4).Info("DeviceProfile already exists on edge platform") + dp.Status.Synced = true + dp.Status.EdgeId = edgeDp.Status.EdgeId + return r.Status().Update(ctx, dp) + } + + // b. If object does not exist, a request is sent to the edge platform to create a new deviceProfile + createDp, err := r.edgeClient.Create(context.Background(), dp, clients.CreateOptions{}) + if err != nil { + klog.V(4).ErrorS(err, "failed to create deviceProfile on edge platform") + return fmt.Errorf("failed to add deviceProfile to edge platform: %v", err) + } + klog.V(3).Infof("Successfully add DeviceProfile to edge platform, Name: %s, EdgeId: %s", createDp.GetName(), createDp.Status.EdgeId) + dp.Status.EdgeId = createDp.Status.EdgeId + dp.Status.Synced = true + return r.Status().Update(ctx, dp) +} diff --git a/pkg/yurtiotdock/controllers/deviceprofile_syncer.go b/pkg/yurtiotdock/controllers/deviceprofile_syncer.go new file mode 100644 index 00000000000..72d67729b47 --- /dev/null +++ b/pkg/yurtiotdock/controllers/deviceprofile_syncer.go @@ -0,0 +1,214 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "strings" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlmgr "sigs.k8s.io/controller-runtime/pkg/manager" + + "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + devcli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" + edgexclis "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" +) + +type DeviceProfileSyncer struct { + // syncing period in seconds + syncPeriod time.Duration + // edge platform client + edgeClient devcli.DeviceProfileInterface + // Kubernetes client + client.Client + NodePool string + Namespace string +} + +// NewDeviceProfileSyncer initialize a New DeviceProfileSyncer +func NewDeviceProfileSyncer(client client.Client, opts *options.YurtIoTDockOptions) (DeviceProfileSyncer, error) { + return DeviceProfileSyncer{ + syncPeriod: time.Duration(opts.EdgeSyncPeriod) * time.Second, + edgeClient: edgexclis.NewEdgexDeviceProfile(opts.CoreMetadataAddr), + Client: client, + NodePool: opts.Nodepool, + Namespace: opts.Namespace, + }, nil +} + +// NewDeviceProfileSyncerRunnable initialize a controller-runtime manager runnable +func (dps *DeviceProfileSyncer) NewDeviceProfileSyncerRunnable() ctrlmgr.RunnableFunc { + return func(ctx context.Context) error { + dps.Run(ctx.Done()) + return nil + } +} + +func (dps *DeviceProfileSyncer) Run(stop <-chan struct{}) { + klog.V(1).Info("[DeviceProfile] Starting the syncer...") + go func() { + for { + <-time.After(dps.syncPeriod) + klog.V(2).Info("[DeviceProfile] Start a round of synchronization.") + + // 1. get deviceProfiles on edge platform and OpenYurt + edgeDeviceProfiles, kubeDeviceProfiles, err := dps.getAllDeviceProfiles() + if err != nil { + klog.V(3).ErrorS(err, "fail to list the deviceProfiles") + continue + } + + // 2. find the deviceProfiles that need to be synchronized + redundantEdgeDeviceProfiles, redundantKubeDeviceProfiles, syncedDeviceProfiles := + dps.findDiffDeviceProfiles(edgeDeviceProfiles, kubeDeviceProfiles) + klog.V(2).Infof("[DeviceProfile] The number of objects waiting for synchronization { %s:%d, %s:%d, %s:%d }", + "Edge deviceProfiles should be added to OpenYurt", len(redundantEdgeDeviceProfiles), + "OpenYurt deviceProfiles that should be deleted", len(redundantKubeDeviceProfiles), + "DeviceProfiles that should be synchronized", len(syncedDeviceProfiles)) + + // 3. create deviceProfiles on OpenYurt which are exists in edge platform but not in OpenYurt + if err := dps.syncEdgeToKube(redundantEdgeDeviceProfiles); err != nil { + klog.V(3).ErrorS(err, "fail to create deviceProfiles on OpenYurt") + } + + // 4. delete redundant deviceProfiles on OpenYurt + if err := dps.deleteDeviceProfiles(redundantKubeDeviceProfiles); err != nil { + klog.V(3).ErrorS(err, "fail to delete redundant deviceProfiles on OpenYurt") + } + + // 5. update deviceProfiles on OpenYurt + // TODO + } + }() + + <-stop + klog.V(1).Info("[DeviceProfile] Stopping the syncer") +} + +// Get the existing DeviceProfile on the Edge platform, as well as OpenYurt existing DeviceProfile +// edgeDeviceProfiles:map[actualName]DeviceProfile +// kubeDeviceProfiles:map[actualName]DeviceProfile +func (dps *DeviceProfileSyncer) getAllDeviceProfiles() ( + map[string]iotv1alpha1.DeviceProfile, map[string]iotv1alpha1.DeviceProfile, error) { + + edgeDeviceProfiles := map[string]iotv1alpha1.DeviceProfile{} + kubeDeviceProfiles := map[string]iotv1alpha1.DeviceProfile{} + + // 1. list deviceProfiles on edge platform + eDps, err := dps.edgeClient.List(context.TODO(), devcli.ListOptions{Namespace: dps.Namespace}) + if err != nil { + klog.V(4).ErrorS(err, "fail to list the deviceProfiles on the edge platform") + return edgeDeviceProfiles, kubeDeviceProfiles, err + } + // 2. list deviceProfiles on OpenYurt (filter objects belonging to edgeServer) + var kDps iotv1alpha1.DeviceProfileList + listOptions := client.MatchingFields{util.IndexerPathForNodepool: dps.NodePool} + if err = dps.List(context.TODO(), &kDps, listOptions, client.InNamespace(dps.Namespace)); err != nil { + klog.V(4).ErrorS(err, "fail to list the deviceProfiles on the Kubernetes") + return edgeDeviceProfiles, kubeDeviceProfiles, err + } + for i := range eDps { + deviceProfilesName := util.GetEdgeDeviceProfileName(&eDps[i], EdgeXObjectName) + edgeDeviceProfiles[deviceProfilesName] = eDps[i] + } + + for i := range kDps.Items { + deviceProfilesName := util.GetEdgeDeviceProfileName(&kDps.Items[i], EdgeXObjectName) + kubeDeviceProfiles[deviceProfilesName] = kDps.Items[i] + } + return edgeDeviceProfiles, kubeDeviceProfiles, nil +} + +// Get the list of deviceProfiles that need to be added, deleted and updated +func (dps *DeviceProfileSyncer) findDiffDeviceProfiles( + edgeDeviceProfiles map[string]iotv1alpha1.DeviceProfile, kubeDeviceProfiles map[string]iotv1alpha1.DeviceProfile) ( + redundantEdgeDeviceProfiles map[string]*iotv1alpha1.DeviceProfile, redundantKubeDeviceProfiles map[string]*iotv1alpha1.DeviceProfile, syncedDeviceProfiles map[string]*iotv1alpha1.DeviceProfile) { + + redundantEdgeDeviceProfiles = map[string]*iotv1alpha1.DeviceProfile{} + redundantKubeDeviceProfiles = map[string]*iotv1alpha1.DeviceProfile{} + syncedDeviceProfiles = map[string]*iotv1alpha1.DeviceProfile{} + + for i := range edgeDeviceProfiles { + edp := edgeDeviceProfiles[i] + edpName := util.GetEdgeDeviceProfileName(&edp, EdgeXObjectName) + if _, exists := kubeDeviceProfiles[edpName]; !exists { + redundantEdgeDeviceProfiles[edpName] = dps.completeCreateContent(&edp) + } else { + kdp := kubeDeviceProfiles[edpName] + syncedDeviceProfiles[edpName] = dps.completeUpdateContent(&kdp, &edp) + } + } + + for i := range kubeDeviceProfiles { + kdp := kubeDeviceProfiles[i] + if !kdp.Status.Synced { + continue + } + kdpName := util.GetEdgeDeviceProfileName(&kdp, EdgeXObjectName) + if _, exists := edgeDeviceProfiles[kdpName]; !exists { + redundantKubeDeviceProfiles[kdpName] = &kdp + } + } + return +} + +// completeCreateContent completes the content of the deviceProfile which will be created on OpenYurt +func (dps *DeviceProfileSyncer) completeCreateContent(edgeDps *iotv1alpha1.DeviceProfile) *iotv1alpha1.DeviceProfile { + createDeviceProfile := edgeDps.DeepCopy() + createDeviceProfile.Namespace = dps.Namespace + createDeviceProfile.Name = strings.Join([]string{dps.NodePool, createDeviceProfile.Name}, "-") + createDeviceProfile.Spec.NodePool = dps.NodePool + return createDeviceProfile +} + +// completeUpdateContent completes the content of the deviceProfile which will be updated on OpenYurt +// TODO +func (dps *DeviceProfileSyncer) completeUpdateContent(kubeDps *iotv1alpha1.DeviceProfile, edgeDS *iotv1alpha1.DeviceProfile) *iotv1alpha1.DeviceProfile { + return kubeDps +} + +// syncEdgeToKube creates deviceProfiles on OpenYurt which are exists in edge platform but not in OpenYurt +func (dps *DeviceProfileSyncer) syncEdgeToKube(edgeDps map[string]*iotv1alpha1.DeviceProfile) error { + for _, edp := range edgeDps { + if err := dps.Client.Create(context.TODO(), edp); err != nil { + if apierrors.IsAlreadyExists(err) { + klog.V(5).Infof("DeviceProfile already exist on Kubernetes: %s", strings.ToLower(edp.Name)) + continue + } + klog.Infof("created deviceProfile failed: %s", strings.ToLower(edp.Name)) + return err + } + } + return nil +} + +// deleteDeviceProfiles deletes redundant deviceProfiles on OpenYurt +func (dps *DeviceProfileSyncer) deleteDeviceProfiles(redundantKubeDeviceProfiles map[string]*iotv1alpha1.DeviceProfile) error { + for _, kdp := range redundantKubeDeviceProfiles { + if err := dps.Client.Delete(context.TODO(), kdp); err != nil { + klog.V(5).ErrorS(err, "fail to delete the DeviceProfile on Kubernetes: %s ", + "DeviceProfile", kdp.Name) + return err + } + } + return nil +} diff --git a/pkg/yurtiotdock/controllers/deviceservice_controller.go b/pkg/yurtiotdock/controllers/deviceservice_controller.go new file mode 100644 index 00000000000..2822558b5f0 --- /dev/null +++ b/pkg/yurtiotdock/controllers/deviceservice_controller.go @@ -0,0 +1,220 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "encoding/json" + "fmt" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" + edgexCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + util "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" +) + +// DeviceServiceReconciler reconciles a DeviceService object +type DeviceServiceReconciler struct { + client.Client + Scheme *runtime.Scheme + deviceServiceCli clients.DeviceServiceInterface + NodePool string + Namespace string +} + +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=deviceservices,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=deviceservices/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=deviceservices/finalizers,verbs=update + +func (r *DeviceServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var ds iotv1alpha1.DeviceService + if err := r.Get(ctx, req.NamespacedName, &ds); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // If objects doesn't belong to the edge platform to which the controller is connected, the controller does not handle events for that object + if ds.Spec.NodePool != r.NodePool { + return ctrl.Result{}, nil + } + klog.V(3).Infof("Reconciling the DeviceService: %s", ds.GetName()) + + deviceServiceStatus := ds.Status.DeepCopy() + // Update deviceService conditions + defer func() { + if !ds.Spec.Managed { + util.SetDeviceServiceCondition(deviceServiceStatus, util.NewDeviceServiceCondition(iotv1alpha1.DeviceServiceManagingCondition, corev1.ConditionFalse, iotv1alpha1.DeviceServiceManagingReason, "")) + } + + err := r.Status().Update(ctx, &ds) + if client.IgnoreNotFound(err) != nil { + if !apierrors.IsConflict(err) { + klog.V(4).ErrorS(err, "update deviceService conditions failed", "deviceService", ds.GetName()) + } + } + }() + + // 1. Handle the deviceService deletion event + if err := r.reconcileDeleteDeviceService(ctx, &ds); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } else if !ds.ObjectMeta.DeletionTimestamp.IsZero() { + return ctrl.Result{}, nil + } + + if !ds.Status.Synced { + // 2. Synchronize OpenYurt deviceService to edge platform + if err := r.reconcileCreateDeviceService(ctx, &ds, deviceServiceStatus); err != nil { + if apierrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } else { + return ctrl.Result{}, err + } + } + } else if ds.Spec.Managed { + // 3. If the deviceService has been synchronized and is managed by the cloud, reconcile the deviceService fields + if err := r.reconcileUpdateDeviceService(ctx, &ds, deviceServiceStatus); err != nil { + if apierrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } else { + return ctrl.Result{}, err + } + } + } + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *DeviceServiceReconciler) SetupWithManager(mgr ctrl.Manager, opts *options.YurtIoTDockOptions) error { + r.deviceServiceCli = edgexCli.NewEdgexDeviceServiceClient(opts.CoreMetadataAddr) + r.NodePool = opts.Nodepool + r.Namespace = opts.Namespace + + return ctrl.NewControllerManagedBy(mgr). + For(&iotv1alpha1.DeviceService{}). + Complete(r) +} + +func (r *DeviceServiceReconciler) reconcileDeleteDeviceService(ctx context.Context, ds *iotv1alpha1.DeviceService) error { + // gets the actual name of deviceService on the edge platform from the Label of the device + edgeDeviceServiceName := util.GetEdgeDeviceServiceName(ds, EdgeXObjectName) + if ds.ObjectMeta.DeletionTimestamp.IsZero() { + if len(ds.GetFinalizers()) == 0 { + patchString := map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": []string{iotv1alpha1.DeviceServiceFinalizer}, + }, + } + if patchData, err := json.Marshal(patchString); err != nil { + return err + } else { + if err = r.Patch(ctx, ds, client.RawPatch(types.MergePatchType, patchData)); err != nil { + return err + } + } + } + } else { + patchString := map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": []string{}, + }, + } + // delete the deviceService in OpenYurt + if patchData, err := json.Marshal(patchString); err != nil { + return err + } else { + if err = r.Patch(ctx, ds, client.RawPatch(types.MergePatchType, patchData)); err != nil { + return err + } + } + + // delete the deviceService object on edge platform + err := r.deviceServiceCli.Delete(context.TODO(), edgeDeviceServiceName, clients.DeleteOptions{}) + if err != nil && !clients.IsNotFoundErr(err) { + return err + } + } + return nil +} + +func (r *DeviceServiceReconciler) reconcileCreateDeviceService(ctx context.Context, ds *iotv1alpha1.DeviceService, deviceServiceStatus *iotv1alpha1.DeviceServiceStatus) error { + // get the actual name of deviceService on the Edge platform from the Label of the device + edgeDeviceServiceName := util.GetEdgeDeviceServiceName(ds, EdgeXObjectName) + klog.V(4).Infof("Checking if deviceService already exist on the edge platform: %s", ds.GetName()) + // Checking if deviceService already exist on the edge platform + if edgeDs, err := r.deviceServiceCli.Get(context.TODO(), edgeDeviceServiceName, clients.GetOptions{Namespace: r.Namespace}); err != nil { + if !clients.IsNotFoundErr(err) { + klog.V(4).ErrorS(err, "fail to visit the edge platform") + return nil + } else { + createdDs, err := r.deviceServiceCli.Create(context.TODO(), ds, clients.CreateOptions{}) + if err != nil { + klog.V(4).ErrorS(err, "failed to create deviceService on edge platform") + util.SetDeviceServiceCondition(deviceServiceStatus, util.NewDeviceServiceCondition(iotv1alpha1.DeviceServiceSyncedCondition, corev1.ConditionFalse, iotv1alpha1.DeviceServiceCreateSyncedReason, err.Error())) + return fmt.Errorf("fail to create DeviceService to edge platform: %v", err) + } + + klog.V(4).Infof("Successfully add DeviceService to Edge Platform, Name: %s, EdgeId: %s", ds.GetName(), createdDs.Status.EdgeId) + ds.Status.EdgeId = createdDs.Status.EdgeId + ds.Status.Synced = true + util.SetDeviceServiceCondition(deviceServiceStatus, util.NewDeviceServiceCondition(iotv1alpha1.DeviceServiceSyncedCondition, corev1.ConditionTrue, "", "")) + return r.Status().Update(ctx, ds) + } + } else { + // a. If object exists, the status of the device on OpenYurt is updated + klog.V(4).Infof("DeviceServiceName: %s, obj already exists on edge platform", ds.GetName()) + ds.Status.Synced = true + ds.Status.EdgeId = edgeDs.Status.EdgeId + return r.Status().Update(ctx, ds) + } +} + +func (r *DeviceServiceReconciler) reconcileUpdateDeviceService(ctx context.Context, ds *iotv1alpha1.DeviceService, deviceServiceStatus *iotv1alpha1.DeviceServiceStatus) error { + // 1. reconciling the AdminState field of deviceService + newDeviceServiceStatus := ds.Status.DeepCopy() + updateDeviceService := ds.DeepCopy() + + if ds.Spec.AdminState != "" && ds.Spec.AdminState != ds.Status.AdminState { + newDeviceServiceStatus.AdminState = ds.Spec.AdminState + } else { + updateDeviceService.Spec.AdminState = "" + } + + _, err := r.deviceServiceCli.Update(context.TODO(), updateDeviceService, clients.UpdateOptions{}) + if err != nil { + util.SetDeviceServiceCondition(deviceServiceStatus, util.NewDeviceServiceCondition(iotv1alpha1.DeviceServiceManagingCondition, corev1.ConditionFalse, iotv1alpha1.DeviceServiceUpdateStatusSyncedReason, err.Error())) + + return err + } + + // 2. update the device status on OpenYurt + ds.Status = *newDeviceServiceStatus + if err = r.Status().Update(ctx, ds); err != nil { + util.SetDeviceServiceCondition(deviceServiceStatus, util.NewDeviceServiceCondition(iotv1alpha1.DeviceServiceManagingCondition, corev1.ConditionFalse, iotv1alpha1.DeviceServiceUpdateStatusSyncedReason, err.Error())) + + return err + } + util.SetDeviceServiceCondition(deviceServiceStatus, util.NewDeviceServiceCondition(iotv1alpha1.DeviceServiceManagingCondition, corev1.ConditionTrue, "", "")) + return nil +} diff --git a/pkg/yurtiotdock/controllers/deviceservice_syncer.go b/pkg/yurtiotdock/controllers/deviceservice_syncer.go new file mode 100644 index 00000000000..78949c55069 --- /dev/null +++ b/pkg/yurtiotdock/controllers/deviceservice_syncer.go @@ -0,0 +1,237 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package controllers + +import ( + "context" + "strings" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlmgr "sigs.k8s.io/controller-runtime/pkg/manager" + + "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + iotcli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" + edgexCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" +) + +type DeviceServiceSyncer struct { + // Kubernetes client + client.Client + // syncing period in seconds + syncPeriod time.Duration + deviceServiceCli iotcli.DeviceServiceInterface + NodePool string + Namespace string +} + +func NewDeviceServiceSyncer(client client.Client, opts *options.YurtIoTDockOptions) (DeviceServiceSyncer, error) { + return DeviceServiceSyncer{ + syncPeriod: time.Duration(opts.EdgeSyncPeriod) * time.Second, + deviceServiceCli: edgexCli.NewEdgexDeviceServiceClient(opts.CoreMetadataAddr), + Client: client, + NodePool: opts.Nodepool, + Namespace: opts.Namespace, + }, nil +} + +func (ds *DeviceServiceSyncer) NewDeviceServiceSyncerRunnable() ctrlmgr.RunnableFunc { + return func(ctx context.Context) error { + ds.Run(ctx.Done()) + return nil + } +} + +func (ds *DeviceServiceSyncer) Run(stop <-chan struct{}) { + klog.V(1).Info("[DeviceService] Starting the syncer...") + go func() { + for { + <-time.After(ds.syncPeriod) + klog.V(2).Info("[DeviceService] Start a round of synchronization.") + // 1. get deviceServices on edge platform and OpenYurt + edgeDeviceServices, kubeDeviceServices, err := ds.getAllDeviceServices() + if err != nil { + klog.V(3).ErrorS(err, "fail to list the deviceServices") + continue + } + + // 2. find the deviceServices that need to be synchronized + redundantEdgeDeviceServices, redundantKubeDeviceServices, syncedDeviceServices := + ds.findDiffDeviceServices(edgeDeviceServices, kubeDeviceServices) + klog.V(2).Infof("[DeviceService] The number of objects waiting for synchronization { %s:%d, %s:%d, %s:%d }", + "Edge deviceServices should be added to OpenYurt", len(redundantEdgeDeviceServices), + "OpenYurt deviceServices that should be deleted", len(redundantKubeDeviceServices), + "DeviceServices that should be synchronized", len(syncedDeviceServices)) + + // 3. create deviceServices on OpenYurt which are exists in edge platform but not in OpenYurt + if err := ds.syncEdgeToKube(redundantEdgeDeviceServices); err != nil { + klog.V(3).ErrorS(err, "fail to create deviceServices on OpenYurt") + } + + // 4. delete redundant deviceServices on OpenYurt + if err := ds.deleteDeviceServices(redundantKubeDeviceServices); err != nil { + klog.V(3).ErrorS(err, "fail to delete redundant deviceServices on OpenYurt") + } + + // 5. update deviceService status on OpenYurt + if err := ds.updateDeviceServices(syncedDeviceServices); err != nil { + klog.V(3).ErrorS(err, "fail to update deviceServices") + } + klog.V(2).Info("[DeviceService] One round of synchronization is complete") + } + }() + + <-stop + klog.V(1).Info("[DeviceService] Stopping the syncer") +} + +// Get the existing DeviceService on the Edge platform, as well as OpenYurt existing DeviceService +// edgeDeviceServices:map[actualName]DeviceService +// kubeDeviceServices:map[actualName]DeviceService +func (ds *DeviceServiceSyncer) getAllDeviceServices() ( + map[string]iotv1alpha1.DeviceService, map[string]iotv1alpha1.DeviceService, error) { + + edgeDeviceServices := map[string]iotv1alpha1.DeviceService{} + kubeDeviceServices := map[string]iotv1alpha1.DeviceService{} + + // 1. list deviceServices on edge platform + eDevSs, err := ds.deviceServiceCli.List(context.TODO(), iotcli.ListOptions{Namespace: ds.Namespace}) + if err != nil { + klog.V(4).ErrorS(err, "fail to list the deviceServices object on the edge platform") + return edgeDeviceServices, kubeDeviceServices, err + } + // 2. list deviceServices on OpenYurt (filter objects belonging to edgeServer) + var kDevSs iotv1alpha1.DeviceServiceList + listOptions := client.MatchingFields{util.IndexerPathForNodepool: ds.NodePool} + if err = ds.List(context.TODO(), &kDevSs, listOptions, client.InNamespace(ds.Namespace)); err != nil { + klog.V(4).ErrorS(err, "fail to list the deviceServices object on the Kubernetes") + return edgeDeviceServices, kubeDeviceServices, err + } + for i := range eDevSs { + deviceServicesName := util.GetEdgeDeviceServiceName(&eDevSs[i], EdgeXObjectName) + edgeDeviceServices[deviceServicesName] = eDevSs[i] + } + + for i := range kDevSs.Items { + deviceServicesName := util.GetEdgeDeviceServiceName(&kDevSs.Items[i], EdgeXObjectName) + kubeDeviceServices[deviceServicesName] = kDevSs.Items[i] + } + return edgeDeviceServices, kubeDeviceServices, nil +} + +// Get the list of deviceServices that need to be added, deleted and updated +func (ds *DeviceServiceSyncer) findDiffDeviceServices( + edgeDeviceService map[string]iotv1alpha1.DeviceService, kubeDeviceService map[string]iotv1alpha1.DeviceService) ( + redundantEdgeDeviceServices map[string]*iotv1alpha1.DeviceService, redundantKubeDeviceServices map[string]*iotv1alpha1.DeviceService, syncedDeviceServices map[string]*iotv1alpha1.DeviceService) { + + redundantEdgeDeviceServices = map[string]*iotv1alpha1.DeviceService{} + redundantKubeDeviceServices = map[string]*iotv1alpha1.DeviceService{} + syncedDeviceServices = map[string]*iotv1alpha1.DeviceService{} + + for i := range edgeDeviceService { + eds := edgeDeviceService[i] + edName := util.GetEdgeDeviceServiceName(&eds, EdgeXObjectName) + if _, exists := kubeDeviceService[edName]; !exists { + redundantEdgeDeviceServices[edName] = ds.completeCreateContent(&eds) + } else { + kd := kubeDeviceService[edName] + syncedDeviceServices[edName] = ds.completeUpdateContent(&kd, &eds) + } + } + + for i := range kubeDeviceService { + kds := kubeDeviceService[i] + if !kds.Status.Synced { + continue + } + kdName := util.GetEdgeDeviceServiceName(&kds, EdgeXObjectName) + if _, exists := edgeDeviceService[kdName]; !exists { + redundantKubeDeviceServices[kdName] = &kds + } + } + return +} + +// syncEdgeToKube creates deviceServices on OpenYurt which are exists in edge platform but not in OpenYurt +func (ds *DeviceServiceSyncer) syncEdgeToKube(edgeDevs map[string]*iotv1alpha1.DeviceService) error { + for _, ed := range edgeDevs { + if err := ds.Client.Create(context.TODO(), ed); err != nil { + if apierrors.IsAlreadyExists(err) { + klog.V(5).InfoS("DeviceService already exist on Kubernetes", + "DeviceService", strings.ToLower(ed.Name)) + continue + } + klog.InfoS("created deviceService failed:", "DeviceService", strings.ToLower(ed.Name)) + return err + } + } + return nil +} + +// deleteDeviceServices deletes redundant deviceServices on OpenYurt +func (ds *DeviceServiceSyncer) deleteDeviceServices(redundantKubeDeviceServices map[string]*iotv1alpha1.DeviceService) error { + for _, kds := range redundantKubeDeviceServices { + if err := ds.Client.Delete(context.TODO(), kds); err != nil { + klog.V(5).ErrorS(err, "fail to delete the DeviceService on Kubernetes", + "DeviceService", kds.Name) + return err + } + } + return nil +} + +// updateDeviceServices updates deviceServices status on OpenYurt +func (ds *DeviceServiceSyncer) updateDeviceServices(syncedDeviceServices map[string]*iotv1alpha1.DeviceService) error { + for _, sd := range syncedDeviceServices { + if sd.ObjectMeta.ResourceVersion == "" { + continue + } + if err := ds.Client.Status().Update(context.TODO(), sd); err != nil { + if apierrors.IsConflict(err) { + klog.V(5).InfoS("update Conflicts", "DeviceService", sd.Name) + continue + } + klog.V(5).ErrorS(err, "fail to update the DeviceService on Kubernetes", + "DeviceService", sd.Name) + return err + } + } + return nil +} + +// completeCreateContent completes the content of the deviceService which will be created on OpenYurt +func (ds *DeviceServiceSyncer) completeCreateContent(edgeDS *iotv1alpha1.DeviceService) *iotv1alpha1.DeviceService { + createDevice := edgeDS.DeepCopy() + createDevice.Spec.NodePool = ds.NodePool + createDevice.Namespace = ds.Namespace + createDevice.Name = strings.Join([]string{ds.NodePool, edgeDS.Name}, "-") + createDevice.Spec.Managed = false + return createDevice +} + +// completeUpdateContent completes the content of the deviceService which will be updated on OpenYurt +func (ds *DeviceServiceSyncer) completeUpdateContent(kubeDS *iotv1alpha1.DeviceService, edgeDS *iotv1alpha1.DeviceService) *iotv1alpha1.DeviceService { + updatedDS := kubeDS.DeepCopy() + // update device status + updatedDS.Status.LastConnected = edgeDS.Status.LastConnected + updatedDS.Status.LastReported = edgeDS.Status.LastReported + updatedDS.Status.AdminState = edgeDS.Status.AdminState + return updatedDS +} diff --git a/pkg/yurtiotdock/controllers/predicate.go b/pkg/yurtiotdock/controllers/predicate.go new file mode 100644 index 00000000000..0c6dc5eadb5 --- /dev/null +++ b/pkg/yurtiotdock/controllers/predicate.go @@ -0,0 +1,48 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + edgexCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" +) + +func genFirstUpdateFilter(objKind string) predicate.Predicate { + return predicate.Funcs{ + // ignore the update event that is generated due to a + // new deviceprofile being added to the Edgex Foundry + UpdateFunc: func(e event.UpdateEvent) bool { + oldDp, ok := e.ObjectOld.(edgexCli.EdgeXObject) + if !ok { + klog.Infof("fail to assert object to deviceprofile, object kind is %s", objKind) + return false + } + newDp, ok := e.ObjectNew.(edgexCli.EdgeXObject) + if !ok { + klog.Infof("fail to assert object to deviceprofile, object kind is %s", objKind) + return false + } + if !oldDp.IsAddedToEdgeX() && newDp.IsAddedToEdgeX() { + return false + } + return true + }, + } +} diff --git a/pkg/yurtiotdock/controllers/util/conditions.go b/pkg/yurtiotdock/controllers/util/conditions.go new file mode 100644 index 00000000000..db892cc1459 --- /dev/null +++ b/pkg/yurtiotdock/controllers/util/conditions.go @@ -0,0 +1,120 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" +) + +// NewDeviceCondition creates a new Device condition. +func NewDeviceCondition(condType iotv1alpha1.DeviceConditionType, status corev1.ConditionStatus, reason, message string) *iotv1alpha1.DeviceCondition { + return &iotv1alpha1.DeviceCondition{ + Type: condType, + Status: status, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + } +} + +// GetDeviceCondition returns the condition with the provided type. +func GetDeviceCondition(status iotv1alpha1.DeviceStatus, condType iotv1alpha1.DeviceConditionType) *iotv1alpha1.DeviceCondition { + for i := range status.Conditions { + c := status.Conditions[i] + if c.Type == condType { + return &c + } + } + return nil +} + +// SetDeviceCondition updates the Device to include the provided condition. If the condition that +// we are about to add already exists and has the same status, reason and message then we are not going to update. +func SetDeviceCondition(status *iotv1alpha1.DeviceStatus, condition *iotv1alpha1.DeviceCondition) { + currentCond := GetDeviceCondition(*status, condition.Type) + if currentCond != nil && currentCond.Status == condition.Status && currentCond.Reason == condition.Reason { + return + } + + if currentCond != nil && currentCond.Status == condition.Status { + condition.LastTransitionTime = currentCond.LastTransitionTime + } + newConditions := filterOutDeviceCondition(status.Conditions, condition.Type) + status.Conditions = append(newConditions, *condition) +} + +func filterOutDeviceCondition(conditions []iotv1alpha1.DeviceCondition, condType iotv1alpha1.DeviceConditionType) []iotv1alpha1.DeviceCondition { + var newConditions []iotv1alpha1.DeviceCondition + for _, c := range conditions { + if c.Type == condType { + continue + } + newConditions = append(newConditions, c) + } + return newConditions +} + +// NewDeviceServiceCondition creates a new DeviceService condition. +func NewDeviceServiceCondition(condType iotv1alpha1.DeviceServiceConditionType, status corev1.ConditionStatus, reason, message string) *iotv1alpha1.DeviceServiceCondition { + return &iotv1alpha1.DeviceServiceCondition{ + Type: condType, + Status: status, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + } +} + +// GetDeviceServiceCondition returns the condition with the provided type. +func GetDeviceServiceCondition(status iotv1alpha1.DeviceServiceStatus, condType iotv1alpha1.DeviceServiceConditionType) *iotv1alpha1.DeviceServiceCondition { + for i := range status.Conditions { + c := status.Conditions[i] + if c.Type == condType { + return &c + } + } + return nil +} + +// SetDeviceServiceCondition updates the DeviceService to include the provided condition. If the condition that +// we are about to add already exists and has the same status, reason and message then we are not going to update. +func SetDeviceServiceCondition(status *iotv1alpha1.DeviceServiceStatus, condition *iotv1alpha1.DeviceServiceCondition) { + currentCond := GetDeviceServiceCondition(*status, condition.Type) + if currentCond != nil && currentCond.Status == condition.Status && currentCond.Reason == condition.Reason { + return + } + + if currentCond != nil && currentCond.Status == condition.Status { + condition.LastTransitionTime = currentCond.LastTransitionTime + } + newConditions := filterOutDeviceServiceCondition(status.Conditions, condition.Type) + status.Conditions = append(newConditions, *condition) +} + +func filterOutDeviceServiceCondition(conditions []iotv1alpha1.DeviceServiceCondition, condType iotv1alpha1.DeviceServiceConditionType) []iotv1alpha1.DeviceServiceCondition { + var newConditions []iotv1alpha1.DeviceServiceCondition + for _, c := range conditions { + if c.Type == condType { + continue + } + newConditions = append(newConditions, c) + } + return newConditions +} diff --git a/pkg/yurtiotdock/controllers/util/fieldindexer.go b/pkg/yurtiotdock/controllers/util/fieldindexer.go new file mode 100644 index 00000000000..8245d343274 --- /dev/null +++ b/pkg/yurtiotdock/controllers/util/fieldindexer.go @@ -0,0 +1,62 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + "sync" + + "sigs.k8s.io/controller-runtime/pkg/client" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" +) + +const ( + IndexerPathForNodepool = "spec.nodePool" +) + +var registerOnce sync.Once + +func RegisterFieldIndexers(fi client.FieldIndexer) error { + var err error + registerOnce.Do(func() { + // register the fieldIndexer for device + if err = fi.IndexField(context.TODO(), &iotv1alpha1.Device{}, IndexerPathForNodepool, func(rawObj client.Object) []string { + device := rawObj.(*iotv1alpha1.Device) + return []string{device.Spec.NodePool} + }); err != nil { + return + } + + // register the fieldIndexer for deviceService + if err = fi.IndexField(context.TODO(), &iotv1alpha1.DeviceService{}, IndexerPathForNodepool, func(rawObj client.Object) []string { + deviceService := rawObj.(*iotv1alpha1.DeviceService) + return []string{deviceService.Spec.NodePool} + }); err != nil { + return + } + + // register the fieldIndexer for deviceProfile + if err = fi.IndexField(context.TODO(), &iotv1alpha1.DeviceProfile{}, IndexerPathForNodepool, func(rawObj client.Object) []string { + profile := rawObj.(*iotv1alpha1.DeviceProfile) + return []string{profile.Spec.NodePool} + }); err != nil { + return + } + }) + return err +} diff --git a/pkg/yurtiotdock/controllers/util/string.go b/pkg/yurtiotdock/controllers/util/string.go new file mode 100644 index 00000000000..4e7607a4d56 --- /dev/null +++ b/pkg/yurtiotdock/controllers/util/string.go @@ -0,0 +1,30 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +// IsInStringLst checks if 'str' is in the 'strLst' +func IsInStringLst(strLst []string, str string) bool { + if len(strLst) == 0 { + return false + } + for _, s := range strLst { + if str == s { + return true + } + } + return false +} diff --git a/pkg/yurtiotdock/controllers/util/string_test.go b/pkg/yurtiotdock/controllers/util/string_test.go new file mode 100644 index 00000000000..7f56226ad8d --- /dev/null +++ b/pkg/yurtiotdock/controllers/util/string_test.go @@ -0,0 +1,70 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "testing" +) + +func TestIsInStringLst(t *testing.T) { + tests := []struct { + desc string + sl []string + s string + res bool + }{ + { + "test empty list", + []string{}, + "a", + false, + }, + { + "test not in list", + []string{"a", "b", "c"}, + "d", + false, + }, + { + "test not in list with one element", + []string{"a"}, + "b", + false, + }, + { + "test in list with one element", + []string{"aaa"}, + "aaa", + true, + }, + { + "test in list with one element", + []string{"aaa", "a", "bbb"}, + "a", + true, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + res := IsInStringLst(tt.sl, tt.s) + if res != tt.res { + t.Errorf("expect %v, but %v returned", tt.res, res) + } + }) + } +} diff --git a/pkg/yurtiotdock/controllers/util/tools.go b/pkg/yurtiotdock/controllers/util/tools.go new file mode 100644 index 00000000000..a95dc50395a --- /dev/null +++ b/pkg/yurtiotdock/controllers/util/tools.go @@ -0,0 +1,99 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + "fmt" + "io/ioutil" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" +) + +const ( + PODHOSTNAME = "/etc/hostname" + PODNAMESPACE = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" +) + +// GetNodePool get nodepool where yurt-iot-dock run +func GetNodePool(cfg *rest.Config) (string, error) { + var nodePool string + client, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nodePool, err + } + + bn, err := ioutil.ReadFile(PODHOSTNAME) + if err != nil { + return nodePool, fmt.Errorf("Read file %s fail: %v", PODHOSTNAME, err) + } + bns, err := ioutil.ReadFile(PODNAMESPACE) + if err != nil { + return nodePool, fmt.Errorf("Read file %s fail: %v", PODNAMESPACE, err) + } + name := strings.Replace(string(bn), "\n", "", -1) + namespace := string(bns) + + pod, err := client.CoreV1().Pods(namespace).Get(context.Background(), name, metav1.GetOptions{}) + if err != nil { + return nodePool, fmt.Errorf("not found pod %s/%s: %v", namespace, name, err) + } + node, err := client.CoreV1().Nodes().Get(context.Background(), pod.Spec.NodeName, metav1.GetOptions{}) + if err != nil { + return nodePool, fmt.Errorf("not found node %s: %v", pod.Spec.NodeName, err) + } + nodePool, ok := node.Labels["apps.openyurt.io/nodepool"] + if !ok { + return nodePool, fmt.Errorf("node %s doesn't add to a nodepool", node.GetName()) + } + return nodePool, err +} + +func GetEdgeDeviceServiceName(ds *iotv1alpha1.DeviceService, label string) string { + var actualDSName string + if _, ok := ds.ObjectMeta.Labels[label]; ok { + actualDSName = ds.ObjectMeta.Labels[label] + } else { + actualDSName = ds.GetName() + } + return actualDSName +} + +func GetEdgeDeviceName(d *iotv1alpha1.Device, label string) string { + var actualDeviceName string + if _, ok := d.ObjectMeta.Labels[label]; ok { + actualDeviceName = d.ObjectMeta.Labels[label] + } else { + actualDeviceName = d.GetName() + } + return actualDeviceName +} + +func GetEdgeDeviceProfileName(dp *iotv1alpha1.DeviceProfile, label string) string { + var actualDPName string + if _, ok := dp.ObjectMeta.Labels[label]; ok { + actualDPName = dp.ObjectMeta.Labels[label] + } else { + actualDPName = dp.GetName() + } + return actualDPName +} diff --git a/pkg/yurtiotdock/controllers/util/tools_test.go b/pkg/yurtiotdock/controllers/util/tools_test.go new file mode 100644 index 00000000000..fde94c4fffa --- /dev/null +++ b/pkg/yurtiotdock/controllers/util/tools_test.go @@ -0,0 +1,55 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/client-go/rest" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" +) + +func TestGetNodePool(t *testing.T) { + cfg := &rest.Config{} + res, err := GetNodePool(cfg) + if res != "" { + t.Errorf("expect nil on null config") + } + if err == nil { + t.Errorf("null config must cause error") + } +} + +func TestGetEdgeDeviceServiceName(t *testing.T) { + d := &iotv1alpha1.DeviceService{} + assert.Equal(t, GetEdgeDeviceServiceName(d, ""), "") + assert.Equal(t, GetEdgeDeviceServiceName(d, "a"), "") +} + +func TestGetEdgeDeviceName(t *testing.T) { + d := &iotv1alpha1.Device{} + assert.Equal(t, GetEdgeDeviceName(d, ""), "") + assert.Equal(t, GetEdgeDeviceName(d, "a"), "") +} + +func TestGetEdgeDeviceProfileName(t *testing.T) { + d := &iotv1alpha1.DeviceProfile{} + assert.Equal(t, GetEdgeDeviceProfileName(d, ""), "") + assert.Equal(t, GetEdgeDeviceProfileName(d, "a"), "") +} diff --git a/pkg/yurtiotdock/controllers/well_known_labels.go b/pkg/yurtiotdock/controllers/well_known_labels.go new file mode 100644 index 00000000000..fd414f13c9e --- /dev/null +++ b/pkg/yurtiotdock/controllers/well_known_labels.go @@ -0,0 +1,21 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +const ( + EdgeXObjectName = "yurt-iot-dock/edgex-object.name" +)