From ea9e3993694e5474bfa835c7425e35319118f7a3 Mon Sep 17 00:00:00 2001 From: Bella Khizgiyaev Date: Mon, 3 Jul 2023 17:22:10 +0300 Subject: [PATCH 1/2] WIP: Adding initiall support for OVA migration Signed-off-by: Bella Khizgiyaev --- .bazelrc | 1 + operator/config/manager/manager.yaml | 2 + .../config/rbac/forklift-controller_role.yaml | 15 +- operator/config/rbac/role.yaml | 12 + .../forkliftcontroller/defaults/main.yml | 2 + .../roles/forkliftcontroller/tasks/main.yml | 6 + .../controller/controller-scc.yml.j2 | 12 + .../controller/deployment-controller.yml.j2 | 2 + pkg/apis/forklift/v1beta1/provider.go | 5 +- pkg/controller/host/handler/BUILD.bazel | 1 + pkg/controller/host/handler/doc.go | 6 + pkg/controller/host/handler/ova/BUILD.bazel | 17 + pkg/controller/host/handler/ova/doc.go | 22 ++ pkg/controller/host/handler/ova/handler.go | 15 + .../map/storage/handler/ova/BUILD.bazel | 18 + pkg/controller/map/storage/handler/ova/doc.go | 22 ++ .../map/storage/handler/ova/handler.go | 14 + pkg/controller/plan/handler/BUILD.bazel | 1 + pkg/controller/plan/handler/doc.go | 6 + pkg/controller/plan/handler/ova/BUILD.bazel | 22 ++ pkg/controller/plan/handler/ova/doc.go | 22 ++ pkg/controller/plan/handler/ova/handler.go | 116 ++++++ pkg/controller/provider/BUILD.bazel | 3 + pkg/controller/provider/container/BUILD.bazel | 1 + pkg/controller/provider/container/doc.go | 3 + .../provider/container/ova/BUILD.bazel | 31 ++ .../provider/container/ova/client.go | 118 ++++++ .../provider/container/ova/collector.go | 346 ++++++++++++++++++ pkg/controller/provider/container/ova/doc.go | 1 + .../provider/container/ova/model.go | 230 ++++++++++++ .../provider/container/ova/resource.go | 204 +++++++++++ .../provider/container/ova/watch.go | 344 +++++++++++++++++ pkg/controller/provider/controller.go | 175 ++++++++- pkg/controller/provider/model/BUILD.bazel | 1 + pkg/controller/provider/model/doc.go | 5 + pkg/controller/provider/model/ova/BUILD.bazel | 18 + pkg/controller/provider/model/ova/doc.go | 15 + pkg/controller/provider/model/ova/model.go | 123 +++++++ pkg/controller/provider/model/ova/tree.go | 19 + pkg/controller/provider/validation.go | 21 ++ pkg/controller/provider/web/BUILD.bazel | 1 + pkg/controller/provider/web/client.go | 9 + pkg/controller/provider/web/doc.go | 4 + pkg/controller/provider/web/ova/BUILD.bazel | 33 ++ pkg/controller/provider/web/ova/base.go | 89 +++++ pkg/controller/provider/web/ova/client.go | 294 +++++++++++++++ pkg/controller/provider/web/ova/disk.go | 172 +++++++++ pkg/controller/provider/web/ova/doc.go | 49 +++ pkg/controller/provider/web/ova/network.go | 205 +++++++++++ pkg/controller/provider/web/ova/provider.go | 192 ++++++++++ pkg/controller/provider/web/ova/resource.go | 29 ++ pkg/controller/provider/web/ova/tree.go | 187 ++++++++++ pkg/controller/provider/web/ova/vm.go | 273 ++++++++++++++ pkg/controller/provider/web/ova/workload.go | 93 +++++ pkg/controller/provider/web/provider.go | 23 ++ 55 files changed, 3644 insertions(+), 6 deletions(-) create mode 100644 operator/roles/forkliftcontroller/templates/controller/controller-scc.yml.j2 create mode 100644 pkg/controller/host/handler/ova/BUILD.bazel create mode 100644 pkg/controller/host/handler/ova/doc.go create mode 100644 pkg/controller/host/handler/ova/handler.go create mode 100644 pkg/controller/map/storage/handler/ova/BUILD.bazel create mode 100644 pkg/controller/map/storage/handler/ova/doc.go create mode 100644 pkg/controller/map/storage/handler/ova/handler.go create mode 100644 pkg/controller/plan/handler/ova/BUILD.bazel create mode 100644 pkg/controller/plan/handler/ova/doc.go create mode 100644 pkg/controller/plan/handler/ova/handler.go create mode 100644 pkg/controller/provider/container/ova/BUILD.bazel create mode 100644 pkg/controller/provider/container/ova/client.go create mode 100644 pkg/controller/provider/container/ova/collector.go create mode 100644 pkg/controller/provider/container/ova/doc.go create mode 100644 pkg/controller/provider/container/ova/model.go create mode 100644 pkg/controller/provider/container/ova/resource.go create mode 100644 pkg/controller/provider/container/ova/watch.go create mode 100644 pkg/controller/provider/model/ova/BUILD.bazel create mode 100644 pkg/controller/provider/model/ova/doc.go create mode 100644 pkg/controller/provider/model/ova/model.go create mode 100644 pkg/controller/provider/model/ova/tree.go create mode 100644 pkg/controller/provider/web/ova/BUILD.bazel create mode 100644 pkg/controller/provider/web/ova/base.go create mode 100644 pkg/controller/provider/web/ova/client.go create mode 100644 pkg/controller/provider/web/ova/disk.go create mode 100644 pkg/controller/provider/web/ova/doc.go create mode 100644 pkg/controller/provider/web/ova/network.go create mode 100644 pkg/controller/provider/web/ova/provider.go create mode 100644 pkg/controller/provider/web/ova/resource.go create mode 100644 pkg/controller/provider/web/ova/tree.go create mode 100644 pkg/controller/provider/web/ova/vm.go create mode 100644 pkg/controller/provider/web/ova/workload.go diff --git a/.bazelrc b/.bazelrc index 2213df308..fb28bc021 100644 --- a/.bazelrc +++ b/.bazelrc @@ -23,6 +23,7 @@ build --action_env=POPULATOR_CONTROLLER_IMAGE=quay.io/kubev2v/populator-controll build --action_env=OPENSTACK_POPULATOR_IMAGE=quay.io/kubev2v/openstack-populator:latest build --action_env=OVIRT_POPULATOR_IMAGE=quay.io/kubev2v/ovirt-populator:latest build --action_env=OPERATOR_IMAGE=quay.io/kubev2v/forklift-operator:latest +build --action_env=OVA_PROVIDER_SERVER_IMAGE=quay.io/kubev2v/forklift-ova-provider-server:latest # Appliance build # container_run_and_extract() does not work inside Podman and Docker diff --git a/operator/config/manager/manager.yaml b/operator/config/manager/manager.yaml index efc535889..d5751db21 100644 --- a/operator/config/manager/manager.yaml +++ b/operator/config/manager/manager.yaml @@ -68,6 +68,8 @@ spec: value: ${OVIRT_POPULATOR_IMAGE} - name: OPENSTACK_POPULATOR_IMAGE value: ${OPENSTACK_POPULATOR_IMAGE} + - name: OVA_PROVIDER_SERVER_IMAGE + value: ${OVA_PROVIDER_SERVER_IMAGE} livenessProbe: httpGet: path: /healthz diff --git a/operator/config/rbac/forklift-controller_role.yaml b/operator/config/rbac/forklift-controller_role.yaml index 068c1ed0b..21078e2e5 100644 --- a/operator/config/rbac/forklift-controller_role.yaml +++ b/operator/config/rbac/forklift-controller_role.yaml @@ -21,6 +21,7 @@ rules: # PVs added for the populator(s) that uses the same role as forklift-controller - persistentvolumes - persistentvolumeclaims + - services verbs: - get - list @@ -121,4 +122,16 @@ rules: - create - update - patch - - delete \ No newline at end of file + - delete +- apiGroups: + - apps + resources: + - deployments + verbs: + - get + - list + - watch + - create + - update + - patch + - delete diff --git a/operator/config/rbac/role.yaml b/operator/config/rbac/role.yaml index 0ccaf5ef8..c37828a77 100644 --- a/operator/config/rbac/role.yaml +++ b/operator/config/rbac/role.yaml @@ -61,3 +61,15 @@ rules: - certificates verbs: - '*' +- apiGroups: + - security.openshift.io + resources: + - securitycontextconstraints + verbs: + - create + - delete + - get + - list + - watch + - update + - patch diff --git a/operator/roles/forkliftcontroller/defaults/main.yml b/operator/roles/forkliftcontroller/defaults/main.yml index ac791da94..0268c2acc 100644 --- a/operator/roles/forkliftcontroller/defaults/main.yml +++ b/operator/roles/forkliftcontroller/defaults/main.yml @@ -115,3 +115,5 @@ must_gather_image_fqin: "{{ lookup( 'env', 'MUST_GATHER_IMAGE') or lookup( 'env' virt_v2v_image_fqin: "{{ lookup( 'env', 'VIRT_V2V_IMAGE') or lookup( 'env', 'RELATED_IMAGE_VIRT_V2V') }}" virt_v2v_warm_image_fqin: "{{ lookup( 'env', 'VIRT_V2V_WARM_IMAGE') or lookup( 'env', 'RELATED_IMAGE_VIRT_V2V_WARM') }}" virt_v2v_dont_request_kvm: "{{ lookup( 'env', 'VIRT_V2V_DONT_REQUEST_KVM') }}" + +ova_provider_server_fqin: "{{ lookup( 'env', 'OVA_PROVIDER_SERVER_IMAGE') or lookup( 'env', 'RELATED_IMAGE_OVA_PROVIDER_SERVER') }}" \ No newline at end of file diff --git a/operator/roles/forkliftcontroller/tasks/main.yml b/operator/roles/forkliftcontroller/tasks/main.yml index 3195d3c9b..72721fa47 100644 --- a/operator/roles/forkliftcontroller/tasks/main.yml +++ b/operator/roles/forkliftcontroller/tasks/main.yml @@ -79,6 +79,12 @@ definition: "{{ lookup('template', 'controller/route-inventory.yml.j2') }}" when: not k8s_cluster|bool + - name: "Setup forklift-controller security context constraints" + k8s: + state: present + definition: "{{ lookup('template', 'controller/controller-scc.yml.j2') }}" + when: not k8s_cluster|bool + - when: feature_volume_populator|bool block: - name: "Setup populator controller deployment" diff --git a/operator/roles/forkliftcontroller/templates/controller/controller-scc.yml.j2 b/operator/roles/forkliftcontroller/templates/controller/controller-scc.yml.j2 new file mode 100644 index 000000000..5d0e4cb7f --- /dev/null +++ b/operator/roles/forkliftcontroller/templates/controller/controller-scc.yml.j2 @@ -0,0 +1,12 @@ +--- +kind: SecurityContextConstraints +apiVersion: security.openshift.io/v1 +metadata: + name: forklift-controller-scc +users: + - system:serviceaccount:konveyor-forklift:forklift-controller +runAsUser: + type: RunAsAny +seLinuxContext: + type: RunAsAny +allowPrivilegedContainer: false \ No newline at end of file diff --git a/operator/roles/forkliftcontroller/templates/controller/deployment-controller.yml.j2 b/operator/roles/forkliftcontroller/templates/controller/deployment-controller.yml.j2 index 800333ec4..4f8d54443 100644 --- a/operator/roles/forkliftcontroller/templates/controller/deployment-controller.yml.j2 +++ b/operator/roles/forkliftcontroller/templates/controller/deployment-controller.yml.j2 @@ -142,6 +142,8 @@ spec: value: '8082' - name: SECRET_NAME value: webhook-server-secret + - name: OVA_PROVIDER_SERVER_IMAGE + value: {{ ova_provider_server_fqin }} {% if feature_validation|bool %} - name: POLICY_AGENT_URL value: "https://{{ validation_service_name }}.{{ app_namespace }}.svc.cluster.local:8181" diff --git a/pkg/apis/forklift/v1beta1/provider.go b/pkg/apis/forklift/v1beta1/provider.go index 6b5852b68..1f9d4a2a5 100644 --- a/pkg/apis/forklift/v1beta1/provider.go +++ b/pkg/apis/forklift/v1beta1/provider.go @@ -40,6 +40,8 @@ const ( OVirt ProviderType = "ovirt" // OpenStack OpenStack ProviderType = "openstack" + // OVA + Ova ProviderType = "ova" ) var ProviderTypes = []ProviderType{ @@ -47,6 +49,7 @@ var ProviderTypes = []ProviderType{ VSphere, OVirt, OpenStack, + Ova, } func (t ProviderType) String() string { @@ -164,5 +167,5 @@ func (p *Provider) HasReconciled() bool { // This provider requires VM guest conversion. func (p *Provider) RequiresConversion() bool { - return p.Type() == VSphere + return p.Type() == VSphere || p.Type() == Ova } diff --git a/pkg/controller/host/handler/BUILD.bazel b/pkg/controller/host/handler/BUILD.bazel index c639c2e9a..89c6ae4e2 100644 --- a/pkg/controller/host/handler/BUILD.bazel +++ b/pkg/controller/host/handler/BUILD.bazel @@ -9,6 +9,7 @@ go_library( "//pkg/apis/forklift/v1beta1", "//pkg/controller/host/handler/ocp", "//pkg/controller/host/handler/openstack", + "//pkg/controller/host/handler/ova", "//pkg/controller/host/handler/ovirt", "//pkg/controller/host/handler/vsphere", "//pkg/controller/watch/handler", diff --git a/pkg/controller/host/handler/doc.go b/pkg/controller/host/handler/doc.go index 8a752243a..ffb4c505a 100644 --- a/pkg/controller/host/handler/doc.go +++ b/pkg/controller/host/handler/doc.go @@ -4,6 +4,7 @@ import ( api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" "github.com/konveyor/forklift-controller/pkg/controller/host/handler/ocp" "github.com/konveyor/forklift-controller/pkg/controller/host/handler/openstack" + "github.com/konveyor/forklift-controller/pkg/controller/host/handler/ova" "github.com/konveyor/forklift-controller/pkg/controller/host/handler/ovirt" "github.com/konveyor/forklift-controller/pkg/controller/host/handler/vsphere" "github.com/konveyor/forklift-controller/pkg/controller/watch/handler" @@ -47,6 +48,11 @@ func New( client, channel, provider) + case api.Ova: + h, err = ova.New( + client, + channel, + provider) default: err = liberr.New("provider not supported.") } diff --git a/pkg/controller/host/handler/ova/BUILD.bazel b/pkg/controller/host/handler/ova/BUILD.bazel new file mode 100644 index 000000000..a725992ea --- /dev/null +++ b/pkg/controller/host/handler/ova/BUILD.bazel @@ -0,0 +1,17 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "ova", + srcs = [ + "doc.go", + "handler.go", + ], + importpath = "github.com/konveyor/forklift-controller/pkg/controller/host/handler/ova", + visibility = ["//visibility:public"], + deps = [ + "//pkg/apis/forklift/v1beta1", + "//pkg/controller/watch/handler", + "//vendor/sigs.k8s.io/controller-runtime/pkg/client", + "//vendor/sigs.k8s.io/controller-runtime/pkg/event", + ], +) diff --git a/pkg/controller/host/handler/ova/doc.go b/pkg/controller/host/handler/ova/doc.go new file mode 100644 index 000000000..72bdc1b13 --- /dev/null +++ b/pkg/controller/host/handler/ova/doc.go @@ -0,0 +1,22 @@ +package ova + +import ( + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + "github.com/konveyor/forklift-controller/pkg/controller/watch/handler" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" +) + +// Handler factory. +func New( + client client.Client, + channel chan event.GenericEvent, + provider *api.Provider) (h *Handler, err error) { + // + b, err := handler.New(client, channel, provider) + if err != nil { + return + } + h = &Handler{Handler: b} + return +} diff --git a/pkg/controller/host/handler/ova/handler.go b/pkg/controller/host/handler/ova/handler.go new file mode 100644 index 000000000..58a6ed2be --- /dev/null +++ b/pkg/controller/host/handler/ova/handler.go @@ -0,0 +1,15 @@ +package ova + +import ( + "github.com/konveyor/forklift-controller/pkg/controller/watch/handler" +) + +// Provider watch event handler. +type Handler struct { + *handler.Handler +} + +// Ensure watch on hosts. +func (r *Handler) Watch(watch *handler.WatchManager) (err error) { + return +} diff --git a/pkg/controller/map/storage/handler/ova/BUILD.bazel b/pkg/controller/map/storage/handler/ova/BUILD.bazel new file mode 100644 index 000000000..61b00207c --- /dev/null +++ b/pkg/controller/map/storage/handler/ova/BUILD.bazel @@ -0,0 +1,18 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "ova", + srcs = [ + "doc.go", + "handler.go", + ], + importpath = "github.com/konveyor/forklift-controller/pkg/controller/map/storage/handler/ova", + visibility = ["//visibility:public"], + deps = [ + "//pkg/apis/forklift/v1beta1", + "//pkg/controller/watch/handler", + "//pkg/lib/logging", + "//vendor/sigs.k8s.io/controller-runtime/pkg/client", + "//vendor/sigs.k8s.io/controller-runtime/pkg/event", + ], +) diff --git a/pkg/controller/map/storage/handler/ova/doc.go b/pkg/controller/map/storage/handler/ova/doc.go new file mode 100644 index 000000000..72bdc1b13 --- /dev/null +++ b/pkg/controller/map/storage/handler/ova/doc.go @@ -0,0 +1,22 @@ +package ova + +import ( + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + "github.com/konveyor/forklift-controller/pkg/controller/watch/handler" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" +) + +// Handler factory. +func New( + client client.Client, + channel chan event.GenericEvent, + provider *api.Provider) (h *Handler, err error) { + // + b, err := handler.New(client, channel, provider) + if err != nil { + return + } + h = &Handler{Handler: b} + return +} diff --git a/pkg/controller/map/storage/handler/ova/handler.go b/pkg/controller/map/storage/handler/ova/handler.go new file mode 100644 index 000000000..ff512cbd0 --- /dev/null +++ b/pkg/controller/map/storage/handler/ova/handler.go @@ -0,0 +1,14 @@ +package ova + +import ( + "github.com/konveyor/forklift-controller/pkg/controller/watch/handler" + "github.com/konveyor/forklift-controller/pkg/lib/logging" +) + +// Package logger. +var log = logging.WithName("storageMap|ova") + +// Provider watch event handler. +type Handler struct { + *handler.Handler +} diff --git a/pkg/controller/plan/handler/BUILD.bazel b/pkg/controller/plan/handler/BUILD.bazel index edb588a8d..f246f17f9 100644 --- a/pkg/controller/plan/handler/BUILD.bazel +++ b/pkg/controller/plan/handler/BUILD.bazel @@ -9,6 +9,7 @@ go_library( "//pkg/apis/forklift/v1beta1", "//pkg/controller/plan/handler/ocp", "//pkg/controller/plan/handler/openstack", + "//pkg/controller/plan/handler/ova", "//pkg/controller/plan/handler/ovirt", "//pkg/controller/plan/handler/vsphere", "//pkg/controller/watch/handler", diff --git a/pkg/controller/plan/handler/doc.go b/pkg/controller/plan/handler/doc.go index ba890e18c..2d4784e82 100644 --- a/pkg/controller/plan/handler/doc.go +++ b/pkg/controller/plan/handler/doc.go @@ -4,6 +4,7 @@ import ( api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" "github.com/konveyor/forklift-controller/pkg/controller/plan/handler/ocp" "github.com/konveyor/forklift-controller/pkg/controller/plan/handler/openstack" + "github.com/konveyor/forklift-controller/pkg/controller/plan/handler/ova" "github.com/konveyor/forklift-controller/pkg/controller/plan/handler/ovirt" "github.com/konveyor/forklift-controller/pkg/controller/plan/handler/vsphere" "github.com/konveyor/forklift-controller/pkg/controller/watch/handler" @@ -47,6 +48,11 @@ func New( client, channel, provider) + case api.Ova: + h, err = ova.New( + client, + channel, + provider) default: err = liberr.New("provider not supported.") } diff --git a/pkg/controller/plan/handler/ova/BUILD.bazel b/pkg/controller/plan/handler/ova/BUILD.bazel new file mode 100644 index 000000000..855c23f9b --- /dev/null +++ b/pkg/controller/plan/handler/ova/BUILD.bazel @@ -0,0 +1,22 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "ova", + srcs = [ + "doc.go", + "handler.go", + ], + importpath = "github.com/konveyor/forklift-controller/pkg/controller/plan/handler/ova", + visibility = ["//visibility:public"], + deps = [ + "//pkg/apis/forklift/v1beta1", + "//pkg/controller/provider/web/ova", + "//pkg/controller/watch/handler", + "//pkg/lib/error", + "//pkg/lib/inventory/web", + "//pkg/lib/logging", + "//vendor/golang.org/x/net/context", + "//vendor/sigs.k8s.io/controller-runtime/pkg/client", + "//vendor/sigs.k8s.io/controller-runtime/pkg/event", + ], +) diff --git a/pkg/controller/plan/handler/ova/doc.go b/pkg/controller/plan/handler/ova/doc.go new file mode 100644 index 000000000..72bdc1b13 --- /dev/null +++ b/pkg/controller/plan/handler/ova/doc.go @@ -0,0 +1,22 @@ +package ova + +import ( + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + "github.com/konveyor/forklift-controller/pkg/controller/watch/handler" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" +) + +// Handler factory. +func New( + client client.Client, + channel chan event.GenericEvent, + provider *api.Provider) (h *Handler, err error) { + // + b, err := handler.New(client, channel, provider) + if err != nil { + return + } + h = &Handler{Handler: b} + return +} diff --git a/pkg/controller/plan/handler/ova/handler.go b/pkg/controller/plan/handler/ova/handler.go new file mode 100644 index 000000000..bc113911a --- /dev/null +++ b/pkg/controller/plan/handler/ova/handler.go @@ -0,0 +1,116 @@ +package ova + +import ( + "path" + "strings" + + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/ova" + "github.com/konveyor/forklift-controller/pkg/controller/watch/handler" + liberr "github.com/konveyor/forklift-controller/pkg/lib/error" + libweb "github.com/konveyor/forklift-controller/pkg/lib/inventory/web" + "github.com/konveyor/forklift-controller/pkg/lib/logging" + "golang.org/x/net/context" + "sigs.k8s.io/controller-runtime/pkg/event" +) + +// Package logger. +var log = logging.WithName("plan|ova") + +// Provider watch event handler. +type Handler struct { + *handler.Handler +} + +// Ensure watch on VMs. +func (r *Handler) Watch(watch *handler.WatchManager) (err error) { + w, err := watch.Ensure( + r.Provider(), + &ova.VM{}, + r) + if err != nil { + return + } + + log.Info( + "Inventory watch ensured.", + "provider", + path.Join( + r.Provider().Namespace, + r.Provider().Name), + "watch", + w.ID()) + + return +} + +// Resource created. +func (r *Handler) Created(e libweb.Event) { + if vm, cast := e.Resource.(*ova.VM); cast { + r.changed(vm) + } +} + +// Resource created. +func (r *Handler) Updated(e libweb.Event) { + if vm, cast := e.Resource.(*ova.VM); cast { + updated := e.Updated.(*ova.VM) + if updated.Path != vm.Path { + r.changed(vm, updated) + } + } +} + +// Resource deleted. +func (r *Handler) Deleted(e libweb.Event) { + if vm, cast := e.Resource.(*ova.VM); cast { + r.changed(vm) + } +} + +// VM changed. +// Find all of the Plan CRs the reference both the +// provider and the changed VM and enqueue reconcile events. +func (r *Handler) changed(models ...*ova.VM) { + log.V(3).Info( + "VM changed.", + "id", + models[0].ID) + list := api.PlanList{} + err := r.List(context.TODO(), &list) + if err != nil { + err = liberr.Wrap(err) + return + } + for i := range list.Items { + plan := &list.Items[i] + ref := plan.Spec.Provider.Source + if plan.Spec.Archived || !r.MatchProvider(ref) { + continue + } + referenced := false + for _, planVM := range plan.Spec.VMs { + ref := planVM.Ref + for _, vm := range models { + if ref.ID == vm.ID || strings.HasSuffix(vm.Path, ref.Name) { + referenced = true + break + } + } + if referenced { + break + } + } + if referenced { + log.V(3).Info( + "Queue reconcile event.", + "plan", + path.Join( + plan.Namespace, + plan.Name)) + r.Enqueue(event.GenericEvent{ + Object: plan, + }) + } + } +} diff --git a/pkg/controller/provider/BUILD.bazel b/pkg/controller/provider/BUILD.bazel index 174c9f1d6..348974097 100644 --- a/pkg/controller/provider/BUILD.bazel +++ b/pkg/controller/provider/BUILD.bazel @@ -27,8 +27,11 @@ go_library( "//pkg/lib/logging", "//pkg/lib/ref", "//pkg/settings", + "//vendor/k8s.io/api/apps/v1:apps", "//vendor/k8s.io/api/core/v1:core", "//vendor/k8s.io/apimachinery/pkg/api/errors", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:meta", + "//vendor/k8s.io/apimachinery/pkg/util/intstr", "//vendor/k8s.io/apiserver/pkg/storage/names", "//vendor/sigs.k8s.io/controller-runtime/pkg/client", "//vendor/sigs.k8s.io/controller-runtime/pkg/controller", diff --git a/pkg/controller/provider/container/BUILD.bazel b/pkg/controller/provider/container/BUILD.bazel index 7a15d6470..c612bdefa 100644 --- a/pkg/controller/provider/container/BUILD.bazel +++ b/pkg/controller/provider/container/BUILD.bazel @@ -9,6 +9,7 @@ go_library( "//pkg/apis/forklift/v1beta1", "//pkg/controller/provider/container/ocp", "//pkg/controller/provider/container/openstack", + "//pkg/controller/provider/container/ova", "//pkg/controller/provider/container/ovirt", "//pkg/controller/provider/container/vsphere", "//pkg/lib/inventory/container", diff --git a/pkg/controller/provider/container/doc.go b/pkg/controller/provider/container/doc.go index 3f3c987e5..7e3df8347 100644 --- a/pkg/controller/provider/container/doc.go +++ b/pkg/controller/provider/container/doc.go @@ -4,6 +4,7 @@ import ( api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" "github.com/konveyor/forklift-controller/pkg/controller/provider/container/ocp" "github.com/konveyor/forklift-controller/pkg/controller/provider/container/openstack" + "github.com/konveyor/forklift-controller/pkg/controller/provider/container/ova" "github.com/konveyor/forklift-controller/pkg/controller/provider/container/ovirt" "github.com/konveyor/forklift-controller/pkg/controller/provider/container/vsphere" libcontainer "github.com/konveyor/forklift-controller/pkg/lib/inventory/container" @@ -26,6 +27,8 @@ func Build( return ovirt.New(db, provider, secret) case api.OpenStack: return openstack.New(db, provider, secret) + case api.Ova: + return ova.New(db, provider, secret) } return nil diff --git a/pkg/controller/provider/container/ova/BUILD.bazel b/pkg/controller/provider/container/ova/BUILD.bazel new file mode 100644 index 000000000..0f00fdcaa --- /dev/null +++ b/pkg/controller/provider/container/ova/BUILD.bazel @@ -0,0 +1,31 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "ova", + srcs = [ + "client.go", + "collector.go", + "doc.go", + "model.go", + "resource.go", + "watch.go", + ], + importpath = "github.com/konveyor/forklift-controller/pkg/controller/provider/container/ova", + visibility = ["//visibility:public"], + deps = [ + "//pkg/apis/forklift/v1beta1", + "//pkg/apis/forklift/v1beta1/ref", + "//pkg/controller/provider/model/ova", + "//pkg/controller/provider/web/ova", + "//pkg/controller/validation/policy", + "//pkg/lib/error", + "//pkg/lib/filebacked", + "//pkg/lib/inventory/model", + "//pkg/lib/inventory/web", + "//pkg/lib/logging", + "//pkg/settings", + "//vendor/github.com/go-logr/logr", + "//vendor/k8s.io/api/core/v1:core", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:meta", + ], +) diff --git a/pkg/controller/provider/container/ova/client.go b/pkg/controller/provider/container/ova/client.go new file mode 100644 index 000000000..bb136a4e9 --- /dev/null +++ b/pkg/controller/provider/container/ova/client.go @@ -0,0 +1,118 @@ +package ova + +import ( + "fmt" + "net" + "net/http" + liburl "net/url" + "time" + + "github.com/go-logr/logr" + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + liberr "github.com/konveyor/forklift-controller/pkg/lib/error" + libweb "github.com/konveyor/forklift-controller/pkg/lib/inventory/web" + core "k8s.io/api/core/v1" +) + +// Not found error. +type NotFound struct { +} + +func (e *NotFound) Error() string { + return "not found." +} + +// Client. +type Client struct { + URL string + client *libweb.Client + Secret *core.Secret + Log logr.Logger + serviceURL string +} + +// Connect. +func (r *Client) Connect(provider *api.Provider) (err error) { + + if r.client != nil { + return + } + + client := &libweb.Client{ + Transport: &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 15 * time.Second, + KeepAlive: 15 * time.Second, + }).DialContext, + MaxIdleConns: 10, + }, + } + + serverURL := fmt.Sprintf("http://ova-service-%s:8080", provider.Name) + if serverURL == "" { + return + } + + url := serverURL + "/test_connection" + res := "" + status, err := client.Get(url, &res) + if err != nil { + return + } + if status != http.StatusOK { + err = liberr.New(http.StatusText(status)) + return + } + + r.client = client + r.serviceURL = serverURL + return +} + +// List collection. +func (r *Client) list(path string, list interface{}) (err error) { + url, err := liburl.Parse(r.serviceURL) + if err != nil { + err = liberr.Wrap(err) + return + } + url.Path += "/" + path + status, err := r.client.Get(url.String(), list) + if err != nil { + return + } + if status != http.StatusOK { + err = liberr.New(http.StatusText(status)) + return + } + + return +} + +// Get a resource. +func (r *Client) get(path string, object interface{}) (err error) { + url, err := liburl.Parse(r.serviceURL) + if err != nil { + err = liberr.Wrap(err) + return + } + url.Path = path + defer func() { + if err != nil { + err = liberr.Wrap(err, "url", url.String()) + } + }() + status, err := r.client.Get(url.String(), object) + if err != nil { + return + } + switch status { + case http.StatusOK: + case http.StatusNotFound: + err = &NotFound{} + default: + err = liberr.New(http.StatusText(status)) + } + + return +} diff --git a/pkg/controller/provider/container/ova/collector.go b/pkg/controller/provider/container/ova/collector.go new file mode 100644 index 000000000..661cd1d50 --- /dev/null +++ b/pkg/controller/provider/container/ova/collector.go @@ -0,0 +1,346 @@ +package ova + +import ( + "context" + liburl "net/url" + libpath "path" + "time" + + "github.com/go-logr/logr" + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" + liberr "github.com/konveyor/forklift-controller/pkg/lib/error" + libmodel "github.com/konveyor/forklift-controller/pkg/lib/inventory/model" + "github.com/konveyor/forklift-controller/pkg/lib/logging" + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Settings +const ( + // Retry interval. + RetryInterval = 5 * time.Second + // Refresh interval. + RefreshInterval = 10 * time.Second +) + +// Phases +const ( + Started = "" + Load = "load" + Loaded = "loaded" + Parity = "parity" + Refresh = "refresh" +) + +// OVA data collector. +type Collector struct { + // Provider + provider *api.Provider + // DB client. + db libmodel.DB + // Logger. + log logr.Logger + // has parity. + parity bool + // REST client. + client *Client + // cancel function. + cancel func() + // Start Time + startTime time.Time + // Phase + phase string + // List of watches. + watches []*libmodel.Watch +} + +// New collector. +func New(db libmodel.DB, provider *api.Provider, secret *core.Secret) (r *Collector) { + log := logging.WithName("collector|ova").WithValues( + "provider", + libpath.Join( + provider.GetNamespace(), + provider.GetName())) + clientLog := logging.WithName("client|ova").WithValues( + "provider", + libpath.Join( + provider.GetNamespace(), + provider.GetName())) + + r = &Collector{ + client: &Client{ + URL: provider.Spec.URL, + Secret: secret, + Log: clientLog, + }, + provider: provider, + db: db, + log: log, + } + + return +} + +// The name. +func (r *Collector) Name() string { + url, err := liburl.Parse(r.client.URL) + if err == nil { + return url.Host + } + + return r.client.URL +} + +// The owner. +func (r *Collector) Owner() meta.Object { + return r.provider +} + +// Get the DB. +func (r *Collector) DB() libmodel.DB { + return r.db +} + +// Reset. +func (r *Collector) Reset() { + r.parity = false +} + +// Reset. +func (r *Collector) HasParity() bool { + return r.parity +} + +// Test connect/logout. +func (r *Collector) Test() (_ int, err error) { + err = r.client.Connect(r.provider) + return +} + +// Start the collector. +func (r *Collector) Start() error { + ctx := Context{ + client: r.client, + db: r.db, + log: r.log, + } + ctx.ctx, r.cancel = context.WithCancel(context.Background()) + start := func() { + defer func() { + r.endWatch() + r.log.Info("Stopped.") + }() + for { + if !ctx.canceled() { + _ = r.run(&ctx) + } else { + return + } + } + } + + go start() + + return nil +} + +// Run the current phase. +func (r *Collector) run(ctx *Context) (err error) { + r.log.V(3).Info( + "Running.", + "phase", + r.phase) + switch r.phase { + case Started: + err = r.client.Connect(r.provider) + if err != nil { + return + } + r.startTime = time.Now() + r.phase = Load + case Load: + err = r.load(ctx) + if err == nil { + r.phase = Loaded + } + case Loaded: + err = r.refresh(ctx) + if err == nil { + r.phase = Parity + } + case Parity: + r.endWatch() + err = r.beginWatch() + if err == nil { + r.phase = Refresh + r.parity = true + } + case Refresh: + err = r.refresh(ctx) + if err == nil { + r.parity = true + time.Sleep(RefreshInterval) + } else { + r.parity = false + } + default: + err = liberr.New("Phase unknown.") + } + if err != nil { + r.log.Error( + err, + "Failed.", + "phase", + r.phase) + time.Sleep(RetryInterval) + } + + return +} + +// Shutdown the collector. +func (r *Collector) Shutdown() { + r.log.Info("Shutdown.") + if r.cancel != nil { + r.cancel() + } +} + +// Load the inventory. +func (r *Collector) load(ctx *Context) (err error) { + mark := time.Now() + for _, adapter := range adapterList { + if ctx.canceled() { + return + } + err = r.create(ctx, adapter) + if err != nil { + return + } + } + r.log.Info( + "Initial Parity.", + "duration", + time.Since(mark)) + + return +} + +// List and create resources using the adapter. +func (r *Collector) create(ctx *Context, adapter Adapter) (err error) { + itr, aErr := adapter.List(ctx) + + if aErr != nil { + err = aErr + return + } + tx, err := r.db.Begin() + if err != nil { + return + } + defer func() { + _ = tx.End() + }() + for { + object, hasNext := itr.Next() + if !hasNext { + break + } + if ctx.canceled() { + return + } + m := object.(libmodel.Model) + err = tx.Insert(m) + if err != nil { + return + } + } + err = tx.Commit() + if err != nil { + return + } + + return +} + +// Add model watches. +func (r *Collector) beginWatch() (err error) { + defer func() { + if err != nil { + r.endWatch() + } + }() + // Cluster + w, err := r.db.Watch( + &model.VM{}, + &VMEventHandler{ + Provider: r.provider, + DB: r.db, + log: r.log, + }) + + if err == nil { + r.watches = append(r.watches, w) + } else { + return + } + return +} + +// End watches. +func (r *Collector) endWatch() { + for _, watch := range r.watches { + watch.End() + } +} + +// Refresh the inventory. +// - List modified vms. +// - Build the changeSet. +// - Apply the changeSet. +// +// The two-phased approach ensures we do not hold the +// DB transaction while using the provider API which +// can block or be slow. +func (r *Collector) refresh(ctx *Context) (err error) { + var updates []Updater + mark := time.Now() + for _, adapter := range adapterList { + if ctx.canceled() { + return + } + updates, err = adapter.GetUpdates(ctx) + if err != nil { + return + } + err = r.apply(updates) + if err != nil { + return + } + } + r.log.Info( + "Refresh finished.", + "duration", + time.Since(mark)) + return +} + +// Apply the changeSet. +func (r *Collector) apply(changeSet []Updater) (err error) { + tx, err := r.db.Begin() + if err != nil { + return + } + defer func() { + _ = tx.End() + }() + for _, updater := range changeSet { + err = updater(tx) + if err != nil { + return + } + } + err = tx.Commit() + return +} diff --git a/pkg/controller/provider/container/ova/doc.go b/pkg/controller/provider/container/ova/doc.go new file mode 100644 index 000000000..f20de7637 --- /dev/null +++ b/pkg/controller/provider/container/ova/doc.go @@ -0,0 +1 @@ +package ova diff --git a/pkg/controller/provider/container/ova/model.go b/pkg/controller/provider/container/ova/model.go new file mode 100644 index 000000000..ad27c62ae --- /dev/null +++ b/pkg/controller/provider/container/ova/model.go @@ -0,0 +1,230 @@ +package ova + +import ( + "context" + "errors" + + "github.com/go-logr/logr" + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" + fb "github.com/konveyor/forklift-controller/pkg/lib/filebacked" + libmodel "github.com/konveyor/forklift-controller/pkg/lib/inventory/model" +) + +// All adapters. +var adapterList []Adapter + +// Event (type) mapped to adapter. +var adapterMap = map[int][]Adapter{} + +func init() { + adapterList = []Adapter{ + &NetworkAdapter{}, + &DiskAdapter{}, + &VMAdapter{}, + } +} + +// Updates the DB based on +// changes described by an Event. +type Updater func(tx *libmodel.Tx) error + +// Adapter context. +type Context struct { + // Context. + ctx context.Context + // OVA client. + client *Client + // Log. + log logr.Logger + // DB client. + db libmodel.DB +} + +// The adapter request is canceled. +func (r *Context) canceled() (done bool) { + select { + case <-r.ctx.Done(): + done = true + default: + } + + return +} + +// Model adapter. +// Provides integration between the REST resource +// model and the inventory model. +type Adapter interface { + // List REST collections. + List(ctx *Context) (itr fb.Iterator, err error) + // Get object updates + GetUpdates(ctx *Context) (updater []Updater, err error) +} + +// Base adapter. +type BaseAdapter struct { +} + +// Network adapter. +type NetworkAdapter struct { + BaseAdapter +} + +// List the collection. +func (r *NetworkAdapter) List(ctx *Context) (itr fb.Iterator, err error) { + networkList := []Network{} + err = ctx.client.list("networks", &networkList) + if err != nil { + return + } + list := fb.NewList() + for _, object := range networkList { + m := &model.Network{ + Base: model.Base{Name: object.Name}, + } + object.ApplyTo(m) + list.Append(m) + } + + itr = list.Iter() + + return +} + +func (r *NetworkAdapter) GetUpdates(ctx *Context) (updates []Updater, err error) { + networkList := []Network{} + err = ctx.client.list("networks", &networkList) + if err != nil { + return + } + for i := range networkList { + network := &networkList[i] + updater := func(tx *libmodel.Tx) (err error) { + m := &model.Network{ + Base: model.Base{Name: network.Name}, + } + err = tx.Get(m) + if err != nil { + if errors.Is(err, libmodel.NotFound) { + network.ApplyTo(m) + err = tx.Insert(m) + } + return + } + network.ApplyTo(m) + err = tx.Update(m) + return + } + updates = append(updates, updater) + } + return +} + +// VM adapter. +type VMAdapter struct { + BaseAdapter +} + +// List the collection. +func (r *VMAdapter) List(ctx *Context) (itr fb.Iterator, err error) { + vmList := []VM{} + err = ctx.client.list("vms", &vmList) + if err != nil { + return + } + list := fb.NewList() + for _, object := range vmList { + m := &model.VM{ + Base: model.Base{ID: object.UUID}, + } + object.ApplyTo(m) + list.Append(m) + } + + itr = list.Iter() + return +} + +// Get updates since last sync. +func (r *VMAdapter) GetUpdates(ctx *Context) (updates []Updater, err error) { + vmList := []VM{} + err = ctx.client.list("vms", &vmList) + if err != nil { + return + } + for i := range vmList { + vm := &vmList[i] + updater := func(tx *libmodel.Tx) (err error) { + m := &model.VM{ + Base: model.Base{ID: vm.UUID}, + } + if err = tx.Get(m); err != nil { + if errors.Is(err, libmodel.NotFound) { + vm.ApplyTo(m) + err = tx.Insert(m) + } + } else if vm.OvaPath != m.OvaPath { + vm.ApplyTo(m) + err = tx.Update(m) + } + return + } + updates = append(updates, updater) + } + return +} + +// Disk adapter. +type DiskAdapter struct { + BaseAdapter +} + +// List the collection. +func (r *DiskAdapter) List(ctx *Context) (itr fb.Iterator, err error) { + diskList := []Disk{} + err = ctx.client.list("disks", &diskList) + if err != nil { + return + } + list := fb.NewList() + for _, object := range diskList { + m := &model.Disk{ + Base: model.Base{ID: object.DiskId}, + } + object.ApplyTo(m) + list.Append(m) + } + + itr = list.Iter() + + return +} + +func (r *DiskAdapter) GetUpdates(ctx *Context) (updates []Updater, err error) { + diskList := []Disk{} + err = ctx.client.list("disks", &diskList) + if err != nil { + return + } + for i := range diskList { + disk := &diskList[i] + updater := func(tx *libmodel.Tx) (err error) { + m := &model.Disk{ + Base: model.Base{ID: disk.DiskId}, + } + err = tx.Get(m) + if err != nil { + if errors.Is(err, libmodel.NotFound) { + disk.ApplyTo(m) + err = tx.Insert(m) + } + return + } + disk.ApplyTo(m) + err = tx.Update(m) + return + } + updates = append(updates, updater) + } + return +} diff --git a/pkg/controller/provider/container/ova/resource.go b/pkg/controller/provider/container/ova/resource.go new file mode 100644 index 000000000..133715f56 --- /dev/null +++ b/pkg/controller/provider/container/ova/resource.go @@ -0,0 +1,204 @@ +package ova + +import ( + "strconv" + + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" +) + +type Base struct { + ID string `json:"ID"` + Name string `json:"Name"` + Description string `json:"Description"` +} + +func (b *Base) bool(s string) (v bool) { + v, _ = strconv.ParseBool(s) + return +} + +func (b *Base) int32(s string) (v int32) { + n, _ := strconv.ParseInt(s, 10, 32) + v = int32(n) + return +} + +func (b *Base) int64(s string) (v int64) { + v, _ = strconv.ParseInt(s, 10, 64) + return +} + +// VM. +type VM struct { + Name string `json:"Name"` + OvaPath string `json:"OvaPath"` + RevisionValidated int64 `json:"RevisionValidated"` + PolicyVersion int `json:"PolicyVersion"` + UUID string `json:"UUID"` + Firmware string `json:"Firmware"` + CpuAffinity []int32 `json:"CpuAffinity"` + CpuHotAddEnabled bool `json:"CpuHotAddEnabled"` + CpuHotRemoveEnabled bool `json:"CpuHotRemoveEnabled"` + MemoryHotAddEnabled bool `json:"MemoryHotAddEnabled"` + FaultToleranceEnabled bool `json:"FaultToleranceEnabled"` + CpuCount int32 `json:"CpuCount"` + CoresPerSocket int32 `json:"CoresPerSocket"` + MemoryMB int32 `json:"MemoryMB"` + BalloonedMemory int32 `json:"BalloonedMemory"` + IpAddress string `json:"IpAddress"` + NumaNodeAffinity []string `json:"NumaNodeAffinity"` + StorageUsed int64 `json:"StorageUsed"` + ChangeTrackingEnabled bool `json:"ChangeTrackingEnabled"` + Devices []struct { + Kind string `json:"Kind"` + } `json:"Devices"` + NICs []struct { + Name string `json:"Name"` + MAC string `json:"MAC"` + Config []struct { + Key string `json:"Key"` + Value string `json:"Value"` + } `json:"Config"` + } `json:"Nics"` + Disks []struct { + FilePath string `json:"FilePath"` + Capacity string `json:"Capacity"` + CapacityAllocationUnits string `json:"CapacityAllocationUnits"` + DiskId string `json:"DiskId"` + FileRef string `json:"FileRef"` + Format string `json:"Format"` + PopulatedSize string `json:"PopulatedSize"` + } `json:"Disks"` + Networks []struct { + Name string `json:"Name"` + Description string `json:"Description"` + } `json:"Networks"` +} + +// Apply to (update) the model. +func (r *VM) ApplyTo(m *model.VM) { + m.Name = r.Name + m.OvaPath = r.OvaPath + m.RevisionValidated = r.RevisionValidated + m.PolicyVersion = r.PolicyVersion + m.UUID = r.UUID + m.Firmware = r.Firmware + m.CpuAffinity = r.CpuAffinity + m.CpuHotAddEnabled = r.CpuHotAddEnabled + m.CpuHotRemoveEnabled = r.CpuHotRemoveEnabled + m.MemoryHotAddEnabled = r.MemoryHotAddEnabled + m.FaultToleranceEnabled = r.FaultToleranceEnabled + m.CpuCount = r.CpuCount + m.CoresPerSocket = r.CoresPerSocket + m.MemoryMB = r.MemoryMB + m.BalloonedMemory = r.BalloonedMemory + m.IpAddress = r.IpAddress + m.NumaNodeAffinity = r.NumaNodeAffinity + m.StorageUsed = r.StorageUsed + m.ChangeTrackingEnabled = r.ChangeTrackingEnabled + r.addNICs(m) + r.addDisks(m) + r.addDevices(m) + r.addNetworks(m) +} + +func (r *VM) addNICs(m *model.VM) { + m.NICs = []model.NIC{} + for _, n := range r.NICs { + configs := []model.Conf{} + for _, conf := range n.Config { + configs = append( + configs, + model.Conf{ + Key: conf.Key, + Value: conf.Value, + }) + } + m.NICs = append( + m.NICs, model.NIC{ + Name: n.Name, + MAC: n.MAC, + Config: configs, + }) + } +} + +func (r *VM) addDisks(m *model.VM) { + m.Disks = []model.Disk{} + for _, disk := range r.Disks { + m.Disks = append( + m.Disks, + model.Disk{ + FilePath: disk.FilePath, + Capacity: disk.Capacity, + CapacityAllocationUnits: disk.CapacityAllocationUnits, + DiskId: disk.DiskId, + FileRef: disk.FileRef, + Format: disk.Format, + PopulatedSize: disk.PopulatedSize, + }) + } +} + +func (r *VM) addDevices(m *model.VM) { + m.Devices = []model.Device{} + for _, device := range r.Devices { + m.Devices = append( + m.Devices, + model.Device{ + Kind: device.Kind, + }) + } +} + +func (r *VM) addNetworks(m *model.VM) { + m.Networks = []model.Network{} + for _, network := range r.Networks { + m.Networks = append( + m.Networks, + model.Network{ + Description: network.Description, + }) + } +} + +// Network. +type Network struct { + Name string `json:"Name"` + Description string `json:"Description"` +} + +// Apply to (update) the model. +func (r *Network) ApplyTo(m *model.Network) { + m.Description = r.Description +} + +// Network (list). +//type NetworkList []Network `json:"network"` + +// Disk. +type Disk struct { + FilePath string `json:"FilePath"` + Capacity string `json:"Capacity"` + CapacityAllocationUnits string `json:"Capacity_allocation_units"` + DiskId string `json:"DiskId"` + FileRef string `json:"FileRef"` + Format string `json:"Format"` + PopulatedSize string `json:"PopulatedSize"` +} + +// Apply to (update) the model. +func (r *Disk) ApplyTo(m *model.Disk) { + m.FilePath = r.FilePath + m.Capacity = r.Capacity + m.CapacityAllocationUnits = r.CapacityAllocationUnits + m.DiskId = r.DiskId + m.FileRef = r.FileRef + m.Format = r.Format + m.PopulatedSize = r.PopulatedSize +} + +// Disk (list). +type DiskList struct { + Items []Disk `json:"Disk"` +} diff --git a/pkg/controller/provider/container/ova/watch.go b/pkg/controller/provider/container/ova/watch.go new file mode 100644 index 000000000..69db24d10 --- /dev/null +++ b/pkg/controller/provider/container/ova/watch.go @@ -0,0 +1,344 @@ +package ova + +import ( + "context" + "errors" + "time" + + "github.com/go-logr/logr" + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + refapi "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1/ref" + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" + web "github.com/konveyor/forklift-controller/pkg/controller/provider/web/ova" + "github.com/konveyor/forklift-controller/pkg/controller/validation/policy" + liberr "github.com/konveyor/forklift-controller/pkg/lib/error" + libmodel "github.com/konveyor/forklift-controller/pkg/lib/inventory/model" + "github.com/konveyor/forklift-controller/pkg/settings" +) + +const ( + // The (max) number of batched task results. + MaxBatch = 1024 + // Transaction label. + ValidationLabel = "VM-validated" +) + +// Endpoints. +const ( + BaseEndpoint = "/v1/data/io/konveyor/forklift/ova/" + VersionEndpoint = BaseEndpoint + "rules_version" + ValidationEndpoint = BaseEndpoint + "validate" +) + +// Application settings. +var Settings = &settings.Settings + +// Watch for VM changes and validate as needed. +type VMEventHandler struct { + libmodel.StockEventHandler + // Provider. + Provider *api.Provider + // DB. + DB libmodel.DB + // Validation event latch. + latch chan int8 + // Last search. + lastSearch time.Time + // Logger. + log logr.Logger + // Context + context context.Context + // Context cancel. + cancel context.CancelFunc + // Task result + taskResult chan *policy.Task +} + +// Reset. +func (r *VMEventHandler) reset() { + r.lastSearch = time.Now() +} + +// Watch ended. +func (r *VMEventHandler) Started(uint64) { + r.log.Info("Started.") + r.taskResult = make(chan *policy.Task) + r.latch = make(chan int8, 1) + r.context, r.cancel = context.WithCancel(context.Background()) + go r.run() + go r.harvest() +} + +// VM Created. +// The VM is scheduled (and reported as scheduled). +// This is best-effort. If the validate() fails, it wil be +// picked up in the next search(). +func (r *VMEventHandler) Created(event libmodel.Event) { + if r.canceled() { + return + } + if VM, cast := event.Model.(*model.VM); cast { + if !VM.Validated() { + r.tripLatch() + } + } +} + +// VM Updated. +// The VM is scheduled (and reported as scheduled). +// This is best-effort. If the validate() fails, it wil be +// picked up in the next search(). +func (r *VMEventHandler) Updated(event libmodel.Event) { + if r.canceled() { + return + } + if event.HasLabel(ValidationLabel) { + return + } + if VM, cast := event.Updated.(*model.VM); cast { + if !VM.Validated() { + r.tripLatch() + } + } +} + +// Report errors. +func (r *VMEventHandler) Error(err error) { + r.log.Error(liberr.Wrap(err), err.Error()) +} + +// Watch ended. +func (r *VMEventHandler) End() { + r.log.Info("Ended.") + r.cancel() + close(r.latch) + close(r.taskResult) +} + +// Trip the validation event latch. +func (r *VMEventHandler) tripLatch() { + defer func() { + _ = recover() + }() + select { + case r.latch <- 1: + // trip. + default: + // tripped. + } +} + +// Run. +// Periodically search for VMs that need to be validated. +func (r *VMEventHandler) run() { + r.log.Info("Run started.") + defer r.log.Info("Run stopped.") + interval := time.Second * time.Duration( + Settings.PolicyAgent.SearchInterval) + r.list() + r.reset() + for { + select { + case <-time.After(interval): + r.list() + r.reset() + case _, open := <-r.latch: + if open { + r.list() + r.reset() + } else { + return + } + } + } +} + +// Harvest validation task results and update VMs. +// Collect completed tasks in batches. Apply the batch +// to VMs when one of: +// - The batch is full. +// - No tasks have been received within +// the delay period. +func (r *VMEventHandler) harvest() { + r.log.Info("Harvest started.") + defer r.log.Info("Harvest stopped.") + long := time.Hour + short := time.Second + delay := long + batch := []*policy.Task{} + mark := time.Now() + for { + select { + case <-time.After(delay): + case task, open := <-r.taskResult: + if open { + batch = append(batch, task) + delay = short + } else { + return + } + } + if time.Since(mark) > delay || len(batch) > MaxBatch { + r.validated(batch) + batch = []*policy.Task{} + delay = long + mark = time.Now() + } + } +} + +// List for VMs to be validated. +// VMs that have been reported through the model event +// watch are ignored. +func (r *VMEventHandler) list() { + r.log.V(3).Info("List VMs that need to be validated.") + version, err := policy.Agent.Version(VersionEndpoint) + if err != nil { + r.log.Error(err, err.Error()) + return + } + if r.canceled() { + return + } + itr, err := r.DB.Find( + &model.VM{}, + libmodel.ListOptions{ + Predicate: libmodel.Or( + libmodel.Neq("Revision", libmodel.Field{Name: "RevisionValidated"}), + libmodel.Neq("PolicyVersion", version)), + }) + if err != nil { + r.log.Error(err, "List VM failed.") + return + } + if itr.Len() > 0 { + r.log.V(3).Info( + "List (unvalidated) VMs found.", + "count", + itr.Len()) + } + for { + VM := &model.VM{} + hasNext := itr.NextWith(VM) + if !hasNext || r.canceled() { + break + } + _ = r.validate(VM) + } +} + +// Handler canceled. +func (r *VMEventHandler) canceled() bool { + select { + case <-r.context.Done(): + return true + default: + return false + } +} + +// Analyze the VM. +func (r *VMEventHandler) validate(VM *model.VM) (err error) { + task := &policy.Task{ + Path: ValidationEndpoint, + Context: r.context, + // Workload: r.workload, + Result: r.taskResult, + Revision: VM.Revision, + Ref: refapi.Ref{ + ID: VM.ID, + }, + Workload: r.workload, + } + r.log.V(4).Info( + "Validate VM.", + "VMID", + VM.ID) + err = policy.Agent.Submit(task) + if err != nil { + r.log.Error(err, "VM task (submit) failed.") + } + + return +} + +// VMs validated. +func (r *VMEventHandler) validated(batch []*policy.Task) { + if len(batch) == 0 { + return + } + r.log.V(3).Info( + "VM (batch) completed.", + "count", + len(batch)) + tx, err := r.DB.Begin(ValidationLabel) + if err != nil { + r.log.Error(err, "Begin tx failed.") + return + } + defer func() { + _ = tx.End() + }() + for _, task := range batch { + if task.Error != nil { + r.log.Error( + task.Error, "VM validation failed.") + continue + } + latest := &model.VM{Base: model.Base{ID: task.Ref.ID}} + err = tx.Get(latest) + if err != nil { + r.log.Error(err, "VM (get) failed.") + continue + } + if task.Revision != latest.Revision { + continue + } + latest.PolicyVersion = task.Version + latest.RevisionValidated = latest.Revision + latest.Concerns = task.Concerns + latest.Revision-- + err = tx.Update(latest, libmodel.Eq("Revision", task.Revision)) + if errors.Is(err, model.NotFound) { + continue + } + if err != nil { + r.log.Error(err, "VM update failed.") + continue + } + r.log.V(3).Info( + "VM validated.", + "ID", + latest.ID, + "revision", + latest.Revision, + "duration", + task.Duration()) + } + err = tx.Commit() + if err != nil { + r.log.Error(err, "Tx commit failed.") + return + } +} + +// Build the workload. +func (r *VMEventHandler) workload(vmID string) (object interface{}, err error) { + vm := &model.VM{ + Base: model.Base{ID: vmID}, + } + err = r.DB.Get(vm) + if err != nil { + return + } + workload := web.Workload{} + workload.With(vm) + if err != nil { + return + } + + workload.Link(r.Provider) + object = workload + + return +} diff --git a/pkg/controller/provider/controller.go b/pkg/controller/provider/controller.go index 1300999dd..8f1932a12 100644 --- a/pkg/controller/provider/controller.go +++ b/pkg/controller/provider/controller.go @@ -18,8 +18,10 @@ package provider import ( "context" + "fmt" "os" "path/filepath" + "strings" "sync" api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" @@ -37,8 +39,11 @@ import ( "github.com/konveyor/forklift-controller/pkg/lib/logging" libref "github.com/konveyor/forklift-controller/pkg/lib/ref" "github.com/konveyor/forklift-controller/pkg/settings" - core "k8s.io/api/core/v1" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" k8serr "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apiserver/pkg/storage/names" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -59,6 +64,13 @@ var log = logging.WithName(Name) // Application settings. var Settings = &settings.Settings +const ( + ovaServerPrefix = "ova-server" + ovaImageVar = "OVA_PROVIDER_SERVER_IMAGE" + nfsVolumeNamePrefix = "nfs-volume" + mountPath = "/ova" +) + // Creates a new Inventory Controller and adds it to the Manager. func Add(mgr manager.Manager) error { libfb.WorkingDir = Settings.WorkingDir @@ -107,7 +119,7 @@ func Add(mgr manager.Manager) error { // References. err = cnt.Watch( &source.Kind{ - Type: &core.Secret{}, + Type: &v1.Secret{}, }, libref.Handler(&api.Provider{})) if err != nil { @@ -161,6 +173,7 @@ func (r Reconciler) Reconcile(ctx context.Context, request reconcile.Request) (r return } else { r.catalog.add(request, provider) + } defer func() { @@ -184,6 +197,24 @@ func (r Reconciler) Reconcile(ctx context.Context, request reconcile.Request) (r } } + if provider.Type() == api.Ova { + + deploymentName := fmt.Sprintf("%s-deployment-%s", ovaServerPrefix, provider.Name) + + deployment := &appsv1.Deployment{} + err = r.Get(context.TODO(), client.ObjectKey{ + Namespace: provider.Namespace, + Name: deploymentName}, + deployment) + + // If the deployment does not exist + if k8serr.IsNotFound(err) { + r.createOVAServerDeployment(provider, ctx) + } else if err != nil { + return + } + } + // Begin staging conditions. provider.Status.Phase = Staging provider.Status.BeginStagingConditions() @@ -284,6 +315,7 @@ func (r *Reconciler) updateContainer(provider *api.Provider) (err error) { if err != nil { return } + collector := container.Build(db, provider, secret) err = r.container.Add(collector) if err != nil { @@ -316,8 +348,8 @@ func (r *Reconciler) getDB(provider *api.Provider) (db libmodel.DB) { } // Get the secret referenced by the provider. -func (r *Reconciler) getSecret(provider *api.Provider) (*core.Secret, error) { - secret := &core.Secret{} +func (r *Reconciler) getSecret(provider *api.Provider) (*v1.Secret, error) { + secret := &v1.Secret{} if provider.IsHost() { return secret, nil } @@ -334,6 +366,141 @@ func (r *Reconciler) getSecret(provider *api.Provider) (*core.Secret, error) { return secret, nil } +func (r *Reconciler) createOVAServerDeployment(provider *api.Provider, ctx context.Context) { + + deploymentName := fmt.Sprintf("%s-deployment-%s", ovaServerPrefix, provider.Name) + annotations := make(map[string]string) + labels := map[string]string{"providerName": provider.Name, "app": "forklift"} + url := provider.Spec.URL + var replicas int32 = 1 + + ownerReference := metav1.OwnerReference{ + APIVersion: "forklift.konveyor.io/v1beta1", + Kind: "Provider", + Name: provider.Name, + UID: provider.UID, + } + + //OVA server deployment + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: deploymentName, + Namespace: provider.Namespace, + Annotations: annotations, + Labels: labels, + OwnerReferences: []metav1.OwnerReference{ownerReference}, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "forklift", + }, + }, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "providerName": provider.Name, + "app": "forklift", + }, + }, + Spec: r.makeOvaProviderPodSpec(url, string(provider.Name)), + }, + }, + } + + err := r.Create(ctx, deployment) + if err != nil { + r.Log.Error(err, "Failed to create OVA server deployment") + return + } + + // OVA Server Service + serviceName := fmt.Sprintf("ova-service-%s", provider.Name) + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName, + Namespace: provider.Namespace, + Labels: labels, + OwnerReferences: []metav1.OwnerReference{ownerReference}, + }, + Spec: v1.ServiceSpec{ + Selector: map[string]string{ + "providerName": provider.Name, + "app": "forklift", + }, + Ports: []v1.ServicePort{ + { + Name: "api-http", + Protocol: v1.ProtocolTCP, + Port: 8080, + TargetPort: intstr.FromInt(8080), + }, + }, + Type: v1.ServiceTypeClusterIP, + }, + } + + err = r.Create(ctx, service) + if err != nil { + r.Log.Error(err, "Failed to create OVA server service") + return + } +} + +func (r *Reconciler) makeOvaProviderPodSpec(url string, providerName string) v1.PodSpec { + splitted := strings.Split(url, ":") + nonRoot := false + + if len(splitted) != 2 { + r.Log.Error(nil, "NFS server path doesn't contains :") + } + nfsServer := splitted[0] + nfsPath := splitted[1] + + imageName, ok := os.LookupEnv(ovaImageVar) + if !ok { + r.Log.Error(nil, "Failed to find OVA server image") + } + + nfsVolumeName := fmt.Sprintf("%s-%s", nfsVolumeNamePrefix, providerName) + + ovaContainerName := fmt.Sprintf("%s-pod-%s", ovaServerPrefix, providerName) + + return v1.PodSpec{ + + Containers: []v1.Container{ + { + Name: ovaContainerName, + Ports: []v1.ContainerPort{{ContainerPort: 8080, Protocol: v1.ProtocolTCP}}, + SecurityContext: &v1.SecurityContext{ + RunAsNonRoot: &nonRoot, + }, + Image: imageName, + VolumeMounts: []v1.VolumeMount{ + { + Name: nfsVolumeName, + MountPath: "/ova", + }, + }, + }, + }, + ServiceAccountName: "forklift-controller", + Volumes: []v1.Volume{ + { + Name: nfsVolumeName, + VolumeSource: v1.VolumeSource{ + NFS: &v1.NFSVolumeSource{ + Server: nfsServer, + Path: nfsPath, + ReadOnly: false, + }, + }, + }, + }, + } +} + // Provider catalog. type Catalog struct { mutex sync.Mutex diff --git a/pkg/controller/provider/model/BUILD.bazel b/pkg/controller/provider/model/BUILD.bazel index b30d591e8..386aa5054 100644 --- a/pkg/controller/provider/model/BUILD.bazel +++ b/pkg/controller/provider/model/BUILD.bazel @@ -9,6 +9,7 @@ go_library( "//pkg/apis/forklift/v1beta1", "//pkg/controller/provider/model/ocp", "//pkg/controller/provider/model/openstack", + "//pkg/controller/provider/model/ova", "//pkg/controller/provider/model/ovirt", "//pkg/controller/provider/model/vsphere", ], diff --git a/pkg/controller/provider/model/doc.go b/pkg/controller/provider/model/doc.go index a2109a7eb..30c2e15ea 100644 --- a/pkg/controller/provider/model/doc.go +++ b/pkg/controller/provider/model/doc.go @@ -4,6 +4,7 @@ import ( api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ocp" "github.com/konveyor/forklift-controller/pkg/controller/provider/model/openstack" + "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ovirt" "github.com/konveyor/forklift-controller/pkg/controller/provider/model/vsphere" ) @@ -27,6 +28,10 @@ func Models(provider *api.Provider) (all []interface{}) { all = append( all, openstack.All()...) + case api.Ova: + all = append( + all, + ova.All()...) } return diff --git a/pkg/controller/provider/model/ova/BUILD.bazel b/pkg/controller/provider/model/ova/BUILD.bazel new file mode 100644 index 000000000..5b6908db8 --- /dev/null +++ b/pkg/controller/provider/model/ova/BUILD.bazel @@ -0,0 +1,18 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "ova", + srcs = [ + "doc.go", + "model.go", + "tree.go", + ], + importpath = "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova", + visibility = ["//visibility:public"], + deps = [ + "//pkg/controller/provider/model/base", + "//pkg/controller/provider/model/ocp", + "//pkg/lib/inventory/model", + "//pkg/lib/ref", + ], +) diff --git a/pkg/controller/provider/model/ova/doc.go b/pkg/controller/provider/model/ova/doc.go new file mode 100644 index 000000000..dd5bc4e97 --- /dev/null +++ b/pkg/controller/provider/model/ova/doc.go @@ -0,0 +1,15 @@ +package ova + +import ( + "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ocp" +) + +// Build all models. +func All() []interface{} { + return []interface{}{ + &ocp.Provider{}, + &VM{}, + &Network{}, + &Disk{}, + } +} diff --git a/pkg/controller/provider/model/ova/model.go b/pkg/controller/provider/model/ova/model.go new file mode 100644 index 000000000..e89026d52 --- /dev/null +++ b/pkg/controller/provider/model/ova/model.go @@ -0,0 +1,123 @@ +package ova + +import ( + "github.com/konveyor/forklift-controller/pkg/controller/provider/model/base" + libmodel "github.com/konveyor/forklift-controller/pkg/lib/inventory/model" +) + +// Errors +var NotFound = libmodel.NotFound + +type InvalidRefError = base.InvalidRefError + +const ( + MaxDetail = base.MaxDetail +) + +// Types +type ListOptions = base.ListOptions +type Concern = base.Concern +type Ref = base.Ref + +// Model. +type Model interface { + base.Model + GetName() string +} + +// Base OVA model. +type Base struct { + // Managed object ID. + ID string `sql:"pk"` + // Variant + Variant string `sql:"d0,index(variant)"` + // Name + Name string `sql:"d0,index(name)"` + // Revision + Revision int64 `sql:"incremented,d0,index(revision)"` +} + +func (m *Base) Pk() string { + return m.ID +} + +// String representation. +func (m *Base) String() string { + return m.ID +} + +// Get labels. +func (m *Base) Labels() libmodel.Labels { + return nil +} + +// Name. +func (m *Base) GetName() string { + return m.Name +} + +// Determine if current revision has been validated. +func (m *VM) Validated() bool { + return m.RevisionValidated == m.Revision +} + +type Network struct { + Base + Description string `sql:""` +} + +type VM struct { + Base + OvaPath string `sql:""` + RevisionValidated int64 `sql:"d0,index(revisionValidated)"` + PolicyVersion int `sql:"d0,index(policyVersion)"` + UUID string `sql:""` + Firmware string `sql:""` + CpuAffinity []int32 `sql:""` + CpuHotAddEnabled bool `sql:""` + CpuHotRemoveEnabled bool `sql:""` + MemoryHotAddEnabled bool `sql:""` + FaultToleranceEnabled bool `sql:""` + CpuCount int32 `sql:""` + CoresPerSocket int32 `sql:""` + MemoryMB int32 `sql:""` + BalloonedMemory int32 `sql:""` + IpAddress string `sql:""` + NumaNodeAffinity []string `sql:""` + StorageUsed int64 `sql:""` + ChangeTrackingEnabled bool `sql:""` + Devices []Device `sql:""` + NICs []NIC `sql:""` + Disks []Disk `sql:""` + Networks []Network `sql:""` + Concerns []Concern `sql:""` +} + +// Virtual Disk. +type Disk struct { + Base + FilePath string `sql:""` + Capacity string `sql:""` + CapacityAllocationUnits string `sql:""` + DiskId string `sql:""` + FileRef string `sql:""` + Format string `sql:""` + PopulatedSize string `sql:""` +} + +// Virtual Device. +type Device struct { + Kind string `sql:""` +} + +type Conf struct { + Key string `sql:""` + Value string `sql:""` +} + +// Virtual ethernet card. +type NIC struct { + Name string `sql:""` + MAC string `sql:""` + Config []Conf `sql:""` +} diff --git a/pkg/controller/provider/model/ova/tree.go b/pkg/controller/provider/model/ova/tree.go new file mode 100644 index 000000000..ba043c06d --- /dev/null +++ b/pkg/controller/provider/model/ova/tree.go @@ -0,0 +1,19 @@ +package ova + +import ( + "github.com/konveyor/forklift-controller/pkg/controller/provider/model/base" + libref "github.com/konveyor/forklift-controller/pkg/lib/ref" +) + +// Kinds +var ( + VmKind = libref.ToKind(VM{}) + NetKind = libref.ToKind(Network{}) + DiskKind = libref.ToKind(Disk{}) +) + +// Types. +type Tree = base.Tree +type TreeNode = base.TreeNode +type BranchNavigator = base.BranchNavigator +type ParentNavigator = base.ParentNavigator diff --git a/pkg/controller/provider/validation.go b/pkg/controller/provider/validation.go index 1446fe0f4..193985ae1 100644 --- a/pkg/controller/provider/validation.go +++ b/pkg/controller/provider/validation.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/url" + "regexp" api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" "github.com/konveyor/forklift-controller/pkg/controller/provider/container" @@ -142,6 +143,20 @@ func (r *Reconciler) validateURL(provider *api.Provider) error { Message: "The `url` is not valid.", }) } + if provider.Type() == api.Ova { + if !isValidNFSPath(provider.Spec.URL) { + provider.Status.Phase = ValidationFailed + provider.Status.SetCondition( + libcnd.Condition{ + Type: UrlNotValid, + Status: True, + Reason: Malformed, + Category: Critical, + Message: fmt.Sprintf("The NFS path is malformed"), + }) + } + return nil + } _, err := url.Parse(provider.Spec.URL) if err != nil { provider.Status.Phase = ValidationFailed @@ -338,3 +353,9 @@ func (r *Reconciler) inventoryCreated(provider *api.Provider) error { return nil } + +func isValidNFSPath(nfsPath string) bool { + nfsRegex := `^[^:]+:\/[^:].*$` + re := regexp.MustCompile(nfsRegex) + return re.MatchString(nfsPath) +} diff --git a/pkg/controller/provider/web/BUILD.bazel b/pkg/controller/provider/web/BUILD.bazel index b2c084bbc..d3076c3df 100644 --- a/pkg/controller/provider/web/BUILD.bazel +++ b/pkg/controller/provider/web/BUILD.bazel @@ -14,6 +14,7 @@ go_library( "//pkg/controller/provider/web/base", "//pkg/controller/provider/web/ocp", "//pkg/controller/provider/web/openstack", + "//pkg/controller/provider/web/ova", "//pkg/controller/provider/web/ovirt", "//pkg/controller/provider/web/vsphere", "//pkg/lib/error", diff --git a/pkg/controller/provider/web/client.go b/pkg/controller/provider/web/client.go index f37b9e8ff..feb4c21e7 100644 --- a/pkg/controller/provider/web/client.go +++ b/pkg/controller/provider/web/client.go @@ -9,6 +9,7 @@ import ( "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" "github.com/konveyor/forklift-controller/pkg/controller/provider/web/ocp" "github.com/konveyor/forklift-controller/pkg/controller/provider/web/openstack" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/ova" "github.com/konveyor/forklift-controller/pkg/controller/provider/web/ovirt" "github.com/konveyor/forklift-controller/pkg/controller/provider/web/vsphere" liberr "github.com/konveyor/forklift-controller/pkg/lib/error" @@ -77,6 +78,14 @@ func NewClient(provider *api.Provider) (client Client, err error) { Resolver: &openstack.Resolver{Provider: provider}, }, } + case api.Ova: + client = &ProviderClient{ + provider: provider, + finder: &ova.Finder{}, + restClient: base.RestClient{ + Resolver: &ova.Resolver{Provider: provider}, + }, + } default: err = liberr.Wrap( ProviderNotSupportedError{ diff --git a/pkg/controller/provider/web/doc.go b/pkg/controller/provider/web/doc.go index be640a690..26ecfd6f7 100644 --- a/pkg/controller/provider/web/doc.go +++ b/pkg/controller/provider/web/doc.go @@ -4,6 +4,7 @@ import ( "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" "github.com/konveyor/forklift-controller/pkg/controller/provider/web/ocp" "github.com/konveyor/forklift-controller/pkg/controller/provider/web/openstack" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/ova" "github.com/konveyor/forklift-controller/pkg/controller/provider/web/ovirt" "github.com/konveyor/forklift-controller/pkg/controller/provider/web/vsphere" "github.com/konveyor/forklift-controller/pkg/lib/inventory/container" @@ -32,5 +33,8 @@ func All(container *container.Container) (all []libweb.RequestHandler) { all = append( all, openstack.Handlers(container)...) + all = append( + all, + ova.Handlers(container)...) return } diff --git a/pkg/controller/provider/web/ova/BUILD.bazel b/pkg/controller/provider/web/ova/BUILD.bazel new file mode 100644 index 000000000..6edc2fee0 --- /dev/null +++ b/pkg/controller/provider/web/ova/BUILD.bazel @@ -0,0 +1,33 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "ova", + srcs = [ + "base.go", + "client.go", + "disk.go", + "doc.go", + "network.go", + "provider.go", + "resource.go", + "tree.go", + "vm.go", + "workload.go", + ], + importpath = "github.com/konveyor/forklift-controller/pkg/controller/provider/web/ova", + visibility = ["//visibility:public"], + deps = [ + "//pkg/apis/forklift/v1beta1", + "//pkg/controller/provider/model/ocp", + "//pkg/controller/provider/model/ova", + "//pkg/controller/provider/web/base", + "//pkg/controller/provider/web/ocp", + "//pkg/lib/error", + "//pkg/lib/inventory/container", + "//pkg/lib/inventory/model", + "//pkg/lib/inventory/web", + "//pkg/lib/logging", + "//pkg/lib/ref", + "//vendor/github.com/gin-gonic/gin", + ], +) diff --git a/pkg/controller/provider/web/ova/base.go b/pkg/controller/provider/web/ova/base.go new file mode 100644 index 000000000..5926b3e63 --- /dev/null +++ b/pkg/controller/provider/web/ova/base.go @@ -0,0 +1,89 @@ +package ova + +import ( + "strings" + + pathlib "path" + + "github.com/gin-gonic/gin" + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" + libmodel "github.com/konveyor/forklift-controller/pkg/lib/inventory/model" + "github.com/konveyor/forklift-controller/pkg/lib/logging" +) + +// Package logger. +var log = logging.WithName("web|ova") + +// Fields. +const ( + DetailParam = base.DetailParam + NameParam = base.NameParam +) + +// Base handler. +type Handler struct { + base.Handler +} + +// Build list predicate. +func (h Handler) Predicate(ctx *gin.Context) (p libmodel.Predicate) { + q := ctx.Request.URL.Query() + name := q.Get(NameParam) + if len(name) > 0 { + path := strings.Split(name, "/") + name := path[len(path)-1] + p = libmodel.Eq(NameParam, name) + } + + return +} + +// Build list options. +func (h Handler) ListOptions(ctx *gin.Context) libmodel.ListOptions { + detail := h.Detail + if detail > 0 { + detail = model.MaxDetail + } + return libmodel.ListOptions{ + Predicate: h.Predicate(ctx), + Detail: detail, + Page: &h.Page, + } +} + +// Path builder. +type PathBuilder struct { + // Database. + DB libmodel.DB + // Cached resource + cache map[string]string +} + +func (r *PathBuilder) Path(m model.Model) (path string) { + var err error + if r.cache == nil { + r.cache = map[string]string{} + } + switch m.(type) { + case *model.VM: + vm := m.(*model.VM) + path = pathlib.Join(vm.UUID) + case *model.Network: + net := m.(*model.Network) + path = pathlib.Join(net.ID) + case *model.Disk: + disk := m.(*model.Disk) + path = pathlib.Join(disk.ID) + } + + if err != nil { + log.Error( + err, + "path builder failed.", + "model", + libmodel.Describe(m)) + } + + return +} diff --git a/pkg/controller/provider/web/ova/client.go b/pkg/controller/provider/web/ova/client.go new file mode 100644 index 000000000..b39b13e4f --- /dev/null +++ b/pkg/controller/provider/web/ova/client.go @@ -0,0 +1,294 @@ +package ova + +import ( + "strings" + + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" + liberr "github.com/konveyor/forklift-controller/pkg/lib/error" +) + +// Errors. +type ResourceNotResolvedError = base.ResourceNotResolvedError +type RefNotUniqueError = base.RefNotUniqueError +type NotFoundError = base.NotFoundError + +// API path resolver. +type Resolver struct { + *api.Provider +} + +// Build the URL path. +func (r *Resolver) Path(resource interface{}, id string) (path string, err error) { + provider := r.Provider + switch resource.(type) { + case *Provider: + r := Provider{} + r.UID = id + r.Link() + path = r.SelfLink + case *Network: + r := Network{} + r.ID = id + r.Link(provider) + path = r.SelfLink + case *VM: + r := VM{} + r.ID = id + r.Link(provider) + path = r.SelfLink + case *Disk: + r := Disk{} + r.ID = id + r.Link(provider) + path = r.SelfLink + case *Workload: + r := Workload{} + r.ID = id + r.Link(provider) + path = r.SelfLink + default: + err = liberr.Wrap( + base.ResourceNotResolvedError{ + Object: resource, + }) + } + + path = strings.TrimRight(path, "/") + + return +} + +// Resource finder. +type Finder struct { + base.Client +} + +// With client. +func (r *Finder) With(client base.Client) base.Finder { + r.Client = client + return r +} + +// Find a resource by ref. +// Returns: +// +// ProviderNotSupportedErr +// ProviderNotReadyErr +// NotFoundErr +// RefNotUniqueErr +func (r *Finder) ByRef(resource interface{}, ref base.Ref) (err error) { + switch resource.(type) { + case *Network: + id := ref.ID + if id != "" { + err = r.Get(resource, id) + return + } + name := ref.Name + if name != "" { + list := []Network{} + err = r.List( + &list, + base.Param{ + Key: DetailParam, + Value: "all", + }, + base.Param{ + Key: NameParam, + Value: name, + }) + if err != nil { + break + } + if len(list) == 0 { + err = liberr.Wrap(NotFoundError{Ref: ref}) + break + } + if len(list) > 1 { + err = liberr.Wrap(RefNotUniqueError{Ref: ref}) + break + } + *resource.(*Network) = list[0] + } + case *VM: + id := ref.ID + if id != "" { + err = r.Get(resource, id) + return + } + name := ref.Name + if name != "" { + list := []VM{} + err = r.List( + &list, + base.Param{ + Key: DetailParam, + Value: "all", + }, + base.Param{ + Key: NameParam, + Value: name, + }) + if err != nil { + break + } + if len(list) == 0 { + err = liberr.Wrap(NotFoundError{Ref: ref}) + break + } + if len(list) > 1 { + err = liberr.Wrap(RefNotUniqueError{Ref: ref}) + break + } + *resource.(*VM) = list[0] + } + case *Disk: + id := ref.ID + if id != "" { + err = r.Get(resource, id) + return + } + name := ref.Name + if name != "" { + list := []Disk{} + err = r.List( + &list, + base.Param{ + Key: DetailParam, + Value: "all", + }, + base.Param{ + Key: NameParam, + Value: name, + }) + if err != nil { + break + } + if len(list) == 0 { + err = liberr.Wrap(NotFoundError{Ref: ref}) + break + } + if len(list) > 1 { + err = liberr.Wrap(RefNotUniqueError{Ref: ref}) + break + } + *resource.(*Disk) = list[0] + } + case *Workload: + id := ref.ID + if id != "" { + err = r.Get(resource, id) + return + } + name := ref.Name + if name != "" { + list := []Workload{} + err = r.List( + &list, + base.Param{ + Key: DetailParam, + Value: "all", + }, + base.Param{ + Key: NameParam, + Value: name, + }) + if err != nil { + break + } + if len(list) == 0 { + err = liberr.Wrap(NotFoundError{Ref: ref}) + break + } + if len(list) > 1 { + err = liberr.Wrap(RefNotUniqueError{Ref: ref}) + break + } + *resource.(*Workload) = list[0] + } + default: + err = liberr.Wrap( + ResourceNotResolvedError{ + Object: resource, + }) + } + + return +} + +// Find a VM by ref. +// Returns the matching resource and: +// +// ProviderNotSupportedErr +// ProviderNotReadyErr +// NotFoundErr +// RefNotUniqueErr +func (r *Finder) VM(ref *base.Ref) (object interface{}, err error) { + vm := &VM{} + err = r.ByRef(vm, *ref) + if err == nil { + ref.ID = vm.ID + ref.Name = vm.Name + object = vm + } + + return +} + +// Find a Network by ref. +// Returns the matching resource and: +// +// ProviderNotSupportedErr +// ProviderNotReadyErr +// NotFoundErr +// RefNotUniqueErr +func (r *Finder) Network(ref *base.Ref) (object interface{}, err error) { + network := &Network{} + err = r.ByRef(network, *ref) + if err == nil { + ref.ID = network.ID + ref.Name = network.Name + object = network + } + + return +} + +// Find a Disk by ref. +// Returns the matching resource and: +// +// ProviderNotSupportedErr +// ProviderNotReadyErr +// NotFoundErr +// RefNotUniqueErr +func (r *Finder) Disk(ref *base.Ref) (object interface{}, err error) { + disk := &Disk{} + err = r.ByRef(disk, *ref) + if err == nil { + ref.ID = disk.ID + ref.Name = disk.Name + object = disk + } + + return +} + +// Find workload by ref. +// Returns the matching resource and: +// +// ProviderNotSupportedErr +// ProviderNotReadyErr +// NotFoundErr +// RefNotUniqueErr +func (r *Finder) Workload(ref *base.Ref) (object interface{}, err error) { + workload := &Workload{} + err = r.ByRef(workload, *ref) + if err == nil { + ref.ID = workload.ID + ref.Name = workload.Name + object = workload + } + + return +} diff --git a/pkg/controller/provider/web/ova/disk.go b/pkg/controller/provider/web/ova/disk.go new file mode 100644 index 000000000..5ce6de3e1 --- /dev/null +++ b/pkg/controller/provider/web/ova/disk.go @@ -0,0 +1,172 @@ +package ova + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" + libmodel "github.com/konveyor/forklift-controller/pkg/lib/inventory/model" +) + +// Routes +const ( + DiskParam = "disk" + DiskCollection = "disks" + DisksRoot = ProviderRoot + "/" + DiskCollection + DiskRoot = DisksRoot + "/:" + DiskParam +) + +// Disk handler. +type DiskHandler struct { + Handler +} + +// Add routes to the `gin` router. +func (h *DiskHandler) AddRoutes(e *gin.Engine) { + e.GET(DisksRoot, h.List) + e.GET(DisksRoot+"/", h.List) + e.GET(DiskRoot, h.Get) +} + +// List resources in a REST collection. +// A GET onn the collection that includes the `X-Watch` +// header will negotiate an upgrade of the connection +// to a websocket and push watch events. +func (h DiskHandler) List(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + if h.WatchRequest { + h.watch(ctx) + return + } + db := h.Collector.DB() + list := []model.Disk{} + err = db.List(&list, h.ListOptions(ctx)) + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + return + } + content := []interface{}{} + for _, m := range list { + r := &Disk{} + r.With(&m) + r.Link(h.Provider) + content = append(content, r.Content(h.Detail)) + } + + ctx.JSON(http.StatusOK, content) +} + +// Get a specific REST resource. +func (h DiskHandler) Get(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + h.Detail = model.MaxDetail + m := &model.Disk{ + Base: model.Base{ + ID: ctx.Param(DiskParam), + }, + } + db := h.Collector.DB() + err = db.Get(m) + if errors.Is(err, model.NotFound) { + ctx.Status(http.StatusNotFound) + return + } + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + return + } + r := &Disk{} + r.With(m) + r.Link(h.Provider) + content := r.Content(h.Detail) + + ctx.JSON(http.StatusOK, content) +} + +// Watch. +func (h *DiskHandler) watch(ctx *gin.Context) { + db := h.Collector.DB() + err := h.Watch( + ctx, + db, + &model.Disk{}, + func(in libmodel.Model) (r interface{}) { + m := in.(*model.Disk) + disk := &Disk{} + disk.With(m) + disk.Link(h.Provider) + r = disk + return + }) + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + } +} + +// REST Resource. +type Disk struct { + Resource + FilePath string + Capacity string + CapacityAllocationUnits string + DiskId string + FileRef string + Format string + PopulatedSize string +} + +// Build the resource using the model. +func (r *Disk) With(m *model.Disk) { + r.Resource.With(&m.Base) + r.FilePath = m.FilePath + r.Capacity = m.Capacity + r.CapacityAllocationUnits = m.CapacityAllocationUnits + r.DiskId = m.DiskId + r.FileRef = m.FileRef + r.Format = m.Format + r.PopulatedSize = m.PopulatedSize +} + +// Build self link (URI). +func (r *Disk) Link(p *api.Provider) { + r.SelfLink = base.Link( + DiskRoot, + base.Params{ + base.ProviderParam: string(p.UID), + DiskParam: r.ID, + }) +} + +// As content. +func (r *Disk) Content(detail int) interface{} { + if detail == 0 { + return r.Resource + } + + return r +} diff --git a/pkg/controller/provider/web/ova/doc.go b/pkg/controller/provider/web/ova/doc.go new file mode 100644 index 000000000..381976147 --- /dev/null +++ b/pkg/controller/provider/web/ova/doc.go @@ -0,0 +1,49 @@ +package ova + +import ( + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" + "github.com/konveyor/forklift-controller/pkg/lib/inventory/container" + libweb "github.com/konveyor/forklift-controller/pkg/lib/inventory/web" +) + +// Routes +const ( + Root = base.ProvidersRoot + "/" + string(api.Ova) +) + +// Build all handlers. +func Handlers(container *container.Container) []libweb.RequestHandler { + return []libweb.RequestHandler{ + &ProviderHandler{ + Handler: base.Handler{ + Container: container, + }, + }, + &TreeHandler{ + Handler: Handler{ + base.Handler{Container: container}, + }, + }, + &DiskHandler{ + Handler: Handler{ + base.Handler{Container: container}, + }, + }, + &NetworkHandler{ + Handler: Handler{ + base.Handler{Container: container}, + }, + }, + &VMHandler{ + Handler: Handler{ + base.Handler{Container: container}, + }, + }, + &WorkloadHandler{ + Handler: Handler{ + base.Handler{Container: container}, + }, + }, + } +} diff --git a/pkg/controller/provider/web/ova/network.go b/pkg/controller/provider/web/ova/network.go new file mode 100644 index 000000000..50154c5f5 --- /dev/null +++ b/pkg/controller/provider/web/ova/network.go @@ -0,0 +1,205 @@ +package ova + +import ( + "errors" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" + libmodel "github.com/konveyor/forklift-controller/pkg/lib/inventory/model" +) + +// Routes. +const ( + NetworkParam = "network" + NetworkCollection = "networks" + NetworksRoot = ProviderRoot + "/" + NetworkCollection + NetworkRoot = NetworksRoot + "/:" + NetworkParam +) + +// Network handler. +type NetworkHandler struct { + Handler +} + +// Add routes to the `gin` router. +func (h *NetworkHandler) AddRoutes(e *gin.Engine) { + e.GET(NetworksRoot, h.List) + e.GET(NetworksRoot+"/", h.List) + e.GET(NetworkRoot, h.Get) +} + +// List resources in a REST collection. +// A GET onn the collection that includes the `X-Watch` +// header will negotiate an upgrade of the connection +// to a websocket and push watch events. +func (h NetworkHandler) List(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + if h.WatchRequest { + h.watch(ctx) + return + } + defer func() { + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + } + }() + db := h.Collector.DB() + list := []model.Network{} + err = db.List(&list, h.ListOptions(ctx)) + if err != nil { + return + } + err = h.filter(ctx, &list) + if err != nil { + return + } + pb := PathBuilder{DB: db} + content := []interface{}{} + for _, m := range list { + r := &Network{} + r.With(&m) + r.Link(h.Provider) + r.Path = pb.Path(&m) + content = append(content, r.Content(h.Detail)) + } + + ctx.JSON(http.StatusOK, content) +} + +// Get a specific REST resource. +func (h NetworkHandler) Get(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + m := &model.Network{ + Base: model.Base{ + ID: ctx.Param(NetworkParam), + }, + } + db := h.Collector.DB() + err = db.Get(m) + if errors.Is(err, model.NotFound) { + ctx.Status(http.StatusNotFound) + return + } + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + return + } + pb := PathBuilder{DB: db} + r := &Network{} + r.With(m) + r.Link(h.Provider) + r.Path = pb.Path(m) + content := r.Content(model.MaxDetail) + + ctx.JSON(http.StatusOK, content) +} + +// Watch. +func (h *NetworkHandler) watch(ctx *gin.Context) { + db := h.Collector.DB() + err := h.Watch( + ctx, + db, + &model.Network{}, + func(in libmodel.Model) (r interface{}) { + pb := PathBuilder{DB: db} + m := in.(*model.Network) + network := &Network{} + network.With(m) + network.Link(h.Provider) + network.Path = pb.Path(m) + r = network + return + }) + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + } +} + +// Filter result set. +// Filter by path for `name` query. +func (h *NetworkHandler) filter(ctx *gin.Context, list *[]model.Network) (err error) { + if len(*list) < 2 { + return + } + q := ctx.Request.URL.Query() + name := q.Get(NameParam) + if len(name) == 0 { + return + } + if len(strings.Split(name, "/")) < 2 { + return + } + db := h.Collector.DB() + pb := PathBuilder{DB: db} + kept := []model.Network{} + for _, m := range *list { + path := pb.Path(&m) + if h.PathMatchRoot(path, name) { + kept = append(kept, m) + } + } + + *list = kept + + return +} + +// REST Resource. +type Network struct { + Resource + Description string +} + +// Build the resource using the model. +func (r *Network) With(m *model.Network) { + r.Resource.With(&m.Base) + r.Variant = m.Variant + r.Name = m.Name + r.Description = m.Description +} + +// Build self link (URI). +func (r *Network) Link(p *api.Provider) { + r.SelfLink = base.Link( + NetworkRoot, + base.Params{ + base.ProviderParam: string(p.UID), + NetworkParam: r.ID, + }) +} + +// As content. +func (r *Network) Content(detail int) interface{} { + if detail == 0 { + return r.Resource + } + + return r +} diff --git a/pkg/controller/provider/web/ova/provider.go b/pkg/controller/provider/web/ova/provider.go new file mode 100644 index 000000000..966664938 --- /dev/null +++ b/pkg/controller/provider/web/ova/provider.go @@ -0,0 +1,192 @@ +package ova + +import ( + "net/http" + + "github.com/gin-gonic/gin" + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ocp" + "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/ocp" +) + +// Routes. +const ( + ProviderParam = base.ProviderParam + ProvidersRoot = Root + ProviderRoot = ProvidersRoot + "/:" + ProviderParam +) + +// Provider handler. +type ProviderHandler struct { + base.Handler +} + +// Add routes to the `gin` router. +func (h *ProviderHandler) AddRoutes(e *gin.Engine) { + e.GET(ProvidersRoot, h.List) + e.GET(ProvidersRoot+"/", h.List) + e.GET(ProviderRoot, h.Get) +} + +// List resources in a REST collection. +func (h ProviderHandler) List(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + if h.WatchRequest { + ctx.Status(http.StatusBadRequest) + return + } + content, err := h.ListContent(ctx) + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + return + } + + ctx.JSON(http.StatusOK, content) +} + +// Get a specific REST resource. +func (h ProviderHandler) Get(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + if h.Provider.Type() != api.Ova { + ctx.Status(http.StatusNotFound) + return + } + h.Detail = model.MaxDetail + m := &model.Provider{} + m.With(h.Provider) + r := Provider{} + r.With(m) + err = h.AddDerived(&r) + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + return + } + r.Link() + content := r.Content(h.Detail) + + ctx.JSON(http.StatusOK, content) +} + +// Build the list content. +func (h *ProviderHandler) ListContent(ctx *gin.Context) (content []interface{}, err error) { + content = []interface{}{} + list := h.Container.List() + q := ctx.Request.URL.Query() + ns := q.Get(base.NsParam) + for _, collector := range list { + if p, cast := collector.Owner().(*api.Provider); cast { + if p.Type() != api.Ova { + continue + } + if ns != "" && ns != p.Namespace { + continue + } + if collector, found := h.Container.Get(p); found { + h.Collector = collector + } else { + continue + } + m := &model.Provider{} + m.With(p) + r := Provider{} + r.With(m) + aErr := h.AddDerived(&r) + if aErr != nil { + err = aErr + return + } + r.Link() + content = append(content, r.Content(h.Detail)) + } + } + + h.Page.Slice(&content) + + return +} + +// Add derived fields. +func (h ProviderHandler) AddDerived(r *Provider) (err error) { + var n int64 + if h.Detail == 0 { + return + } + db := h.Collector.DB() + // VM + n, err = db.Count(&ova.VM{}, nil) + if err != nil { + return + } + r.VMCount = n + // Network + n, err = db.Count(&ova.Network{}, nil) + if err != nil { + return + } + r.NetworkCount = n + // Disk + n, err = db.Count(&ova.Disk{}, nil) + if err != nil { + return + } + r.DiskCount = n + + return +} + +// REST Resource. +type Provider struct { + ocp.Resource + Type string `json:"type"` + Object api.Provider `json:"object"` + APIVersion string `json:"apiVersion"` + Product string `json:"product"` + VMCount int64 `json:"vmCount"` + NetworkCount int64 `json:"networkCount"` + DiskCount int64 +} + +// Set fields with the specified object. +func (r *Provider) With(m *model.Provider) { + r.Resource.With(&m.Base) + r.Type = m.Type + r.Object = m.Object +} + +// Build self link (URI). +func (r *Provider) Link() { + r.SelfLink = base.Link( + ProviderRoot, + base.Params{ + base.ProviderParam: r.UID, + }) +} + +// As content. +func (r *Provider) Content(detail int) interface{} { + if detail == 0 { + return r.Resource + } + + return r +} diff --git a/pkg/controller/provider/web/ova/resource.go b/pkg/controller/provider/web/ova/resource.go new file mode 100644 index 000000000..3d6cbb3f3 --- /dev/null +++ b/pkg/controller/provider/web/ova/resource.go @@ -0,0 +1,29 @@ +package ova + +import ( + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" +) + +// REST Resource. +type Resource struct { + // Object ID. + ID string `json:"id"` + // Variant + Variant string `json:"variant,omitempty"` + // Path + Path string `json:"path,omitempty"` + // Revision + Revision int64 `json:"revision"` + // Object name. + Name string `json:"name"` + // Self link. + SelfLink string `json:"selfLink"` +} + +// Build the resource using the model. +func (r *Resource) With(m *model.Base) { + r.ID = m.ID + r.Variant = m.Variant + r.Revision = m.Revision + r.Name = m.Name +} diff --git a/pkg/controller/provider/web/ova/tree.go b/pkg/controller/provider/web/ova/tree.go new file mode 100644 index 000000000..06f3df6b4 --- /dev/null +++ b/pkg/controller/provider/web/ova/tree.go @@ -0,0 +1,187 @@ +package ova + +import ( + "net/http" + + "github.com/gin-gonic/gin" + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" + libmodel "github.com/konveyor/forklift-controller/pkg/lib/inventory/model" + libref "github.com/konveyor/forklift-controller/pkg/lib/ref" +) + +// Routes. +const ( + TreeRoot = ProviderRoot + "/tree" + TreeVMRoot = TreeRoot + "/vm" +) + +// Types. +type Tree = base.Tree +type TreeNode = base.TreeNode + +// Tree handler. +type TreeHandler struct { + Handler + // VM list. + vm []model.VM +} + +// Add routes to the `gin` router. +func (h *TreeHandler) AddRoutes(e *gin.Engine) { + //e.GET(TreeVMRoot, h.Tree) +} + +// Prepare to handle the request. +func (h *TreeHandler) Prepare(ctx *gin.Context) int { + status, err := h.Handler.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return status + } + db := h.Collector.DB() + err = db.List( + &h.vm, + model.ListOptions{ + Detail: model.MaxDetail, + }) + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + return http.StatusInternalServerError + } + + return http.StatusOK +} + +// List not supported. +func (h TreeHandler) List(ctx *gin.Context) { + ctx.Status(http.StatusMethodNotAllowed) +} + +// Get not supported. +func (h TreeHandler) Get(ctx *gin.Context) { + ctx.Status(http.StatusMethodNotAllowed) +} + +// Tree. +func (h TreeHandler) Tree(ctx *gin.Context) { + // status := h.Prepare(ctx) + // if status != http.StatusOK { + // ctx.Status(status) + // return + // } + // if h.WatchRequest { + // ctx.Status(http.StatusBadRequest) + // return + // } + // db := h.Collector.DB() + // pb := PathBuilder{DB: db} + // content := TreeNode{} + // for _, vm := range h.vm { + // tr := Tree{ + // NodeBuilder: &NodeBuilder{ + // handler: h.Handler, + // pathBuilder: pb, + // detail: map[string]int{ + // model.VmKind: h.Detail, + // }, + // }, + // } + // branch, err := tr.Build( + // &vm, + // &BranchNavigator{ + // detail: h.Detail, + // db: db, + // }) + // if err != nil { + // log.Trace( + // err, + // "url", + // ctx.Request.URL) + // ctx.Status(http.StatusInternalServerError) + // return + // } + // r := VM{} + // r.With(&vm) + // r.Link(h.Provider) + // r.Path = pb.Path(&vm) + // branch.Kind = model.VmKind + // branch.Object = r + // content.Children = append(content.Children, branch) + // } + + // ctx.JSON(http.StatusOK, content) +} + +// Tree (branch) navigator. +type BranchNavigator struct { + db libmodel.DB + detail int +} + +// Tree node builder. +type NodeBuilder struct { + // Handler. + handler Handler + // Resource details by kind. + detail map[string]int + // Path builder. + pathBuilder PathBuilder +} + +// Build a node for the model. +func (r *NodeBuilder) Node(parent *TreeNode, m model.Model) *TreeNode { + provider := r.handler.Provider + kind := libref.ToKind(m) + node := &TreeNode{} + switch kind { + case model.VmKind: + resource := &VM{} + resource.With(m.(*model.VM)) + resource.Link(provider) + resource.Path = r.pathBuilder.Path(m) + object := resource.Content(r.withDetail(kind)) + node = &TreeNode{ + Parent: parent, + Kind: kind, + Object: object, + } + case model.NetKind: + resource := &Network{} + resource.With(m.(*model.Network)) + resource.Link(provider) + resource.Path = r.pathBuilder.Path(m) + object := resource.Content(r.withDetail(kind)) + node = &TreeNode{ + Parent: parent, + Kind: kind, + Object: object, + } + case model.DiskKind: + resource := &Disk{} + resource.With(m.(*model.Disk)) + resource.Link(provider) + resource.Path = r.pathBuilder.Path(m) + object := resource.Content(r.withDetail(kind)) + node = &TreeNode{ + Parent: parent, + Kind: kind, + Object: object, + } + } + + return node +} + +// Build with detail. +func (r *NodeBuilder) withDetail(kind string) int { + if b, found := r.detail[kind]; found { + return b + } + + return 0 +} diff --git a/pkg/controller/provider/web/ova/vm.go b/pkg/controller/provider/web/ova/vm.go new file mode 100644 index 000000000..9b6110852 --- /dev/null +++ b/pkg/controller/provider/web/ova/vm.go @@ -0,0 +1,273 @@ +package ova + +import ( + "errors" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" + libmodel "github.com/konveyor/forklift-controller/pkg/lib/inventory/model" +) + +// Routes. +const ( + VMParam = "vm" + VMCollection = "vms" + VMsRoot = ProviderRoot + "/" + VMCollection + VMRoot = VMsRoot + "/:" + VMParam +) + +// Virtual Machine handler. +type VMHandler struct { + Handler +} + +// Add routes to the `gin` router. +func (h *VMHandler) AddRoutes(e *gin.Engine) { + e.GET(VMsRoot, h.List) + e.GET(VMsRoot+"/", h.List) + e.GET(VMRoot, h.Get) +} + +// List resources in a REST collection. +// A GET onn the collection that includes the `X-Watch` +// header will negotiate an upgrade of the connection +// to a websocket and push watch events. +func (h VMHandler) List(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + if h.WatchRequest { + h.watch(ctx) + return + } + defer func() { + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + } + }() + db := h.Collector.DB() + list := []model.VM{} + err = db.List(&list, h.ListOptions(ctx)) + if err != nil { + return + } + content := []interface{}{} + err = h.filter(ctx, &list) + if err != nil { + return + } + pb := PathBuilder{DB: db} + for _, m := range list { + r := &VM{} + r.With(&m) + r.Link(h.Provider) + r.Path = pb.Path(&m) + content = append(content, r.Content(h.Detail)) + } + + ctx.JSON(http.StatusOK, content) +} + +// Get a specific REST resource. +func (h VMHandler) Get(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + m := &model.VM{ + Base: model.Base{ + ID: ctx.Param(VMParam), + }, + } + db := h.Collector.DB() + err = db.Get(m) + if errors.Is(err, model.NotFound) { + ctx.Status(http.StatusNotFound) + return + } + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + return + } + pb := PathBuilder{DB: db} + r := &VM{} + r.With(m) + r.Link(h.Provider) + r.Path = pb.Path(m) + content := r.Content(model.MaxDetail) + + ctx.JSON(http.StatusOK, content) +} + +// Watch. +func (h *VMHandler) watch(ctx *gin.Context) { + db := h.Collector.DB() + err := h.Watch( + ctx, + db, + &model.VM{}, + func(in libmodel.Model) (r interface{}) { + pb := PathBuilder{DB: db} + m := in.(*model.VM) + vm := &VM{} + vm.With(m) + vm.Link(h.Provider) + vm.Path = pb.Path(m) + r = vm + return + }) + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + } +} + +// Filter result set. +// Filter by path for `name` query. +func (h *VMHandler) filter(ctx *gin.Context, list *[]model.VM) (err error) { + if len(*list) < 2 { + return + } + q := ctx.Request.URL.Query() + name := q.Get(NameParam) + if len(name) == 0 { + return + } + if len(strings.Split(name, "/")) < 2 { + return + } + db := h.Collector.DB() + pb := PathBuilder{DB: db} + kept := []model.VM{} + for _, m := range *list { + path := pb.Path(&m) + if h.PathMatch(path, name) { + kept = append(kept, m) + } + } + + *list = kept + + return +} + +// VM detail=0 +type VM0 = Resource + +// VM detail=1 +type VM1 struct { + VM0 + RevisionValidated int64 `json:"revisionValidated"` + Networks []model.Network `json:"networks"` + Disks []model.Disk `json:"disks"` + Concerns []model.Concern `json:"concerns"` +} + +// Build the resource using the model. +func (r *VM1) With(m *model.VM) { + r.VM0.With(&m.Base) + r.RevisionValidated = m.RevisionValidated + r.Disks = m.Disks + r.Concerns = m.Concerns +} + +// As content. +func (r *VM1) Content(detail int) interface{} { + if detail < 1 { + return &r.VM0 + } + + return r +} + +// VM full detail. +type VM struct { + VM1 + OvaPath string + RevisionValidated int64 + PolicyVersion int + UUID string + Firmware string + CpuAffinity []int32 + CpuHotAddEnabled bool + CpuHotRemoveEnabled bool + MemoryHotAddEnabled bool + FaultToleranceEnabled bool + CpuCount int32 + CoresPerSocket int32 + MemoryMB int32 + BalloonedMemory int32 + IpAddress string + NumaNodeAffinity []string + StorageUsed int64 + ChangeTrackingEnabled bool + Devices []model.Device + NICs []model.NIC + Disks []model.Disk + Networks []model.Network +} + +// Build the resource using the model. +func (r *VM) With(m *model.VM) { + r.VM1.With(m) + r.PolicyVersion = m.PolicyVersion + r.UUID = m.UUID + r.Firmware = m.Firmware + r.ChangeTrackingEnabled = m.ChangeTrackingEnabled + r.CpuAffinity = m.CpuAffinity + r.CpuHotAddEnabled = m.CpuHotAddEnabled + r.CpuHotRemoveEnabled = m.CpuHotRemoveEnabled + r.MemoryHotAddEnabled = m.MemoryHotAddEnabled + r.CpuCount = m.CpuCount + r.CoresPerSocket = m.CoresPerSocket + r.MemoryMB = m.MemoryMB + r.BalloonedMemory = m.BalloonedMemory + r.IpAddress = m.IpAddress + r.StorageUsed = m.StorageUsed + r.FaultToleranceEnabled = m.FaultToleranceEnabled + r.Devices = m.Devices + r.NumaNodeAffinity = m.NumaNodeAffinity + r.NICs = m.NICs + r.OvaPath = m.OvaPath + r.Disks = m.Disks + r.Networks = m.Networks +} + +// Build self link (URI). +func (r *VM) Link(p *api.Provider) { + r.SelfLink = base.Link( + VMRoot, + base.Params{ + base.ProviderParam: string(p.UID), + VMParam: r.ID, + }) +} + +// As content. +func (r *VM) Content(detail int) interface{} { + if detail < 2 { + return r.VM1.Content(detail) + } + + return r +} diff --git a/pkg/controller/provider/web/ova/workload.go b/pkg/controller/provider/web/ova/workload.go new file mode 100644 index 000000000..1ad3f9854 --- /dev/null +++ b/pkg/controller/provider/web/ova/workload.go @@ -0,0 +1,93 @@ +package ova + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + api "github.com/konveyor/forklift-controller/pkg/apis/forklift/v1beta1" + model "github.com/konveyor/forklift-controller/pkg/controller/provider/model/ova" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" +) + +// Routes. +const ( + WorkloadCollection = "workloads" + WorkloadsRoot = ProviderRoot + "/" + WorkloadCollection + WorkloadRoot = WorkloadsRoot + "/:" + VMParam +) + +// Virtual Machine handler. +type WorkloadHandler struct { + Handler +} + +// Add routes to the `gin` router. +// no-op +func (h *WorkloadHandler) AddRoutes(e *gin.Engine) { + e.GET(WorkloadRoot, h.Get) +} + +// List resources in a REST collection. +func (h WorkloadHandler) List(ctx *gin.Context) { +} + +// Get a specific REST resource. +// no-op +func (h WorkloadHandler) Get(ctx *gin.Context) { + status, err := h.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + m := &model.VM{ + Base: model.Base{ + ID: ctx.Param(VMParam), + }, + } + db := h.Collector.DB() + err = db.Get(m) + if errors.Is(err, model.NotFound) { + ctx.Status(http.StatusNotFound) + return + } + defer func() { + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + } + }() + if err != nil { + return + } + r := Workload{} + r.With(m) + r.Link(h.Provider) + content := r + + ctx.JSON(http.StatusOK, content) +} + +// Workload +type Workload struct { + SelfLink string `json:"selfLink"` + VM +} + +func (r *Workload) With(m *model.VM) { + r.VM.With(m) +} + +// Build self link (URI). +func (r *Workload) Link(p *api.Provider) { + r.SelfLink = base.Link( + WorkloadRoot, + base.Params{ + base.ProviderParam: string(p.UID), + VMParam: r.ID, + }) +} diff --git a/pkg/controller/provider/web/provider.go b/pkg/controller/provider/web/provider.go index da3d9a50f..ec3472d8e 100644 --- a/pkg/controller/provider/web/provider.go +++ b/pkg/controller/provider/web/provider.go @@ -6,6 +6,7 @@ import ( "github.com/konveyor/forklift-controller/pkg/controller/provider/web/base" "github.com/konveyor/forklift-controller/pkg/controller/provider/web/ocp" "github.com/konveyor/forklift-controller/pkg/controller/provider/web/openstack" + "github.com/konveyor/forklift-controller/pkg/controller/provider/web/ova" "github.com/konveyor/forklift-controller/pkg/controller/provider/web/ovirt" "github.com/konveyor/forklift-controller/pkg/controller/provider/web/vsphere" "github.com/konveyor/forklift-controller/pkg/lib/logging" @@ -124,11 +125,33 @@ func (h ProviderHandler) List(ctx *gin.Context) { ctx.Status(http.StatusInternalServerError) return } + // OVA + ovaHandler := &ova.ProviderHandler{ + Handler: base.Handler{ + Container: h.Container, + }, + } + status, err = ovaHandler.Prepare(ctx) + if status != http.StatusOK { + ctx.Status(status) + base.SetForkliftError(ctx, err) + return + } + ovaList, err := ovaHandler.ListContent(ctx) + if err != nil { + log.Trace( + err, + "url", + ctx.Request.URL) + ctx.Status(http.StatusInternalServerError) + return + } r := Provider{ string(api.OpenShift): ocpList, string(api.VSphere): vSphereList, string(api.OVirt): oVirtList, string(api.OpenStack): openStackList, + string(api.Ova): ovaList, } content := r From 116f637f951325fa51c17d82469d823f262ba585 Mon Sep 17 00:00:00 2001 From: Arik Hadas Date: Sun, 23 Jul 2023 16:19:12 +0300 Subject: [PATCH 2/2] Fix code smell issues 1. Remove unused empty function WorkloadHandler#List 2. Reduce Cognitive Complexity of ProviderHandler#ListContent Signed-off-by: Arik Hadas --- pkg/controller/provider/web/ova/workload.go | 6 ------ pkg/controller/provider/web/ovirt/provider.go | 5 +---- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/pkg/controller/provider/web/ova/workload.go b/pkg/controller/provider/web/ova/workload.go index 1ad3f9854..5849a840b 100644 --- a/pkg/controller/provider/web/ova/workload.go +++ b/pkg/controller/provider/web/ova/workload.go @@ -23,17 +23,11 @@ type WorkloadHandler struct { } // Add routes to the `gin` router. -// no-op func (h *WorkloadHandler) AddRoutes(e *gin.Engine) { e.GET(WorkloadRoot, h.Get) } -// List resources in a REST collection. -func (h WorkloadHandler) List(ctx *gin.Context) { -} - // Get a specific REST resource. -// no-op func (h WorkloadHandler) Get(ctx *gin.Context) { status, err := h.Prepare(ctx) if status != http.StatusOK { diff --git a/pkg/controller/provider/web/ovirt/provider.go b/pkg/controller/provider/web/ovirt/provider.go index 3da26a27f..8e748e464 100644 --- a/pkg/controller/provider/web/ovirt/provider.go +++ b/pkg/controller/provider/web/ovirt/provider.go @@ -95,10 +95,7 @@ func (h *ProviderHandler) ListContent(ctx *gin.Context) (content []interface{}, ns := q.Get(base.NsParam) for _, collector := range list { if p, cast := collector.Owner().(*api.Provider); cast { - if p.Type() != api.OVirt { - continue - } - if ns != "" && ns != p.Namespace { + if p.Type() != api.OVirt || (ns != "" && ns != p.Namespace) { continue } if collector, found := h.Container.Get(p); found {