diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c38ead7..e8ea499b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] + +## [v0.2.7-alpha.6.2] - 2024-03-18 +### Fix +- **protocol:** switch protocol field from v1.Protocol to string + + ## [v0.2.7-alpha.6] - 2024-03-06 ### Feat @@ -11,12 +17,6 @@ - **typo:** fix field reference - -## [v0.2.7-alpha.4-udp-test-2] - 2024-03-04 - - -## [v0.2.7-alpha.5] - 2024-03-04 - ## [v0.2.7-alpha.4] - 2024-02-15 @@ -440,10 +440,9 @@ - Merge pull request [#24](https://github.com/robolaunch/robot-operator/issues/24) from robolaunch/23-allow-multiple-launches -[Unreleased]: https://github.com/robolaunch/robot-operator/compare/v0.2.7-alpha.6...HEAD -[v0.2.7-alpha.6]: https://github.com/robolaunch/robot-operator/compare/v0.2.7-alpha.4-udp-test-2...v0.2.7-alpha.6 -[v0.2.7-alpha.4-udp-test-2]: https://github.com/robolaunch/robot-operator/compare/v0.2.7-alpha.5...v0.2.7-alpha.4-udp-test-2 -[v0.2.7-alpha.5]: https://github.com/robolaunch/robot-operator/compare/v0.2.7-alpha.4...v0.2.7-alpha.5 +[Unreleased]: https://github.com/robolaunch/robot-operator/compare/v0.2.7-alpha.6.2...HEAD +[v0.2.7-alpha.6.2]: https://github.com/robolaunch/robot-operator/compare/v0.2.7-alpha.6...v0.2.7-alpha.6.2 +[v0.2.7-alpha.6]: https://github.com/robolaunch/robot-operator/compare/v0.2.7-alpha.4...v0.2.7-alpha.6 [v0.2.7-alpha.4]: https://github.com/robolaunch/robot-operator/compare/v0.2.7-alpha.1...v0.2.7-alpha.4 [v0.2.7-alpha.1]: https://github.com/robolaunch/robot-operator/compare/v0.2.6-alpha.19...v0.2.7-alpha.1 [v0.2.6-alpha.19]: https://github.com/robolaunch/robot-operator/compare/v0.2.6-alpha.18...v0.2.6-alpha.19 diff --git a/config/crd/bases/robot.roboscale.io_codeeditors.yaml b/config/crd/bases/robot.roboscale.io_codeeditors.yaml index 6eed9a65..49126954 100644 --- a/config/crd/bases/robot.roboscale.io_codeeditors.yaml +++ b/config/crd/bases/robot.roboscale.io_codeeditors.yaml @@ -1805,6 +1805,9 @@ spec: - name type: object type: array + ingress: + description: CodeEditor will create an Ingress resource if `true`. + type: boolean port: default: 9000 description: Port that code editor will use inside the container. @@ -1813,9 +1816,21 @@ spec: remote: description: If `true`, code editor will be consumed remotely. type: boolean - root: - description: If `true`, code editor will be consumed as `root` user. - type: boolean + serviceType: + default: ClusterIP + description: Service type of CodeEditor. `ClusterIP` and `NodePort` + is supported. + enum: + - ClusterIP + - NodePort + type: string + tlsSecretName: + description: Name of the TLS secret for ingress resource. + type: string + version: + default: 4.22.0 + description: App version of the code editor. + type: string volumeClaimTemplates: description: Volume templates for ROS 2 workload. For each volume template, operator will create a PersistentVolumeClaim that can @@ -2048,6 +2063,7 @@ spec: type: array required: - port + - version type: object status: description: Most recently observed status of the CodeEditor. @@ -2364,6 +2380,55 @@ spec: type: string type: object type: array + ingressStatus: + description: Status of CodeEditor Ingress. + properties: + created: + description: Shows if the owned resource is created. + type: boolean + phase: + description: Phase of the owned resource. + type: string + reference: + description: Reference to the owned resource. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead + of an entire object, this string should contain a valid + JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part + of an object. TODO: this design is not final and this field + is subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + required: + - created + type: object phase: description: Phase of CodeEditor. It sums the general status of code editor. @@ -2515,6 +2580,68 @@ spec: type: object type: object type: array + serviceStatus: + description: Status of code editor service. + properties: + resource: + description: Generic status for any owned resource. + properties: + created: + description: Shows if the owned resource is created. + type: boolean + phase: + description: Phase of the owned resource. + type: string + reference: + description: Reference to the owned resource. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead + of an entire object, this string should contain a valid + JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container + within a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that + triggered the event) or if no container name is specified + "spec.containers[2]" (container with index 2 in this + pod). This syntax is chosen only to have some well-defined + way of referencing a part of an object. TODO: this design + is not final and this field is subject to change in + the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + required: + - created + type: object + urls: + additionalProperties: + type: string + description: Connection URL. + type: object + type: object + workloadUpdateNeeded: + description: Field to indicate if the workload should be restarted. + type: boolean type: object type: object served: true diff --git a/config/crd/bases/robot.roboscale.io_discoveryservers.yaml b/config/crd/bases/robot.roboscale.io_discoveryservers.yaml index 3255dba8..f2887ca1 100644 --- a/config/crd/bases/robot.roboscale.io_discoveryservers.yaml +++ b/config/crd/bases/robot.roboscale.io_discoveryservers.yaml @@ -75,7 +75,9 @@ spec: Server's hostname. Used for getting Server's IP over DNS. type: string protocol: - default: TCP + enum: + - TCP + - UDP type: string reference: description: Reference to the `Server` instance. It is used if `.spec.type` diff --git a/config/crd/bases/robot.roboscale.io_robotartifacts.yaml b/config/crd/bases/robot.roboscale.io_robotartifacts.yaml index 5eca87b5..56577c80 100644 --- a/config/crd/bases/robot.roboscale.io_robotartifacts.yaml +++ b/config/crd/bases/robot.roboscale.io_robotartifacts.yaml @@ -120,7 +120,9 @@ spec: DNS. type: string protocol: - default: TCP + enum: + - TCP + - UDP type: string reference: description: Reference to the `Server` instance. It is used diff --git a/config/crd/bases/robot.roboscale.io_robots.yaml b/config/crd/bases/robot.roboscale.io_robots.yaml index 7b33c653..c8d326cb 100644 --- a/config/crd/bases/robot.roboscale.io_robots.yaml +++ b/config/crd/bases/robot.roboscale.io_robots.yaml @@ -141,7 +141,9 @@ spec: DNS. type: string protocol: - default: TCP + enum: + - TCP + - UDP type: string reference: description: Reference to the `Server` instance. It is used diff --git a/config/crd/bases/robot.roboscale.io_ros2workloads.yaml b/config/crd/bases/robot.roboscale.io_ros2workloads.yaml index 44b0d023..6de29fcf 100644 --- a/config/crd/bases/robot.roboscale.io_ros2workloads.yaml +++ b/config/crd/bases/robot.roboscale.io_ros2workloads.yaml @@ -55,7 +55,9 @@ spec: Server's hostname. Used for getting Server's IP over DNS. type: string protocol: - default: TCP + enum: + - TCP + - UDP type: string reference: description: Reference to the `Server` instance. It is used if diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 95dfb6ed..fbffece1 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ kind: Kustomization images: - name: controller newName: robolaunchio/robot-controller-manager - newTag: v0.2.7-alpha.6.1 + newTag: v0.2.7-alpha.6.3 diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 98ceb84a..b6c6e76f 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -53,6 +53,18 @@ rules: - patch - update - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "" resources: diff --git a/docs/_coverpage.md b/docs/_coverpage.md index 04379284..693ee0cb 100644 --- a/docs/_coverpage.md +++ b/docs/_coverpage.md @@ -4,6 +4,6 @@ ![](https://raw.githubusercontent.com/robolaunch/trademark/main/logos/svg/rocket.svg) -# Robot Operator 0.2.7-alpha.6.1 +# Robot Operator 0.2.7-alpha.6.3 robolaunch Kubernetes Robot Operator manages lifecycle of ROS 2 based robots and enables defining, deploying and distributing robots declaratively. \ No newline at end of file diff --git a/go.mod b/go.mod index 3c9340e7..bd37821c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/robolaunch/robot-operator -go 1.21 +go 1.21.1 require ( github.com/go-logr/logr v1.2.3 @@ -8,6 +8,7 @@ require ( github.com/onsi/ginkgo/v2 v2.6.0 github.com/onsi/gomega v1.24.1 github.com/robolaunch/cosmodrome v0.1.0-alpha.12 + github.com/robolaunch/platform/server v0.0.0-20240313131851-88d97e7d270b gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.26.1 k8s.io/apimachinery v0.26.1 @@ -36,6 +37,7 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/go.sum b/go.sum index 56e6b39a..58ac6788 100644 --- a/go.sum +++ b/go.sum @@ -169,6 +169,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -256,6 +258,8 @@ github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5 github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/robolaunch/cosmodrome v0.1.0-alpha.12 h1:1ezr5n+7cB/YLv425rc6kkS0iU2ykU9TDNEPQcs8yeQ= github.com/robolaunch/cosmodrome v0.1.0-alpha.12/go.mod h1:TMDMX9Eir84mHX74QqY+qV0E1NHAnXy67yvIPMvn8ko= +github.com/robolaunch/platform/server v0.0.0-20240313131851-88d97e7d270b h1:Juw9f0glukHPbPwnCsOJRwyRZbvfAQFC0SfyAgBYWHA= +github.com/robolaunch/platform/server v0.0.0-20240313131851-88d97e7d270b/go.mod h1:SN3BW8ejEhBHDMmfC1Q20mDltK4l0s88z3S9pVVarZk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= diff --git a/hack/deploy/chart/robot-operator/Chart.yaml b/hack/deploy/chart/robot-operator/Chart.yaml index b3b19948..066d3d39 100644 --- a/hack/deploy/chart/robot-operator/Chart.yaml +++ b/hack/deploy/chart/robot-operator/Chart.yaml @@ -13,9 +13,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.2.7-alpha.6.1 +version: 0.2.7-alpha.6.3 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v0.2.7-alpha.6.1" +appVersion: "v0.2.7-alpha.6.3" diff --git a/hack/deploy/chart/robot-operator/templates/codeeditor-crd.yaml b/hack/deploy/chart/robot-operator/templates/codeeditor-crd.yaml index 154fe391..c9e2afcc 100644 --- a/hack/deploy/chart/robot-operator/templates/codeeditor-crd.yaml +++ b/hack/deploy/chart/robot-operator/templates/codeeditor-crd.yaml @@ -1781,9 +1781,18 @@ spec: remote: description: If `true`, code editor will be consumed remotely. type: boolean - root: - description: If `true`, code editor will be consumed as `root` user. - type: boolean + serviceType: + default: ClusterIP + description: Service type of CodeEditor. `ClusterIP` and `NodePort` + is supported. + enum: + - ClusterIP + - NodePort + type: string + version: + default: 4.22.0 + description: App version of the code editor. + type: string volumeClaimTemplates: description: Volume templates for ROS 2 workload. For each volume template, operator will create a PersistentVolumeClaim that can be mounted to @@ -2014,6 +2023,7 @@ spec: type: array required: - port + - version type: object status: description: Most recently observed status of the CodeEditor. @@ -2478,6 +2488,65 @@ spec: type: object type: object type: array + serviceStatus: + description: Status of code editor service. + properties: + resource: + description: Generic status for any owned resource. + properties: + created: + description: Shows if the owned resource is created. + type: boolean + phase: + description: Phase of the owned resource. + type: string + reference: + description: Reference to the owned resource. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead + of an entire object, this string should contain a valid + JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container + within a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that + triggered the event) or if no container name is specified + "spec.containers[2]" (container with index 2 in this pod). + This syntax is chosen only to have some well-defined way + of referencing a part of an object. TODO: this design + is not final and this field is subject to change in the + future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + required: + - created + type: object + urls: + additionalProperties: + type: string + description: Connection URL. + type: object + type: object type: object type: object served: true diff --git a/hack/deploy/chart/robot-operator/templates/discoveryserver-crd.yaml b/hack/deploy/chart/robot-operator/templates/discoveryserver-crd.yaml index e5768233..887e4b9e 100644 --- a/hack/deploy/chart/robot-operator/templates/discoveryserver-crd.yaml +++ b/hack/deploy/chart/robot-operator/templates/discoveryserver-crd.yaml @@ -75,7 +75,9 @@ spec: hostname. Used for getting Server's IP over DNS. type: string protocol: - default: TCP + enum: + - TCP + - UDP type: string reference: description: Reference to the `Server` instance. It is used if `.spec.type` diff --git a/hack/deploy/chart/robot-operator/templates/metrics-service.yaml b/hack/deploy/chart/robot-operator/templates/metrics-service.yaml index b106621e..3f0f5bfc 100644 --- a/hack/deploy/chart/robot-operator/templates/metrics-service.yaml +++ b/hack/deploy/chart/robot-operator/templates/metrics-service.yaml @@ -14,4 +14,4 @@ spec: control-plane: controller-manager {{- include "robot-operator.selectorLabels" . | nindent 4 }} ports: - {{- .Values.metricsService.ports | toYaml | nindent 2 }} \ No newline at end of file + {{- .Values.metricsService.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/hack/deploy/chart/robot-operator/templates/robot-crd.yaml b/hack/deploy/chart/robot-operator/templates/robot-crd.yaml index 4649b433..5c9605fe 100644 --- a/hack/deploy/chart/robot-operator/templates/robot-crd.yaml +++ b/hack/deploy/chart/robot-operator/templates/robot-crd.yaml @@ -153,7 +153,9 @@ spec: DNS. type: string protocol: - default: TCP + enum: + - TCP + - UDP type: string reference: description: Reference to the `Server` instance. It is used diff --git a/hack/deploy/chart/robot-operator/templates/robotartifact-crd.yaml b/hack/deploy/chart/robot-operator/templates/robotartifact-crd.yaml index ec9ca6f5..43102fbf 100644 --- a/hack/deploy/chart/robot-operator/templates/robotartifact-crd.yaml +++ b/hack/deploy/chart/robot-operator/templates/robotartifact-crd.yaml @@ -120,7 +120,9 @@ spec: DNS. type: string protocol: - default: TCP + enum: + - TCP + - UDP type: string reference: description: Reference to the `Server` instance. It is used diff --git a/hack/deploy/chart/robot-operator/templates/ros2workload-crd.yaml b/hack/deploy/chart/robot-operator/templates/ros2workload-crd.yaml index 7998a706..c475ab9c 100644 --- a/hack/deploy/chart/robot-operator/templates/ros2workload-crd.yaml +++ b/hack/deploy/chart/robot-operator/templates/ros2workload-crd.yaml @@ -55,7 +55,9 @@ spec: Server's hostname. Used for getting Server's IP over DNS. type: string protocol: - default: TCP + enum: + - TCP + - UDP type: string reference: description: Reference to the `Server` instance. It is used if `.spec.type` diff --git a/hack/deploy/chart/robot-operator/templates/webhook-service.yaml b/hack/deploy/chart/robot-operator/templates/webhook-service.yaml index 61ab9ec1..6ffc253b 100644 --- a/hack/deploy/chart/robot-operator/templates/webhook-service.yaml +++ b/hack/deploy/chart/robot-operator/templates/webhook-service.yaml @@ -13,4 +13,4 @@ spec: control-plane: controller-manager {{- include "robot-operator.selectorLabels" . | nindent 4 }} ports: - {{- .Values.webhookService.ports | toYaml | nindent 2 }} \ No newline at end of file + {{- .Values.webhookService.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/hack/deploy/chart/robot-operator/values.yaml b/hack/deploy/chart/robot-operator/values.yaml index 6ae8eda7..ca2bca55 100644 --- a/hack/deploy/chart/robot-operator/values.yaml +++ b/hack/deploy/chart/robot-operator/values.yaml @@ -32,7 +32,7 @@ controllerManager: - ALL image: repository: robolaunchio/robot-controller-manager - tag: v0.2.7-alpha.6.1 + tag: v0.2.7-alpha.6.3 resources: limits: cpu: 500m diff --git a/hack/deploy/manifests/robot_operator.yaml b/hack/deploy/manifests/robot_operator.yaml index 9f3158f0..0b7d5fdd 100644 --- a/hack/deploy/manifests/robot_operator.yaml +++ b/hack/deploy/manifests/robot_operator.yaml @@ -1518,9 +1518,17 @@ spec: remote: description: If `true`, code editor will be consumed remotely. type: boolean - root: - description: If `true`, code editor will be consumed as `root` user. - type: boolean + serviceType: + default: ClusterIP + description: Service type of CodeEditor. `ClusterIP` and `NodePort` is supported. + enum: + - ClusterIP + - NodePort + type: string + version: + default: 4.22.0 + description: App version of the code editor. + type: string volumeClaimTemplates: description: Volume templates for ROS 2 workload. For each volume template, operator will create a PersistentVolumeClaim that can be mounted to the ROS 2 workload. items: @@ -1657,6 +1665,7 @@ spec: type: array required: - port + - version type: object status: description: Most recently observed status of the CodeEditor. @@ -2027,6 +2036,53 @@ spec: type: object type: object type: array + serviceStatus: + description: Status of code editor service. + properties: + resource: + description: Generic status for any owned resource. + properties: + created: + description: Shows if the owned resource is created. + type: boolean + phase: + description: Phase of the owned resource. + type: string + reference: + description: Reference to the owned resource. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. For example, if the object reference is to a container within a pod, this would take on a value like: "spec.containers{name}" (where "name" refers to the name of the container that triggered the event) or if no container name is specified "spec.containers[2]" (container with index 2 in this pod). This syntax is chosen only to have some well-defined way of referencing a part of an object. TODO: this design is not final and this field is subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + required: + - created + type: object + urls: + additionalProperties: + type: string + description: Connection URL. + type: object + type: object type: object type: object served: true @@ -2100,7 +2156,9 @@ spec: description: If instance type is `Server`, it can be an arbitrary value. If instance type is `Client`, it should be the same with Server's hostname. Used for getting Server's IP over DNS. type: string protocol: - default: TCP + enum: + - TCP + - UDP type: string reference: description: Reference to the `Server` instance. It is used if `.spec.type` is `Client`. Referenced object can be previously provisioned in another cluster. In that case, cluster's name can be specified in `.spec.cluster` field. @@ -4015,7 +4073,9 @@ spec: description: If instance type is `Server`, it can be an arbitrary value. If instance type is `Client`, it should be the same with Server's hostname. Used for getting Server's IP over DNS. type: string protocol: - default: TCP + enum: + - TCP + - UDP type: string reference: description: Reference to the `Server` instance. It is used if `.spec.type` is `Client`. Referenced object can be previously provisioned in another cluster. In that case, cluster's name can be specified in `.spec.cluster` field. @@ -5362,7 +5422,9 @@ spec: description: If instance type is `Server`, it can be an arbitrary value. If instance type is `Client`, it should be the same with Server's hostname. Used for getting Server's IP over DNS. type: string protocol: - default: TCP + enum: + - TCP + - UDP type: string reference: description: Reference to the `Server` instance. It is used if `.spec.type` is `Client`. Referenced object can be previously provisioned in another cluster. In that case, cluster's name can be specified in `.spec.cluster` field. @@ -8023,7 +8085,9 @@ spec: description: If instance type is `Server`, it can be an arbitrary value. If instance type is `Client`, it should be the same with Server's hostname. Used for getting Server's IP over DNS. type: string protocol: - default: TCP + enum: + - TCP + - UDP type: string reference: description: Reference to the `Server` instance. It is used if `.spec.type` is `Client`. Referenced object can be previously provisioned in another cluster. In that case, cluster's name can be specified in `.spec.cluster` field. @@ -11259,7 +11323,7 @@ spec: - --leader-elect command: - /manager - image: robolaunchio/robot-controller-manager:v0.2.7-alpha.6.1 + image: robolaunchio/robot-controller-manager:v0.2.7-alpha.6.3 livenessProbe: httpGet: path: /healthz diff --git a/internal/configure/v1alpha2/configure.go b/internal/configure/v1alpha2/configure.go index 499baa90..f20c8dd3 100644 --- a/internal/configure/v1alpha2/configure.go +++ b/internal/configure/v1alpha2/configure.go @@ -1,3 +1,4 @@ package configure type PodSpecConfigInjector struct{} +type ServiceSpecConfigInjector struct{} diff --git a/internal/configure/v1alpha2/remote.go b/internal/configure/v1alpha2/remote.go new file mode 100644 index 00000000..6328a5fb --- /dev/null +++ b/internal/configure/v1alpha2/remote.go @@ -0,0 +1,22 @@ +package configure + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (cfg *PodSpecConfigInjector) InjectRemoteConfigurations(podSpec *corev1.PodSpec, obj metav1.Object) *corev1.PodSpec { + + podSpec.Hostname = obj.GetName() + podSpec.Subdomain = obj.GetName() + + return podSpec +} + +func (cfg *ServiceSpecConfigInjector) InjectRemoteConfigurations(serviceSpec *corev1.ServiceSpec) *corev1.ServiceSpec { + + serviceSpec.Type = "" + serviceSpec.ClusterIP = "None" + + return serviceSpec +} diff --git a/internal/label/image.go b/internal/label/image.go new file mode 100644 index 00000000..324e25b6 --- /dev/null +++ b/internal/label/image.go @@ -0,0 +1,37 @@ +package label + +import ( + "github.com/robolaunch/robot-operator/internal" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ImageMeta struct { + Registry string + User string + Repository string + Tag string +} + +func GetImageMeta(obj metav1.Object) *ImageMeta { + + imageMeta := &ImageMeta{} + labels := obj.GetLabels() + + if registry, ok := labels[internal.IMAGE_REGISTRY_LABEL_KEY]; ok { + imageMeta.Registry = registry + } + + if user, ok := labels[internal.IMAGE_USER_LABEL_KEY]; ok { + imageMeta.User = user + } + + if repository, ok := labels[internal.IMAGE_REPOSITORY_LABEL_KEY]; ok { + imageMeta.Repository = repository + } + + if tag, ok := labels[internal.IMAGE_TAG_LABEL_KEY]; ok { + imageMeta.Tag = tag + } + + return imageMeta +} diff --git a/internal/label/instance.go b/internal/label/instance.go new file mode 100644 index 00000000..30b11833 --- /dev/null +++ b/internal/label/instance.go @@ -0,0 +1,26 @@ +package label + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +type InstanceType string + +const ( + InstanceTypeCloudInstance InstanceType = "CloudInstance" + InstanceTypePhysicalInstance InstanceType = "PhysicalInstance" +) + +func GetInstanceType(obj metav1.Object) InstanceType { + tenancy := GetTenancy(obj) + if tenancy.PhysicalInstance == "" { + return InstanceTypeCloudInstance + } + return InstanceTypePhysicalInstance +} + +func GetClusterName(obj metav1.Object) string { + tenancy := GetTenancy(obj) + if tenancy.PhysicalInstance == "" { + return tenancy.CloudInstance + } + return tenancy.PhysicalInstance +} diff --git a/internal/label/platform.go b/internal/label/platform.go new file mode 100644 index 00000000..577441cc --- /dev/null +++ b/internal/label/platform.go @@ -0,0 +1,26 @@ +package label + +import ( + "github.com/robolaunch/robot-operator/internal" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type PlatformMeta struct { + Version string + Domain string +} + +func GetPlatformMeta(obj metav1.Object) *PlatformMeta { + platformMeta := &PlatformMeta{} + labels := obj.GetLabels() + + if version, ok := labels[internal.PLATFORM_VERSION_LABEL_KEY]; ok { + platformMeta.Version = version + } + + if domain, ok := labels[internal.DOMAIN_LABEL_KEY]; ok { + platformMeta.Domain = domain + } + + return platformMeta +} diff --git a/internal/label/tenancy.go b/internal/label/tenancy.go index 257796ae..88a84ada 100644 --- a/internal/label/tenancy.go +++ b/internal/label/tenancy.go @@ -12,12 +12,6 @@ type Tenancy struct { CloudInstance string CloudInstanceAlias string PhysicalInstance string - Domain string -} - -type Timezone struct { - Continent string - City string } func GetTenancy(obj metav1.Object) *Tenancy { @@ -48,29 +42,9 @@ func GetTenancy(obj metav1.Object) *Tenancy { tenancy.PhysicalInstance = physicalInstance } - if domain, ok := labels[internal.DOMAIN_LABEL_KEY]; ok { - tenancy.Domain = domain - } - return tenancy } -func GetTimezone(obj metav1.Object) *Timezone { - - timezone := &Timezone{} - labels := obj.GetLabels() - - if continent, ok := labels[internal.TZ_CONTINENT_LABEL_KEY]; ok { - timezone.Continent = continent - } - - if city, ok := labels[internal.TZ_CITY_LABEL_KEY]; ok { - timezone.City = city - } - - return timezone -} - func GetTenancyMap(obj metav1.Object) map[string]string { labels := obj.GetLabels() tenancyMap := make(map[string]string) @@ -131,26 +105,3 @@ func GetTenancyMapFromTenancy(tenancy Tenancy) map[string]string { return tenancyMap } - -type InstanceType string - -const ( - InstanceTypeCloudInstance InstanceType = "CloudInstance" - InstanceTypePhysicalInstance InstanceType = "PhysicalInstance" -) - -func GetInstanceType(obj metav1.Object) InstanceType { - tenancy := GetTenancy(obj) - if tenancy.PhysicalInstance == "" { - return InstanceTypeCloudInstance - } - return InstanceTypePhysicalInstance -} - -func GetClusterName(obj metav1.Object) string { - tenancy := GetTenancy(obj) - if tenancy.PhysicalInstance == "" { - return tenancy.CloudInstance - } - return tenancy.PhysicalInstance -} diff --git a/internal/label/timezone.go b/internal/label/timezone.go new file mode 100644 index 00000000..ec514b01 --- /dev/null +++ b/internal/label/timezone.go @@ -0,0 +1,27 @@ +package label + +import ( + "github.com/robolaunch/robot-operator/internal" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type Timezone struct { + Continent string + City string +} + +func GetTimezone(obj metav1.Object) *Timezone { + + timezone := &Timezone{} + labels := obj.GetLabels() + + if continent, ok := labels[internal.TZ_CONTINENT_LABEL_KEY]; ok { + timezone.Continent = continent + } + + if city, ok := labels[internal.TZ_CITY_LABEL_KEY]; ok { + timezone.City = city + } + + return timezone +} diff --git a/internal/node/image.go b/internal/node/image.go index 1afaef43..f19ecf99 100644 --- a/internal/node/image.go +++ b/internal/node/image.go @@ -9,6 +9,7 @@ import ( cosmodrome "github.com/robolaunch/cosmodrome/pkg/api" "github.com/robolaunch/robot-operator/internal" + "github.com/robolaunch/robot-operator/internal/label" robotv1alpha1 "github.com/robolaunch/robot-operator/pkg/api/roboscale.io/v1alpha1" robotv1alpha2 "github.com/robolaunch/robot-operator/pkg/api/roboscale.io/v1alpha2" corev1 "k8s.io/api/core/v1" @@ -56,10 +57,10 @@ type ReadyRobotProperties struct { func GetReadyRobotProperties(robot robotv1alpha1.Robot) ReadyRobotProperties { labels := robot.GetLabels() - if registry, hasRegistry := labels[internal.ROBOT_IMAGE_REGISTRY_LABEL_KEY]; hasRegistry { - if user, hasUser := labels[internal.ROBOT_IMAGE_USER_LABEL_KEY]; hasUser { - if repository, hasRepository := labels[internal.ROBOT_IMAGE_REPOSITORY_LABEL_KEY]; hasRepository { - if tag, hasTag := labels[internal.ROBOT_IMAGE_TAG_LABEL_KEY]; hasTag { + if registry, hasRegistry := labels[internal.IMAGE_REGISTRY_LABEL_KEY]; hasRegistry { + if user, hasUser := labels[internal.IMAGE_USER_LABEL_KEY]; hasUser { + if repository, hasRepository := labels[internal.IMAGE_REPOSITORY_LABEL_KEY]; hasRepository { + if tag, hasTag := labels[internal.IMAGE_TAG_LABEL_KEY]; hasTag { return ReadyRobotProperties{ Enabled: true, Image: registry + "/" + user + "/" + repository + ":" + tag, @@ -95,15 +96,16 @@ func GetImageForRobot(ctx context.Context, r client.Client, node corev1.Node, ro } else { - platformVersion := GetPlatformVersion(node) - imageProps, err := getImagePropsForRobot(ctx, r, platformVersion, getDistroStr(robot.Spec.RobotConfig.Distributions)) + platformMeta := label.GetPlatformMeta(&node) + + imageProps, err := getImagePropsForRobot(ctx, r, platformMeta.Version, getDistroStr(robot.Spec.RobotConfig.Distributions)) if err != nil { return "", err } - registry, hasRegistry := robot.Labels[internal.ROBOT_IMAGE_REGISTRY_LABEL_KEY] + registry, hasRegistry := robot.Labels[internal.IMAGE_REGISTRY_LABEL_KEY] if !hasRegistry { - return "", errors.New("registry is not found in label with key " + internal.ROBOT_IMAGE_REGISTRY_LABEL_KEY) + return "", errors.New("registry is not found in label with key " + internal.IMAGE_REGISTRY_LABEL_KEY) } organization := "robolaunchio" @@ -125,15 +127,16 @@ func GetImageForBridge(ctx context.Context, r client.Client, node corev1.Node, r var imageBuilder strings.Builder var tagBuilder strings.Builder - platformVersion := GetPlatformVersion(node) - imageProps, err := getImagePropsForRobot(ctx, r, platformVersion, getDistroStr(robot.Spec.RobotConfig.Distributions)) + platformMeta := label.GetPlatformMeta(&node) + + imageProps, err := getImagePropsForRobot(ctx, r, platformMeta.Version, getDistroStr(robot.Spec.RobotConfig.Distributions)) if err != nil { return "", err } - registry, hasRegistry := robot.Labels[internal.ROBOT_IMAGE_REGISTRY_LABEL_KEY] + registry, hasRegistry := robot.Labels[internal.IMAGE_REGISTRY_LABEL_KEY] if !hasRegistry { - return "", errors.New("registry is not found in label with key " + internal.ROBOT_IMAGE_REGISTRY_LABEL_KEY) + return "", errors.New("registry is not found in label with key " + internal.IMAGE_REGISTRY_LABEL_KEY) } organization := "robolaunchio" @@ -151,15 +154,16 @@ func GetBridgeImage(ctx context.Context, r client.Client, node corev1.Node, ros2 var imageBuilder strings.Builder var tagBuilder strings.Builder - platformVersion := GetPlatformVersion(node) - imageProps, err := getImagePropsForRobot(ctx, r, platformVersion, getDistroStr([]robotv1alpha1.ROSDistro{ros2Bridge.Spec.Distro})) + platformMeta := label.GetPlatformMeta(&node) + + imageProps, err := getImagePropsForRobot(ctx, r, platformMeta.Version, getDistroStr([]robotv1alpha1.ROSDistro{ros2Bridge.Spec.Distro})) if err != nil { return "", err } - registry, hasRegistry := ros2Bridge.Labels[internal.ROBOT_IMAGE_REGISTRY_LABEL_KEY] + registry, hasRegistry := ros2Bridge.Labels[internal.IMAGE_REGISTRY_LABEL_KEY] if !hasRegistry { - return "", errors.New("registry is not found in label with key " + internal.ROBOT_IMAGE_REGISTRY_LABEL_KEY) + return "", errors.New("registry is not found in label with key " + internal.IMAGE_REGISTRY_LABEL_KEY) } organization := "robolaunchio" @@ -185,15 +189,16 @@ func GetImageForEnvironment(ctx context.Context, r client.Client, node corev1.No } else { - platformVersion := GetPlatformVersion(node) - imageProps, err := getImagePropsForEnvironment(ctx, r, platformVersion) + platformMeta := label.GetPlatformMeta(&node) + + imageProps, err := getImagePropsForEnvironment(ctx, r, platformMeta.Version) if err != nil { return "", err } - registry, hasRegistry := robot.Labels[internal.ROBOT_IMAGE_REGISTRY_LABEL_KEY] + registry, hasRegistry := robot.Labels[internal.IMAGE_REGISTRY_LABEL_KEY] if !hasRegistry { - return "", errors.New("registry is not found in label with key " + internal.ROBOT_IMAGE_REGISTRY_LABEL_KEY) + return "", errors.New("registry is not found in label with key " + internal.IMAGE_REGISTRY_LABEL_KEY) } organization := imageProps.Organization diff --git a/internal/node/node.go b/internal/node/node.go index 1e6011ec..b176654a 100644 --- a/internal/node/node.go +++ b/internal/node/node.go @@ -1,7 +1,6 @@ package node import ( - "github.com/robolaunch/robot-operator/internal" corev1 "k8s.io/api/core/v1" ) @@ -16,10 +15,3 @@ func IsK3s(node corev1.Node) bool { } return false } - -func GetPlatformVersion(node corev1.Node) string { - if platformVersion, ok := node.Labels[internal.PLATFORM_VERSION_LABEL_KEY]; ok { - return platformVersion - } - return "" -} diff --git a/internal/platform/environment.go b/internal/platform/environment.go new file mode 100644 index 00000000..29c52098 --- /dev/null +++ b/internal/platform/environment.go @@ -0,0 +1,50 @@ +package platform + +import ( + "errors" + "net/url" + + platformServer "github.com/robolaunch/platform/server/pkg" + platformArtifact "github.com/robolaunch/platform/server/pkg/models" + "github.com/robolaunch/robot-operator/internal" +) + +func GetVersionedPlatformEnvironments(version string) (platformArtifact.VersionedPlatformImages, error) { + + environmentsResponse := platformServer.GetVersionedPlatformImagesResponse( + map[string]string{ + "version": version, + }, + url.Values{ + "url": []string{ + internal.DEFAULT_ENVIRONMENTS_URL, + }, + }, + ) + + if !environmentsResponse.Success { + return platformArtifact.VersionedPlatformImages{}, errors.New(environmentsResponse.Message) + } + + return platformArtifact.VersionedPlatformImages{}, nil +} + +func GetVersionedPlatformToolEnvironments(version string) (platformArtifact.VersionedPlatformImages, error) { + + environmentsResponse := platformServer.GetVersionedPlatformImagesResponse( + map[string]string{ + "version": version, + }, + url.Values{ + "url": []string{ + internal.DEFAULT_TOOLS_ENVIRONMENTS_URL, + }, + }, + ) + + if !environmentsResponse.Success { + return platformArtifact.VersionedPlatformImages{}, errors.New(environmentsResponse.Message) + } + + return *environmentsResponse.Response, nil +} diff --git a/internal/platform/image.go b/internal/platform/image.go new file mode 100644 index 00000000..547bc763 --- /dev/null +++ b/internal/platform/image.go @@ -0,0 +1,40 @@ +package platform + +import ( + "errors" + "reflect" + + "github.com/robolaunch/platform/server/pkg/models" + "github.com/robolaunch/robot-operator/internal/label" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func GetToolsImage(obj metav1.Object, platformVersion string, application string, appVersion string) (string, error) { + + imageMeta := label.GetImageMeta(obj) + + vpi, err := GetVersionedPlatformToolEnvironments(platformVersion) + if err != nil { + return "", err + } + + selectedEnvironment := models.Environment{} + + if domain, ok := vpi.Domains["tools"]; ok { + for _, environment := range domain.Environments { + if environment.Application.Name == application && environment.Application.Version == appVersion { + selectedEnvironment = environment + } + } + } else { + return "", errors.New("cannot find tools domain") + } + + if reflect.DeepEqual(selectedEnvironment, models.Environment{}) { + return "", errors.New("cannot find application '" + application + "' with version '" + appVersion + "'") + } + + imageName := imageMeta.Registry + "/" + vpi.Organization + "/" + application + ":" + selectedEnvironment.Application.Version + "-" + selectedEnvironment.Base.Version + + return imageName, nil +} diff --git a/internal/platform/platform.go b/internal/platform/platform.go new file mode 100644 index 00000000..4f696ebc --- /dev/null +++ b/internal/platform/platform.go @@ -0,0 +1,27 @@ +package platform + +import ( + "errors" + "net/url" + + platformServer "github.com/robolaunch/platform/server/pkg" + platformArtifact "github.com/robolaunch/platform/server/pkg/models" + "github.com/robolaunch/robot-operator/internal" +) + +func GetPlatform() (platformArtifact.Robolaunch, error) { + + platformResponse := platformServer.GetPlatformResponse( + url.Values{ + "url": []string{ + internal.DEFAULT_PLATFORM_URL, + }, + }, + ) + + if !platformResponse.Success { + return platformArtifact.Robolaunch{}, errors.New(platformResponse.Message) + } + + return *platformResponse.Response, nil +} diff --git a/internal/resources/v1alpha1/discovery_server.go b/internal/resources/v1alpha1/discovery_server.go index 98cdfe25..2cd965aa 100644 --- a/internal/resources/v1alpha1/discovery_server.go +++ b/internal/resources/v1alpha1/discovery_server.go @@ -37,7 +37,7 @@ func GetDiscoveryServerPod(discoveryServer *robotv1alpha1.DiscoveryServer, podNa { Name: discoveryServerPortName, ContainerPort: int32(discoveryServerPortNumber), - Protocol: discoveryServer.Spec.Protocol, + Protocol: corev1.Protocol(discoveryServer.Spec.Protocol), }, }, }, @@ -79,7 +79,7 @@ func GetDiscoveryServerService(discoveryServer *robotv1alpha1.DiscoveryServer, s TargetPort: intstr.IntOrString{ IntVal: int32(discoveryServerPortNumber), }, - Protocol: discoveryServer.Spec.Protocol, + Protocol: corev1.Protocol(discoveryServer.Spec.Protocol), Name: discoveryServerPortName, }, }, diff --git a/internal/resources/v1alpha2/code_editor.go b/internal/resources/v1alpha2/code_editor.go index 06756a53..2604e8e8 100644 --- a/internal/resources/v1alpha2/code_editor.go +++ b/internal/resources/v1alpha2/code_editor.go @@ -1,19 +1,31 @@ package v1alpha2_resources import ( + "fmt" + "path/filepath" + "strconv" + "strings" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" "github.com/robolaunch/robot-operator/internal" configure "github.com/robolaunch/robot-operator/internal/configure/v1alpha2" + "github.com/robolaunch/robot-operator/internal/label" + "github.com/robolaunch/robot-operator/internal/platform" + robotv1alpha1 "github.com/robolaunch/robot-operator/pkg/api/roboscale.io/v1alpha1" robotv1alpha2 "github.com/robolaunch/robot-operator/pkg/api/roboscale.io/v1alpha2" ) -const ( - CODE_EDITOR_PORT_NAME = "code-server" -) +func getCodeEditorSelector(codeEditor robotv1alpha2.CodeEditor) map[string]string { + return map[string]string{ + internal.CODE_EDITOR_CONTAINER_SELECTOR_LABEL_KEY: codeEditor.Name, + } +} func GetCodeEditorPersistentVolumeClaim(codeEditor *robotv1alpha2.CodeEditor, pvcNamespacedName *types.NamespacedName, key int) *corev1.PersistentVolumeClaim { @@ -32,17 +44,34 @@ func GetCodeEditorPersistentVolumeClaim(codeEditor *robotv1alpha2.CodeEditor, pv func GetCodeEditorDeployment(codeEditor *robotv1alpha2.CodeEditor, deploymentNamespacedName *types.NamespacedName, node corev1.Node) *appsv1.Deployment { + platformMeta := label.GetPlatformMeta(&node) + cfg := configure.PodSpecConfigInjector{} + image, err := platform.GetToolsImage(codeEditor, platformMeta.Version, internal.CODE_EDITOR_APP_NAME, codeEditor.Spec.Version) + if err != nil { + return nil + } + + var cmdBuilder strings.Builder + cmdBuilder.WriteString("supervisord -c " + filepath.Join("/etc", "robolaunch", "services", "code-server.conf")) + podSpec := corev1.PodSpec{ Containers: []corev1.Container{ { - Name: "code-editor", - Image: "docker.io/robolaunchio/code-editor:4.22.0-0.2.6-alpha.19", - Command: []string{"/bin/bash", "-c", "sleep infinity"}, + Name: internal.CODE_EDITOR_APP_NAME, + Image: image, + Command: internal.Bash(cmdBuilder.String()), + Env: []corev1.EnvVar{ + internal.Env("CODE_SERVER_PORT", strconv.FormatInt(int64(codeEditor.Spec.Port), 10)), + internal.Env("FILE_BROWSER_PORT", strconv.Itoa(internal.FILE_BROWSER_PORT)), + internal.Env("FILE_BROWSER_SERVICE", "code-server"), + internal.Env("FILE_BROWSER_BASE_URL", robotv1alpha1.GetServicePath(codeEditor, "/"+internal.FILE_BROWSER_PORT_NAME)), + internal.Env("TERM", "xterm-256color"), + }, Ports: []corev1.ContainerPort{ { - Name: CODE_EDITOR_PORT_NAME, + Name: internal.CODE_EDITOR_PORT_NAME, ContainerPort: codeEditor.Spec.Port, Protocol: corev1.ProtocolTCP, }, @@ -65,8 +94,8 @@ func GetCodeEditorDeployment(codeEditor *robotv1alpha2.CodeEditor, deploymentNam cfg.InjectVolumeConfiguration(&podSpec, codeEditor.Status.PVCStatuses) cfg.InjectExternalVolumeConfiguration(&podSpec, codeEditor.Status.ExternalVolumeStatuses) - if !codeEditor.Spec.Root { - cfg.InjectLinuxUserAndGroup(&podSpec) + if codeEditor.Spec.Remote { + cfg.InjectRemoteConfigurations(&podSpec, codeEditor) } deployment := appsv1.Deployment{ @@ -77,15 +106,11 @@ func GetCodeEditorDeployment(codeEditor *robotv1alpha2.CodeEditor, deploymentNam }, Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - internal.CODE_EDITOR_CONTAINER_SELECTOR_LABEL_KEY: "code-editor", - }, + MatchLabels: getCodeEditorSelector(*codeEditor), }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - internal.CODE_EDITOR_CONTAINER_SELECTOR_LABEL_KEY: "code-editor", - }, + Labels: getCodeEditorSelector(*codeEditor), }, Spec: podSpec, }, @@ -94,3 +119,125 @@ func GetCodeEditorDeployment(codeEditor *robotv1alpha2.CodeEditor, deploymentNam return &deployment } + +func GetCodeEditorService(codeEditor *robotv1alpha2.CodeEditor, svcNamespacedName *types.NamespacedName) *corev1.Service { + + cfg := configure.ServiceSpecConfigInjector{} + + serviceSpec := corev1.ServiceSpec{ + Type: codeEditor.Spec.ServiceType, + Selector: getCodeEditorSelector(*codeEditor), + Ports: []corev1.ServicePort{ + { + Name: internal.CODE_EDITOR_PORT_NAME, + Port: codeEditor.Spec.Port, + TargetPort: intstr.IntOrString{ + IntVal: codeEditor.Spec.Port, + }, + Protocol: corev1.ProtocolTCP, + }, + { + Name: internal.FILE_BROWSER_PORT_NAME, + Port: internal.FILE_BROWSER_PORT, + TargetPort: intstr.IntOrString{ + IntVal: internal.FILE_BROWSER_PORT, + }, + Protocol: corev1.ProtocolTCP, + }, + }, + } + + if codeEditor.Spec.Remote { + cfg.InjectRemoteConfigurations(&serviceSpec) + } + + service := corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: svcNamespacedName.Name, + Namespace: svcNamespacedName.Namespace, + Labels: codeEditor.Labels, + }, + Spec: serviceSpec, + } + + return &service +} + +func GetCodeEditorIngress(codeEditor *robotv1alpha2.CodeEditor, ingressNamespacedName *types.NamespacedName) *networkingv1.Ingress { + + tenancy := label.GetTenancy(codeEditor) + platformMeta := label.GetPlatformMeta(codeEditor) + + annotations := map[string]string{ + internal.INGRESS_AUTH_URL_KEY: fmt.Sprintf(internal.INGRESS_AUTH_URL_VAL, tenancy.Team, platformMeta.Domain), + internal.INGRESS_AUTH_SIGNIN_KEY: fmt.Sprintf(internal.INGRESS_AUTH_SIGNIN_VAL, tenancy.Team, platformMeta.Domain), + internal.INGRESS_AUTH_RESPONSE_HEADERS_KEY: internal.INGRESS_AUTH_RESPONSE_HEADERS_VAL, + internal.INGRESS_CONFIGURATION_SNIPPET_KEY: internal.INGRESS_IDE_CONFIGURATION_SNIPPET_VAL, + internal.INGRESS_CERT_MANAGER_KEY: internal.INGRESS_CERT_MANAGER_VAL, + internal.INGRESS_NGINX_PROXY_BUFFER_SIZE_KEY: internal.INGRESS_NGINX_PROXY_BUFFER_SIZE_VAL, + internal.INGRESS_NGINX_REWRITE_TARGET_KEY: internal.INGRESS_NGINX_REWRITE_TARGET_VAL, + internal.INGRESS_PROXY_READ_TIMEOUT_KEY: internal.INGRESS_PROXY_READ_TIMEOUT_VAL, + } + + pathTypePrefix := networkingv1.PathTypePrefix + ingressClassNameNginx := "nginx" + + ingressSpec := networkingv1.IngressSpec{ + TLS: []networkingv1.IngressTLS{ + { + Hosts: []string{ + tenancy.Team + "." + platformMeta.Domain, + }, + SecretName: codeEditor.Spec.TLSSecretName, + }, + }, + Rules: []networkingv1.IngressRule{ + { + Host: tenancy.Team + "." + platformMeta.Domain, + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: robotv1alpha1.GetServicePath(codeEditor, "/"+internal.CODE_EDITOR_APP_NAME) + "(/|$)(.*)", + PathType: &pathTypePrefix, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: codeEditor.GetServiceMetadata().Name, + Port: networkingv1.ServiceBackendPort{ + Number: codeEditor.Spec.Port, + }, + }, + }, + }, + { + Path: robotv1alpha1.GetServicePath(codeEditor, "/"+internal.FILE_BROWSER_PORT_NAME) + "(/|$)(.*)", + PathType: &pathTypePrefix, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: codeEditor.GetServiceMetadata().Name, + Port: networkingv1.ServiceBackendPort{ + Number: internal.FILE_BROWSER_PORT, + }, + }, + }, + }, + }, + }, + }, + }, + }, + IngressClassName: &ingressClassNameNginx, + } + + ingress := &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: ingressNamespacedName.Name, + Namespace: ingressNamespacedName.Namespace, + Labels: codeEditor.Labels, + Annotations: annotations, + }, + Spec: ingressSpec, + } + + return ingress +} diff --git a/internal/resources/v1alpha2/ros2_bridge.go b/internal/resources/v1alpha2/ros2_bridge.go index 1da4807d..278229ab 100644 --- a/internal/resources/v1alpha2/ros2_bridge.go +++ b/internal/resources/v1alpha2/ros2_bridge.go @@ -97,10 +97,11 @@ func GetROS2BridgeService(rosbridge *robotv1alpha2.ROS2Bridge, svcNamespacedName func GetROS2BridgeIngress(ros2Bridge *robotv1alpha2.ROS2Bridge, ingressNamespacedName *types.NamespacedName) *networkingv1.Ingress { tenancy := label.GetTenancy(ros2Bridge) + platformMeta := label.GetPlatformMeta(ros2Bridge) annotations := map[string]string{ - internal.INGRESS_AUTH_URL_KEY: fmt.Sprintf(internal.INGRESS_AUTH_URL_VAL, tenancy.Team, tenancy.Domain), - internal.INGRESS_AUTH_SIGNIN_KEY: fmt.Sprintf(internal.INGRESS_AUTH_SIGNIN_VAL, tenancy.Team, tenancy.Domain), + internal.INGRESS_AUTH_URL_KEY: fmt.Sprintf(internal.INGRESS_AUTH_URL_VAL, tenancy.Team, platformMeta.Domain), + internal.INGRESS_AUTH_SIGNIN_KEY: fmt.Sprintf(internal.INGRESS_AUTH_SIGNIN_VAL, tenancy.Team, platformMeta.Domain), internal.INGRESS_AUTH_RESPONSE_HEADERS_KEY: internal.INGRESS_AUTH_RESPONSE_HEADERS_VAL, internal.INGRESS_CONFIGURATION_SNIPPET_KEY: internal.INGRESS_IDE_CONFIGURATION_SNIPPET_VAL, internal.INGRESS_CERT_MANAGER_KEY: internal.INGRESS_CERT_MANAGER_VAL, @@ -116,14 +117,14 @@ func GetROS2BridgeIngress(ros2Bridge *robotv1alpha2.ROS2Bridge, ingressNamespace TLS: []networkingv1.IngressTLS{ { Hosts: []string{ - tenancy.Team + "." + tenancy.Domain, + tenancy.Team + "." + platformMeta.Domain, }, SecretName: ros2Bridge.Spec.TLSSecretName, }, }, Rules: []networkingv1.IngressRule{ { - Host: tenancy.Team + "." + tenancy.Domain, + Host: tenancy.Team + "." + platformMeta.Domain, IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ diff --git a/internal/shared.go b/internal/shared.go index a12dcd7c..2aac2598 100644 --- a/internal/shared.go +++ b/internal/shared.go @@ -2,9 +2,10 @@ package internal import corev1 "k8s.io/api/core/v1" -// Platform related labels +// Platform meta labels const ( PLATFORM_VERSION_LABEL_KEY = "robolaunch.io/platform" + DOMAIN_LABEL_KEY = "robolaunch.io/domain" ) // Tenancy labels @@ -15,7 +16,6 @@ const ( CLOUD_INSTANCE_LABEL_KEY = "robolaunch.io/cloud-instance" CLOUD_INSTANCE_ALIAS_LABEL_KEY = "robolaunch.io/cloud-instance-alias" PHYSICAL_INSTANCE_LABEL_KEY = "robolaunch.io/physical-instance" - DOMAIN_LABEL_KEY = "robolaunch.io/domain" ) // Selector labels @@ -30,12 +30,12 @@ const ( TZ_CITY_LABEL_KEY = "robolaunch.io/tz-city" ) -// Ready robot label +// Image labels const ( - ROBOT_IMAGE_REGISTRY_LABEL_KEY = "robolaunch.io/robot-image-registry" - ROBOT_IMAGE_USER_LABEL_KEY = "robolaunch.io/robot-image-user" - ROBOT_IMAGE_REPOSITORY_LABEL_KEY = "robolaunch.io/robot-image-repository" - ROBOT_IMAGE_TAG_LABEL_KEY = "robolaunch.io/robot-image-tag" + IMAGE_REGISTRY_LABEL_KEY = "robolaunch.io/robot-image-registry" + IMAGE_USER_LABEL_KEY = "robolaunch.io/robot-image-user" + IMAGE_REPOSITORY_LABEL_KEY = "robolaunch.io/robot-image-repository" + IMAGE_TAG_LABEL_KEY = "robolaunch.io/robot-image-tag" ) // Target resource labels @@ -54,6 +54,20 @@ const ( ROBOT_DEV_SUITE_OWNED = "robolaunch.io/dev-suite-owned" ) +// Platform server +const ( + DEFAULT_PLATFORM_URL = "https://raw.githubusercontent.com/robolaunch/platform/main/platforms/industry-cloud-platform/platform.yaml" + DEFAULT_ENVIRONMENTS_URL = "https://raw.githubusercontent.com/robolaunch/platform/main/platforms/industry-cloud-platform/environments.yaml" + DEFAULT_TOOLS_ENVIRONMENTS_URL = "https://raw.githubusercontent.com/robolaunch/platform/main/platforms/internal/environments.yaml" +) + +// CodeEditor + +const ( + CODE_EDITOR_APP_NAME = "code-editor" + CODE_EDITOR_PORT_NAME = "code-editor" +) + // Robot owned resources' postfixes (v1alpha1) const ( PVC_VAR_POSTFIX = "-pvc-var" @@ -75,6 +89,8 @@ const ( PVC_POSTFIX = "-pvc" STATEFULSET_POSTFIX = "" DEPLOYMENT_POSTFIX = "" + SERVICE_POSTFIX = "" + INGRESS_POSTFIX = "" ) // WorkspaceManager owned resources' postfixes diff --git a/main.go b/main.go index 66217c3f..fc507ee4 100644 --- a/main.go +++ b/main.go @@ -365,8 +365,9 @@ func startToolkitCRDsAndWebhooks(mgr manager.Manager) { // ROS2Bridge controller & webhook if err := (&codeEditor.CodeEditorReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("code-editor"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "CodeEditor") os.Exit(1) diff --git a/pkg/api/roboscale.io/v1alpha1/robot_types.go b/pkg/api/roboscale.io/v1alpha1/robot_types.go index 403cce47..88a5a0e6 100644 --- a/pkg/api/roboscale.io/v1alpha1/robot_types.go +++ b/pkg/api/roboscale.io/v1alpha1/robot_types.go @@ -415,7 +415,8 @@ const ( // DiscoveryServerSpec defines the desired state of DiscoveryServer. type DiscoveryServerSpec struct { - Protocol corev1.Protocol `json:"protocol,omitempty"` + // +kubebuilder:validation:Enum=TCP;UDP + Protocol string `json:"protocol,omitempty"` // ROS domain ID for robot. See https://docs.ros.org/en/foxy/Concepts/About-Domain-ID.html. // +kubebuilder:validation:Minimum=0 // +kubebuilder:validation:Maximum=101 diff --git a/pkg/api/roboscale.io/v1alpha1/robot_webhook.go b/pkg/api/roboscale.io/v1alpha1/robot_webhook.go index 4b86a0d7..5c7f32a3 100644 --- a/pkg/api/roboscale.io/v1alpha1/robot_webhook.go +++ b/pkg/api/roboscale.io/v1alpha1/robot_webhook.go @@ -39,6 +39,7 @@ func (r *Robot) Default() { _ = r.setRepositoryInfo() r.setWorkspacesPath() r.setDiscoveryServerDomainID() + r.setDiscoveryServerProtocol() } //+kubebuilder:webhook:path=/validate-robot-roboscale-io-v1alpha1-robot,mutating=false,failurePolicy=fail,sideEffects=None,groups=robot.roboscale.io,resources=robots,verbs=create;update,versions=v1alpha1,name=vrobot.kb.io,admissionReviewVersions=v1 @@ -273,8 +274,8 @@ func (r *Robot) checkAdditionalConfigs() error { } func (r *Robot) setDefaultLabels() { - if _, ok := r.Labels[internal.ROBOT_IMAGE_REGISTRY_LABEL_KEY]; !ok { - r.Labels[internal.ROBOT_IMAGE_REGISTRY_LABEL_KEY] = "docker.io" + if _, ok := r.Labels[internal.IMAGE_REGISTRY_LABEL_KEY]; !ok { + r.Labels[internal.IMAGE_REGISTRY_LABEL_KEY] = "docker.io" } } @@ -336,6 +337,12 @@ func (r *Robot) setDiscoveryServerDomainID() { } } +func (r *Robot) setDiscoveryServerProtocol() { + if reflect.DeepEqual(r.Spec.Type, TypeRobot) { + r.Spec.RobotConfig.DiscoveryServerTemplate.Protocol = "UDP" + } +} + // ******************************** // DiscoveryServer webhooks // ******************************** diff --git a/pkg/api/roboscale.io/v1alpha1/shared_types.go b/pkg/api/roboscale.io/v1alpha1/shared_types.go index 3716a6ea..1831d0fd 100644 --- a/pkg/api/roboscale.io/v1alpha1/shared_types.go +++ b/pkg/api/roboscale.io/v1alpha1/shared_types.go @@ -142,7 +142,8 @@ func GetRelayServerServicePath(rs RelayServer, postfix string) string { func GetServiceDNS(obj metav1.Object, prefix, postfix string) string { tenancy := label.GetTenancy(obj) - connectionStr := tenancy.Team + "." + tenancy.Domain + GetServicePath(obj, postfix) + platformMeta := label.GetPlatformMeta(obj) + connectionStr := tenancy.Team + "." + platformMeta.Domain + GetServicePath(obj, postfix) if prefix != "" { connectionStr = prefix + connectionStr @@ -167,7 +168,8 @@ func GetServicePath(obj metav1.Object, postfix string) string { func GetServiceDNSWithNodePort(obj metav1.Object, prefix, port string) string { tenancy := label.GetTenancy(obj) - connectionStr := tenancy.Team + "." + tenancy.Domain + ":" + port + platformMeta := label.GetPlatformMeta(obj) + connectionStr := tenancy.Team + "." + platformMeta.Domain + ":" + port if prefix != "" { connectionStr = prefix + connectionStr diff --git a/pkg/api/roboscale.io/v1alpha2/phases.go b/pkg/api/roboscale.io/v1alpha2/phases.go index b23557a7..90607750 100644 --- a/pkg/api/roboscale.io/v1alpha2/phases.go +++ b/pkg/api/roboscale.io/v1alpha2/phases.go @@ -24,7 +24,6 @@ const ( type CodeEditorPhase string const ( - CodeEditorPhaseCreatingPVCs CodeEditorPhase = "CreatingPVCs" - CodeEditorPhaseCreatingDeployment CodeEditorPhase = "CreatingDeployment" - CodeEditorPhaseReady CodeEditorPhase = "Ready" + CodeEditorPhaseConfiguringResources CodeEditorPhase = "ConfiguringResources" + CodeEditorPhaseReady CodeEditorPhase = "Ready" ) diff --git a/pkg/api/roboscale.io/v1alpha2/shared_types.go b/pkg/api/roboscale.io/v1alpha2/shared_types.go index f8984dae..ef75aeb3 100644 --- a/pkg/api/roboscale.io/v1alpha2/shared_types.go +++ b/pkg/api/roboscale.io/v1alpha2/shared_types.go @@ -1,8 +1,10 @@ package v1alpha2 import ( + "github.com/robolaunch/robot-operator/internal/label" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Generic status for any owned resource. @@ -48,3 +50,48 @@ type OwnedDeploymentStatus struct { // Container statuses. ContainerStatuses []corev1.ContainerStatus `json:"containerStatuses,omitempty"` } + +type OwnedServiceStatus struct { + // Generic status for any owned resource. + Resource OwnedResourceStatus `json:"resource,omitempty"` + // Connection URL. + URLs map[string]string `json:"urls,omitempty"` +} + +func GetServiceDNS(obj metav1.Object, prefix, postfix string) string { + tenancy := label.GetTenancy(obj) + platformMeta := label.GetPlatformMeta(obj) + connectionStr := tenancy.Team + "." + platformMeta.Domain + GetServicePath(obj, postfix) + + if prefix != "" { + connectionStr = prefix + connectionStr + } + + return connectionStr +} + +func GetServiceDNSWithNodePort(obj metav1.Object, prefix, port string) string { + tenancy := label.GetTenancy(obj) + platformMeta := label.GetPlatformMeta(obj) + connectionStr := tenancy.Team + "." + platformMeta.Domain + ":" + port + + if prefix != "" { + connectionStr = prefix + connectionStr + } + + return connectionStr +} + +func GetServicePath(obj metav1.Object, postfix string) string { + tenancy := label.GetTenancy(obj) + connectionStr := "/" + tenancy.Region + + "/" + tenancy.CloudInstanceAlias + + "/" + obj.GetNamespace() + + "/" + obj.GetName() + + if postfix != "" { + connectionStr = connectionStr + postfix + } + + return connectionStr +} diff --git a/pkg/api/roboscale.io/v1alpha2/toolkit_helpers.go b/pkg/api/roboscale.io/v1alpha2/toolkit_helpers.go index 242f91e8..736be395 100644 --- a/pkg/api/roboscale.io/v1alpha2/toolkit_helpers.go +++ b/pkg/api/roboscale.io/v1alpha2/toolkit_helpers.go @@ -49,3 +49,17 @@ func (codeEditor *CodeEditor) GetDeploymentMetadata() *types.NamespacedName { Namespace: codeEditor.Namespace, } } + +func (codeEditor *CodeEditor) GetServiceMetadata() *types.NamespacedName { + return &types.NamespacedName{ + Name: codeEditor.Name + internal.SERVICE_POSTFIX, + Namespace: codeEditor.Namespace, + } +} + +func (codeEditor *CodeEditor) GetIngressMetadata() *types.NamespacedName { + return &types.NamespacedName{ + Name: codeEditor.Name + internal.INGRESS_POSTFIX, + Namespace: codeEditor.Namespace, + } +} diff --git a/pkg/api/roboscale.io/v1alpha2/toolkit_types.go b/pkg/api/roboscale.io/v1alpha2/toolkit_types.go index aa5e5421..bb8289f9 100644 --- a/pkg/api/roboscale.io/v1alpha2/toolkit_types.go +++ b/pkg/api/roboscale.io/v1alpha2/toolkit_types.go @@ -128,8 +128,9 @@ type CodeEditorContainer struct { // CodeEditorSpec defines the desired state of CodeEditor. type CodeEditorSpec struct { - // If `true`, code editor will be consumed as `root` user. - Root bool `json:"root,omitempty"` + // App version of the code editor. + // +kubebuilder:default="4.22.0" + Version string `json:"version"` // If `true`, code editor will be consumed remotely. Remote bool `json:"remote,omitempty"` // Configurational parameters for code editor container. @@ -137,6 +138,14 @@ type CodeEditorSpec struct { // Port that code editor will use inside the container. // +kubebuilder:default=9000 Port int32 `json:"port"` + // Service type of CodeEditor. `ClusterIP` and `NodePort` is supported. + // +kubebuilder:validation:Enum=ClusterIP;NodePort + // +kubebuilder:default="ClusterIP" + ServiceType corev1.ServiceType `json:"serviceType,omitempty"` + // CodeEditor will create an Ingress resource if `true`. + Ingress bool `json:"ingress,omitempty"` + // Name of the TLS secret for ingress resource. + TLSSecretName string `json:"tlsSecretName,omitempty"` // Volume templates for ROS 2 workload. // For each volume template, operator will create a PersistentVolumeClaim // that can be mounted to the ROS 2 workload. @@ -155,4 +164,10 @@ type CodeEditorStatus struct { ExternalVolumeStatuses []ExternalVolumeStatus `json:"externalVolumeStatuses,omitempty"` // Status of code editor deployment. DeploymentStatus OwnedDeploymentStatus `json:"deploymentStatus,omitempty"` + // Status of code editor service. + ServiceStatus OwnedServiceStatus `json:"serviceStatus,omitempty"` + // Status of CodeEditor Ingress. + IngressStatus OwnedResourceStatus `json:"ingressStatus,omitempty"` + // Field to indicate if the workload should be restarted. + WorkloadUpdateNeeded bool `json:"workloadUpdateNeeded,omitempty"` } diff --git a/pkg/api/roboscale.io/v1alpha2/toolkit_webhook.go b/pkg/api/roboscale.io/v1alpha2/toolkit_webhook.go index 196f745e..4027b4aa 100644 --- a/pkg/api/roboscale.io/v1alpha2/toolkit_webhook.go +++ b/pkg/api/roboscale.io/v1alpha2/toolkit_webhook.go @@ -20,6 +20,9 @@ import ( "errors" "reflect" + "github.com/robolaunch/robot-operator/internal" + "github.com/robolaunch/robot-operator/internal/label" + "github.com/robolaunch/robot-operator/internal/platform" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" @@ -105,12 +108,24 @@ var _ webhook.Validator = &CodeEditor{} // ValidateCreate implements webhook.Validator so a webhook will be registered for the type func (r *CodeEditor) ValidateCreate() error { codeeditorlog.Info("validate create", "name", r.Name) + + err := r.validateVersion() + if err != nil { + return err + } + return nil } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type func (r *CodeEditor) ValidateUpdate(old runtime.Object) error { codeeditorlog.Info("validate update", "name", r.Name) + + err := r.validateVersion() + if err != nil { + return err + } + return nil } @@ -119,3 +134,14 @@ func (r *CodeEditor) ValidateDelete() error { codeeditorlog.Info("validate delete", "name", r.Name) return nil } + +func (r *CodeEditor) validateVersion() error { + platformMeta := label.GetPlatformMeta(r) + + _, err := platform.GetToolsImage(r, platformMeta.Version, internal.CODE_EDITOR_APP_NAME, r.Spec.Version) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/api/roboscale.io/v1alpha2/zz_generated.deepcopy.go b/pkg/api/roboscale.io/v1alpha2/zz_generated.deepcopy.go index e663b3cc..98898c47 100644 --- a/pkg/api/roboscale.io/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/api/roboscale.io/v1alpha2/zz_generated.deepcopy.go @@ -154,6 +154,8 @@ func (in *CodeEditorStatus) DeepCopyInto(out *CodeEditorStatus) { copy(*out, *in) } in.DeploymentStatus.DeepCopyInto(&out.DeploymentStatus) + in.ServiceStatus.DeepCopyInto(&out.ServiceStatus) + out.IngressStatus = in.IngressStatus } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CodeEditorStatus. @@ -259,6 +261,29 @@ func (in *OwnedResourceStatus) DeepCopy() *OwnedResourceStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OwnedServiceStatus) DeepCopyInto(out *OwnedServiceStatus) { + *out = *in + out.Resource = in.Resource + if in.URLs != nil { + in, out := &in.URLs, &out.URLs + *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 OwnedServiceStatus. +func (in *OwnedServiceStatus) DeepCopy() *OwnedServiceStatus { + if in == nil { + return nil + } + out := new(OwnedServiceStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OwnedStatefulSetStatus) DeepCopyInto(out *OwnedStatefulSetStatus) { *out = *in diff --git a/pkg/controllers/v1alpha2/production/ros2_workload/register.go b/pkg/controllers/v1alpha2/production/ros2_workload/register.go index 70a75fc6..fa4e3c0b 100644 --- a/pkg/controllers/v1alpha2/production/ros2_workload/register.go +++ b/pkg/controllers/v1alpha2/production/ros2_workload/register.go @@ -13,6 +13,22 @@ func (r *ROS2WorkloadReconciler) registerPVCs(ctx context.Context, instance *rob pvcStatuses := []robotv1alpha2.OwnedPVCStatus{} if len(instance.Spec.VolumeClaimTemplates) != len(instance.Status.PVCStatuses) { + + if len(instance.Status.PVCStatuses) > len(instance.Spec.VolumeClaimTemplates) { + for key := len(instance.Spec.VolumeClaimTemplates); key < len(instance.Status.PVCStatuses); key++ { + pvc := corev1.PersistentVolumeClaim{} + err := r.Get(ctx, *instance.GetPersistentVolumeClaimMetadata(key), &pvc) + if err != nil { + return err + } + + err = r.Delete(ctx, &pvc) + if err != nil { + return err + } + } + } + for key := range instance.Spec.VolumeClaimTemplates { pvcStatus := robotv1alpha2.OwnedPVCStatus{ Resource: robotv1alpha2.OwnedResourceStatus{ diff --git a/pkg/controllers/v1alpha2/production/ros2_workload/update.go b/pkg/controllers/v1alpha2/production/ros2_workload/update.go index d866e8ed..ff89d50d 100644 --- a/pkg/controllers/v1alpha2/production/ros2_workload/update.go +++ b/pkg/controllers/v1alpha2/production/ros2_workload/update.go @@ -4,7 +4,6 @@ import ( "context" v1alpha2_resources "github.com/robolaunch/robot-operator/internal/resources/v1alpha2" - "k8s.io/apimachinery/pkg/api/errors" ctrl "sigs.k8s.io/controller-runtime" robotv1alpha2 "github.com/robolaunch/robot-operator/pkg/api/roboscale.io/v1alpha2" @@ -20,9 +19,7 @@ func (r *ROS2WorkloadReconciler) updateDiscoveryServer(ctx context.Context, inst } err = r.Update(ctx, discoveryServer) - if err != nil && errors.IsAlreadyExists(err) { - return nil - } else if err != nil { + if err != nil { return err } @@ -40,9 +37,7 @@ func (r *ROS2WorkloadReconciler) updateROS2Bridge(ctx context.Context, instance } err = r.Update(ctx, ros2Bridge) - if err != nil && errors.IsAlreadyExists(err) { - return nil - } else if err != nil { + if err != nil { return err } @@ -65,9 +60,7 @@ func (r *ROS2WorkloadReconciler) updateStatefulSet(ctx context.Context, instance } err = r.Update(ctx, statefulSet) - if err != nil && errors.IsAlreadyExists(err) { - return nil - } else if err != nil { + if err != nil { return err } diff --git a/pkg/controllers/v1alpha2/toolkit/code_editor/check.go b/pkg/controllers/v1alpha2/toolkit/code_editor/check.go index 61c7049c..2c335168 100644 --- a/pkg/controllers/v1alpha2/toolkit/code_editor/check.go +++ b/pkg/controllers/v1alpha2/toolkit/code_editor/check.go @@ -2,13 +2,23 @@ package code_editor import ( "context" + "errors" + "reflect" + "strconv" + "github.com/robolaunch/robot-operator/internal" + "github.com/robolaunch/robot-operator/internal/label" + "github.com/robolaunch/robot-operator/internal/platform" "github.com/robolaunch/robot-operator/internal/reference" robotv1alpha2 "github.com/robolaunch/robot-operator/pkg/api/roboscale.io/v1alpha2" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" + networkingv1 "k8s.io/api/networking/v1" + k8sErr "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" ) func (r *CodeEditorReconciler) reconcileCheckPVCs(ctx context.Context, instance *robotv1alpha2.CodeEditor) error { @@ -17,7 +27,7 @@ func (r *CodeEditorReconciler) reconcileCheckPVCs(ctx context.Context, instance pvcQuery := &corev1.PersistentVolumeClaim{} err := r.Get(ctx, *instance.GetPersistentVolumeClaimMetadata(key), pvcQuery) - if err != nil && errors.IsNotFound(err) { + if err != nil && k8sErr.IsNotFound(err) { pvcStatus.Resource.Created = false } else if err != nil { return err @@ -43,7 +53,7 @@ func (r *CodeEditorReconciler) reconcileCheckExternalVolumes(ctx context.Context Namespace: instance.Namespace, Name: evStatus.Name, }, pvcQuery) - if err != nil && errors.IsNotFound(err) { + if err != nil && k8sErr.IsNotFound(err) { evStatus.Exists = false } else if err != nil { return err @@ -62,13 +72,75 @@ func (r *CodeEditorReconciler) reconcileCheckDeployment(ctx context.Context, ins deploymentQuery := &appsv1.Deployment{} err := r.Get(ctx, *instance.GetDeploymentMetadata(), deploymentQuery) - if err != nil && errors.IsNotFound(err) { + if err != nil && k8sErr.IsNotFound(err) { instance.Status.DeploymentStatus = robotv1alpha2.OwnedDeploymentStatus{} } else if err != nil { return err } else { - // make deployment dynamic + platformMeta := label.GetPlatformMeta(instance) + + desiredImage, err := platform.GetToolsImage(instance, platformMeta.Version, internal.CODE_EDITOR_APP_NAME, instance.Spec.Version) + if err != nil { + return err + } + + actualImage := deploymentQuery.Spec.Template.Spec.Containers[0].Image + + imageSynced := reflect.DeepEqual(desiredImage, actualImage) + + remoteConfigSynced := (instance.Spec.Remote && reflect.DeepEqual(deploymentQuery.Spec.Template.Spec.Hostname, instance.Name) && reflect.DeepEqual(deploymentQuery.Spec.Template.Spec.Subdomain, instance.Name)) || + (!instance.Spec.Remote && reflect.DeepEqual(deploymentQuery.Spec.Template.Spec.Hostname, "") && reflect.DeepEqual(deploymentQuery.Spec.Template.Spec.Subdomain, "")) + + volumeMountsSynced := reflect.DeepEqual(instance.Spec.Container.VolumeMounts, deploymentQuery.Spec.Template.Spec.Containers[0].VolumeMounts) + + desiredPort := instance.Spec.Port + var actualPort int32 + if len(deploymentQuery.Spec.Template.Spec.Containers) > 0 { + cont := deploymentQuery.Spec.Template.Spec.Containers[0] + for _, cPort := range cont.Ports { + if cPort.Name == internal.CODE_EDITOR_PORT_NAME { + actualPort = cPort.ContainerPort + } + } + } + + portSynced := reflect.DeepEqual(desiredPort, actualPort) + + if !imageSynced || + !remoteConfigSynced || + !volumeMountsSynced || + !portSynced || + instance.Status.WorkloadUpdateNeeded { + err := r.updateDeployment(ctx, instance) + if err != nil { + return err + } + instance.Status.WorkloadUpdateNeeded = false + } + + // update container statuses + newReq, err := labels.NewRequirement(internal.CODE_EDITOR_CONTAINER_SELECTOR_LABEL_KEY, selection.In, []string{instance.Name}) + if err != nil { + return err + } + podSelector := labels.NewSelector().Add([]labels.Requirement{*newReq}...) + + podList := corev1.PodList{} + err = r.List(ctx, &podList, &client.ListOptions{ + LabelSelector: podSelector, + }) + if err != nil && k8sErr.IsNotFound(err) { + instance.Status.DeploymentStatus.ContainerStatuses = []corev1.ContainerStatus{} + } else if err != nil { + return err + } else { + containerStatuses := []corev1.ContainerStatus{} + for _, pod := range podList.Items { + containerStatuses = append(containerStatuses, pod.Status.ContainerStatuses...) + } + instance.Status.DeploymentStatus.ContainerStatuses = containerStatuses + } instance.Status.DeploymentStatus.Resource.Created = true reference.SetReference(&instance.Status.DeploymentStatus.Resource.Reference, deploymentQuery.TypeMeta, deploymentQuery.ObjectMeta) @@ -76,3 +148,163 @@ func (r *CodeEditorReconciler) reconcileCheckDeployment(ctx context.Context, ins return nil } + +func (r *CodeEditorReconciler) reconcileCheckService(ctx context.Context, instance *robotv1alpha2.CodeEditor) error { + + serviceQuery := &corev1.Service{} + err := r.Get(ctx, *instance.GetServiceMetadata(), serviceQuery) + if err != nil && k8sErr.IsNotFound(err) { + instance.Status.ServiceStatus = robotv1alpha2.OwnedServiceStatus{} + } else if err != nil { + return err + } else { + + remoteConfigSynced := (instance.Spec.Remote && reflect.DeepEqual(serviceQuery.Spec.ClusterIP, corev1.ClusterIPNone)) || + (!instance.Spec.Remote && reflect.DeepEqual(serviceQuery.Spec.Type, instance.Spec.ServiceType) && !reflect.DeepEqual(serviceQuery.Spec.ClusterIP, corev1.ClusterIPNone)) + + desiredPort := instance.Spec.Port + var actualPort int32 + for _, sPort := range serviceQuery.Spec.Ports { + if sPort.Name == internal.CODE_EDITOR_PORT_NAME { + actualPort = sPort.Port + } + } + + portSynced := reflect.DeepEqual(desiredPort, actualPort) + + serviceTypeSynced := instance.Spec.Remote || (!instance.Spec.Remote && reflect.DeepEqual(instance.Spec.ServiceType, serviceQuery.Spec.Type)) + + if !remoteConfigSynced { + err := r.Delete(ctx, serviceQuery) + if err != nil { + return err + } + r.Recorder.Event(instance, "Normal", "Deleted", "Service '"+instance.GetServiceMetadata().Name+"' is deleted to sync resources.") + instance.Status.ServiceStatus = robotv1alpha2.OwnedServiceStatus{} + return nil + } + + if !portSynced || + !serviceTypeSynced { + err := r.updateService(ctx, instance) + if err != nil { + return err + } + } + + // set url(s) + + urls := map[string]string{} + + if instance.Spec.Remote { + + // this part will be populated after implementing relay mechanism + + } else { + + if instance.Spec.Ingress { + + urls = map[string]string{ + internal.CODE_EDITOR_PORT_NAME: robotv1alpha2.GetServiceDNS(instance, "https://", "/"+internal.CODE_EDITOR_APP_NAME), + internal.FILE_BROWSER_PORT_NAME: robotv1alpha2.GetServiceDNS(instance, "https://", "/"+internal.FILE_BROWSER_PORT_NAME), + } + + } else { + + if instance.Spec.ServiceType == corev1.ServiceTypeNodePort { + + if len(serviceQuery.Spec.Ports) == 2 { + + var ceNodePort int32 + var fbNodePort int32 + for _, sPort := range serviceQuery.Spec.Ports { + if sPort.Name == internal.CODE_EDITOR_PORT_NAME { + ceNodePort = sPort.NodePort + } + if sPort.Name == internal.FILE_BROWSER_PORT_NAME { + fbNodePort = sPort.NodePort + } + } + + urls = map[string]string{ + internal.CODE_EDITOR_PORT_NAME: robotv1alpha2.GetServiceDNSWithNodePort(instance, "http://", strconv.Itoa(int(ceNodePort))), + internal.FILE_BROWSER_PORT_NAME: robotv1alpha2.GetServiceDNSWithNodePort(instance, "http://", strconv.Itoa(int(fbNodePort))), + } + } else { + return errors.New("wrong number of ports in service") + } + + } + + } + + } + + instance.Status.ServiceStatus.URLs = urls + instance.Status.ServiceStatus.Resource.Created = true + reference.SetReference(&instance.Status.ServiceStatus.Resource.Reference, serviceQuery.TypeMeta, serviceQuery.ObjectMeta) + } + + return nil +} + +func (r *CodeEditorReconciler) reconcileCheckIngress(ctx context.Context, instance *robotv1alpha2.CodeEditor) error { + + ingressQuery := &networkingv1.Ingress{} + err := r.Get(ctx, *instance.GetIngressMetadata(), ingressQuery) + if err != nil && k8sErr.IsNotFound(err) { + instance.Status.IngressStatus = robotv1alpha2.OwnedResourceStatus{} + } else if err != nil { + return err + } else { + + desiredPort := instance.Spec.Port + var actualPort int32 + if len(ingressQuery.Spec.Rules) > 0 { + rule := ingressQuery.Spec.Rules[0] + if len(rule.HTTP.Paths) > 0 { + codeEditorPath := rule.HTTP.Paths[0] + actualPort = codeEditorPath.Backend.Service.Port.Number + } else { + return errors.New("wrong number of ingress paths") + } + } else { + return errors.New("wrong number of ingress rules") + } + + portSynced := reflect.DeepEqual(desiredPort, actualPort) + + actualSecretName := "" + if len(ingressQuery.Spec.TLS) > 0 { + tls := ingressQuery.Spec.TLS[0] + actualSecretName = tls.SecretName + } else { + return errors.New("ingress host is not found") + } + + secretNameSynced := reflect.DeepEqual(instance.Spec.TLSSecretName, actualSecretName) + + if !portSynced || + !secretNameSynced { + err := r.updateIngress(ctx, instance) + if err != nil { + return err + } + } + + if !instance.Spec.Ingress { + err := r.Delete(ctx, ingressQuery) + if err != nil { + return err + } + r.Recorder.Event(instance, "Normal", "Deleted", "Ingress '"+instance.GetIngressMetadata().Name+"' is deleted to sync resources.") + + instance.Status.IngressStatus = robotv1alpha2.OwnedResourceStatus{} + } + + instance.Status.IngressStatus.Created = true + reference.SetReference(&instance.Status.IngressStatus.Reference, ingressQuery.TypeMeta, ingressQuery.ObjectMeta) + } + + return nil +} diff --git a/pkg/controllers/v1alpha2/toolkit/code_editor/codeeditor_controller.go b/pkg/controllers/v1alpha2/toolkit/code_editor/codeeditor_controller.go index 3eb22eb6..febfd29f 100644 --- a/pkg/controllers/v1alpha2/toolkit/code_editor/codeeditor_controller.go +++ b/pkg/controllers/v1alpha2/toolkit/code_editor/codeeditor_controller.go @@ -19,11 +19,14 @@ package code_editor import ( "context" + "github.com/robolaunch/robot-operator/internal" robotErr "github.com/robolaunch/robot-operator/internal/error" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" @@ -35,7 +38,8 @@ import ( // CodeEditorReconciler reconciles a CodeEditor object type CodeEditorReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + Recorder record.EventRecorder } //+kubebuilder:rbac:groups=robot.roboscale.io,resources=codeeditors,verbs=get;list;watch;create;update;patch;delete @@ -44,6 +48,9 @@ type CodeEditorReconciler struct { //+kubebuilder:rbac:groups=core,resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch;delete var logger logr.Logger @@ -60,11 +67,14 @@ func (r *CodeEditorReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{}, err } - r.reconcileRegisterResources(instance) + err = r.reconcileRegisterResources(ctx, instance) + if err != nil { + return result, err + } err = r.reconcileUpdateInstanceStatus(ctx, instance) if err != nil { - return ctrl.Result{}, err + return result, err } err = r.reconcileCheckStatus(ctx, instance, &result) @@ -74,26 +84,33 @@ func (r *CodeEditorReconciler) Reconcile(ctx context.Context, req ctrl.Request) err = r.reconcileUpdateInstanceStatus(ctx, instance) if err != nil { - return ctrl.Result{}, err + return result, err } err = r.reconcileCheckResources(ctx, instance) if err != nil { - return ctrl.Result{}, err + return result, err } err = r.reconcileUpdateInstanceStatus(ctx, instance) if err != nil { - return ctrl.Result{}, err + return result, err } return result, nil } -func (r *CodeEditorReconciler) reconcileRegisterResources(instance *robotv1alpha2.CodeEditor) error { +func (r *CodeEditorReconciler) reconcileRegisterResources(ctx context.Context, instance *robotv1alpha2.CodeEditor) error { + + err := r.registerPVCs(ctx, instance) + if err != nil { + return err + } - r.registerPVCs(instance) - r.registerExternalVolumes(instance) + err = r.registerExternalVolumes(ctx, instance) + if err != nil { + return err + } return nil } @@ -110,6 +127,16 @@ func (r *CodeEditorReconciler) reconcileCheckStatus(ctx context.Context, instanc return robotErr.CheckCreatingOrWaitingError(result, err) } + err = r.reconcileHandleService(ctx, instance) + if err != nil { + return robotErr.CheckCreatingOrWaitingError(result, err) + } + + err = r.reconcileHandleIngress(ctx, instance) + if err != nil { + return robotErr.CheckCreatingOrWaitingError(result, err) + } + return nil } @@ -130,6 +157,44 @@ func (r *CodeEditorReconciler) reconcileCheckResources(ctx context.Context, inst return err } + err = r.reconcileCheckService(ctx, instance) + if err != nil { + return err + } + + err = r.reconcileCheckIngress(ctx, instance) + if err != nil { + return err + } + + err = r.reconcileCalculatePhase(ctx, instance) + if err != nil { + return err + } + + return nil +} + +func (r *CodeEditorReconciler) reconcileCalculatePhase(ctx context.Context, instance *robotv1alpha2.CodeEditor) error { + + containersReady := true + if len(instance.Status.DeploymentStatus.ContainerStatuses) > 0 { + for _, cStatus := range instance.Status.DeploymentStatus.ContainerStatuses { + containersReady = containersReady && cStatus.Ready + } + } else { + containersReady = false + } + + if containersReady && instance.Status.ServiceStatus.Resource.Created && (instance.Spec.Ingress == instance.Status.IngressStatus.Created) { + if codeEditorURL, ok := instance.Status.ServiceStatus.URLs[internal.CODE_EDITOR_PORT_NAME]; ok && instance.Status.Phase != robotv1alpha2.CodeEditorPhaseReady { + r.Recorder.Event(instance, "Normal", "Ready", "CodeEditor is accessible over the URL '"+codeEditorURL+"'.") + } + instance.Status.Phase = robotv1alpha2.CodeEditorPhaseReady + } else { + instance.Status.Phase = robotv1alpha2.CodeEditorPhaseConfiguringResources + } + return nil } @@ -139,5 +204,7 @@ func (r *CodeEditorReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&robotv1alpha2.CodeEditor{}). Owns(&corev1.PersistentVolumeClaim{}). Owns(&appsv1.Deployment{}). + Owns(&corev1.Service{}). + Owns(&networkingv1.Ingress{}). Complete(r) } diff --git a/pkg/controllers/v1alpha2/toolkit/code_editor/create.go b/pkg/controllers/v1alpha2/toolkit/code_editor/create.go index 2992302e..169210ec 100644 --- a/pkg/controllers/v1alpha2/toolkit/code_editor/create.go +++ b/pkg/controllers/v1alpha2/toolkit/code_editor/create.go @@ -25,6 +25,8 @@ func (r *CodeEditorReconciler) createPersistentVolumeClaim(ctx context.Context, return err } + r.Recorder.Event(instance, "Normal", "Created", "PersistentVolumeClaim '"+instance.GetPersistentVolumeClaimMetadata(key).Name+"' is created.") + logger.Info("STATUS: PVC " + instance.GetPersistentVolumeClaimMetadata(key).Name + " is created.") return nil } @@ -50,6 +52,52 @@ func (r *CodeEditorReconciler) createDeployment(ctx context.Context, instance *r return err } + r.Recorder.Event(instance, "Normal", "Created", "Deployment '"+instance.GetDeploymentMetadata().Name+"' is created.") + logger.Info("STATUS: Deployment " + instance.GetDeploymentMetadata().Name + " is created.") return nil } + +func (r *CodeEditorReconciler) createService(ctx context.Context, instance *robotv1alpha2.CodeEditor) error { + + service := v1alpha2_resources.GetCodeEditorService(instance, instance.GetServiceMetadata()) + + err := ctrl.SetControllerReference(instance, service, r.Scheme) + if err != nil { + return err + } + + err = r.Create(ctx, service) + if err != nil && errors.IsAlreadyExists(err) { + return nil + } else if err != nil { + return err + } + + r.Recorder.Event(instance, "Normal", "Created", "Service '"+instance.GetDeploymentMetadata().Name+"' is created.") + + logger.Info("STATUS: Service " + instance.GetServiceMetadata().Name + " is created.") + return nil +} + +func (r *CodeEditorReconciler) createIngress(ctx context.Context, instance *robotv1alpha2.CodeEditor) error { + + ingress := v1alpha2_resources.GetCodeEditorIngress(instance, instance.GetIngressMetadata()) + + err := ctrl.SetControllerReference(instance, ingress, r.Scheme) + if err != nil { + return err + } + + err = r.Create(ctx, ingress) + if err != nil && errors.IsAlreadyExists(err) { + return nil + } else if err != nil { + return err + } + + r.Recorder.Event(instance, "Normal", "Created", "Ingress '"+instance.GetDeploymentMetadata().Name+"' is created.") + + logger.Info("STATUS: Ingress " + instance.GetIngressMetadata().Name + " is created.") + return nil +} diff --git a/pkg/controllers/v1alpha2/toolkit/code_editor/handle.go b/pkg/controllers/v1alpha2/toolkit/code_editor/handle.go index 78cc7a98..8441ee3c 100644 --- a/pkg/controllers/v1alpha2/toolkit/code_editor/handle.go +++ b/pkg/controllers/v1alpha2/toolkit/code_editor/handle.go @@ -13,7 +13,6 @@ func (r *CodeEditorReconciler) reconcileHandlePVCs(ctx context.Context, instance for key, pvcStatus := range instance.Status.PVCStatuses { if !pvcStatus.Resource.Created { - instance.Status.Phase = robotv1alpha2.CodeEditorPhaseCreatingPVCs err := r.createPersistentVolumeClaim(ctx, instance, key) if err != nil { return err @@ -46,7 +45,6 @@ func (r *CodeEditorReconciler) reconcileHandleDeployment(ctx context.Context, in if volumesReady && !instance.Status.DeploymentStatus.Resource.Created { - instance.Status.Phase = robotv1alpha2.CodeEditorPhaseCreatingDeployment err := r.createDeployment(ctx, instance) if err != nil { return err @@ -63,3 +61,45 @@ func (r *CodeEditorReconciler) reconcileHandleDeployment(ctx context.Context, in return nil } + +func (r *CodeEditorReconciler) reconcileHandleService(ctx context.Context, instance *robotv1alpha2.CodeEditor) error { + + if !instance.Status.ServiceStatus.Resource.Created { + + err := r.createService(ctx, instance) + if err != nil { + return err + } + + instance.Status.ServiceStatus.Resource.Created = true + + return &robotErr.CreatingResourceError{ + ResourceKind: "Service", + ResourceName: instance.GetServiceMetadata().Name, + ResourceNamespace: instance.GetServiceMetadata().Namespace, + } + } + + return nil +} + +func (r *CodeEditorReconciler) reconcileHandleIngress(ctx context.Context, instance *robotv1alpha2.CodeEditor) error { + + if instance.Spec.Ingress && !instance.Status.IngressStatus.Created { + + err := r.createIngress(ctx, instance) + if err != nil { + return err + } + + instance.Status.IngressStatus.Created = true + + return &robotErr.CreatingResourceError{ + ResourceKind: "Ingress", + ResourceName: instance.GetIngressMetadata().Name, + ResourceNamespace: instance.GetIngressMetadata().Namespace, + } + } + + return nil +} diff --git a/pkg/controllers/v1alpha2/toolkit/code_editor/register.go b/pkg/controllers/v1alpha2/toolkit/code_editor/register.go index 3c7815d8..8a7c3e33 100644 --- a/pkg/controllers/v1alpha2/toolkit/code_editor/register.go +++ b/pkg/controllers/v1alpha2/toolkit/code_editor/register.go @@ -1,15 +1,33 @@ package code_editor import ( + "context" + robotv1alpha2 "github.com/robolaunch/robot-operator/pkg/api/roboscale.io/v1alpha2" corev1 "k8s.io/api/core/v1" ) -func (r *CodeEditorReconciler) registerPVCs(instance *robotv1alpha2.CodeEditor) { +func (r *CodeEditorReconciler) registerPVCs(ctx context.Context, instance *robotv1alpha2.CodeEditor) error { pvcStatuses := []robotv1alpha2.OwnedPVCStatus{} if len(instance.Spec.VolumeClaimTemplates) != len(instance.Status.PVCStatuses) { + + if len(instance.Status.PVCStatuses) > len(instance.Spec.VolumeClaimTemplates) { + for key := len(instance.Spec.VolumeClaimTemplates); key < len(instance.Status.PVCStatuses); key++ { + pvc := corev1.PersistentVolumeClaim{} + err := r.Get(ctx, *instance.GetPersistentVolumeClaimMetadata(key), &pvc) + if err != nil { + return err + } + + err = r.Delete(ctx, &pvc) + if err != nil { + return err + } + } + } + for key := range instance.Spec.VolumeClaimTemplates { pvcStatus := robotv1alpha2.OwnedPVCStatus{ Resource: robotv1alpha2.OwnedResourceStatus{ @@ -22,10 +40,13 @@ func (r *CodeEditorReconciler) registerPVCs(instance *robotv1alpha2.CodeEditor) pvcStatuses = append(pvcStatuses, pvcStatus) } instance.Status.PVCStatuses = pvcStatuses + instance.Status.WorkloadUpdateNeeded = true } + + return nil } -func (r *CodeEditorReconciler) registerExternalVolumes(instance *robotv1alpha2.CodeEditor) { +func (r *CodeEditorReconciler) registerExternalVolumes(ctx context.Context, instance *robotv1alpha2.CodeEditor) error { evStatuses := []robotv1alpha2.ExternalVolumeStatus{} @@ -37,5 +58,8 @@ func (r *CodeEditorReconciler) registerExternalVolumes(instance *robotv1alpha2.C evStatuses = append(evStatuses, evStatus) } instance.Status.ExternalVolumeStatuses = evStatuses + instance.Status.WorkloadUpdateNeeded = true } + + return nil } diff --git a/pkg/controllers/v1alpha2/toolkit/code_editor/update.go b/pkg/controllers/v1alpha2/toolkit/code_editor/update.go new file mode 100644 index 00000000..c4da573a --- /dev/null +++ b/pkg/controllers/v1alpha2/toolkit/code_editor/update.go @@ -0,0 +1,74 @@ +package code_editor + +import ( + "context" + + v1alpha2_resources "github.com/robolaunch/robot-operator/internal/resources/v1alpha2" + robotv1alpha2 "github.com/robolaunch/robot-operator/pkg/api/roboscale.io/v1alpha2" + ctrl "sigs.k8s.io/controller-runtime" +) + +func (r *CodeEditorReconciler) updateDeployment(ctx context.Context, instance *robotv1alpha2.CodeEditor) error { + + node, err := r.reconcileGetNode(ctx, instance) + if err != nil { + return err + } + + deployment := v1alpha2_resources.GetCodeEditorDeployment(instance, instance.GetDeploymentMetadata(), *node) + + err = ctrl.SetControllerReference(instance, deployment, r.Scheme) + if err != nil { + return err + } + + err = r.Update(ctx, deployment) + if err != nil { + return err + } + + r.Recorder.Event(instance, "Normal", "Updated", "Deployment '"+instance.GetDeploymentMetadata().Name+"' is updated to sync resources.") + + logger.Info("STATUS: Deployment " + deployment.Name + " is updated.") + return nil +} + +func (r *CodeEditorReconciler) updateService(ctx context.Context, instance *robotv1alpha2.CodeEditor) error { + + service := v1alpha2_resources.GetCodeEditorService(instance, instance.GetServiceMetadata()) + + err := ctrl.SetControllerReference(instance, service, r.Scheme) + if err != nil { + return err + } + + err = r.Update(ctx, service) + if err != nil { + return err + } + + r.Recorder.Event(instance, "Normal", "Updated", "Service '"+instance.GetServiceMetadata().Name+"' is updated to sync resources.") + + logger.Info("STATUS: Service " + service.Name + " is updated.") + return nil +} + +func (r *CodeEditorReconciler) updateIngress(ctx context.Context, instance *robotv1alpha2.CodeEditor) error { + + ingress := v1alpha2_resources.GetCodeEditorIngress(instance, instance.GetIngressMetadata()) + + err := ctrl.SetControllerReference(instance, ingress, r.Scheme) + if err != nil { + return err + } + + err = r.Update(ctx, ingress) + if err != nil { + return err + } + + r.Recorder.Event(instance, "Normal", "Updated", "Ingress '"+instance.GetIngressMetadata().Name+"' is updated to sync resources.") + + logger.Info("STATUS: Ingress " + instance.GetIngressMetadata().Name + " is updated.") + return nil +}