From 49798b567159ef08427c414dee02a44b8c932cc9 Mon Sep 17 00:00:00 2001 From: Vasyl Saienko Date: Thu, 27 Nov 2025 09:20:42 +0200 Subject: [PATCH 1/6] Add VPC object The patch impelements VPC crd to manage VPCs. --- api/v1alpha1/vpc_types.go | 70 +++++ api/v1alpha1/vpcmeta_types.go | 73 ++++++ api/v1alpha1/zz_generated.deepcopy.go | 204 +++++++++++++++ config/crd/bases/k8s.netris.ai_vpcmeta.yaml | 101 +++++++ config/crd/bases/k8s.netris.ai_vpcs.yaml | 97 +++++++ config/rbac/role.yaml | 52 ++++ controllers/controller.go | 15 ++ controllers/vpc_controller.go | 227 ++++++++++++++++ controllers/vpc_translations.go | 194 ++++++++++++++ controllers/vpcmeta_controller.go | 246 ++++++++++++++++++ .../crds/k8s.netris.ai_vpcmeta.yaml | 101 +++++++ .../crds/k8s.netris.ai_vpcs.yaml | 97 +++++++ .../netris-operator/templates/rbac.yaml | 52 ++++ main.go | 21 ++ samples/kustomization.yaml | 1 + samples/vpc.yaml | 13 + 16 files changed, 1564 insertions(+) create mode 100644 api/v1alpha1/vpc_types.go create mode 100644 api/v1alpha1/vpcmeta_types.go create mode 100644 config/crd/bases/k8s.netris.ai_vpcmeta.yaml create mode 100644 config/crd/bases/k8s.netris.ai_vpcs.yaml create mode 100644 controllers/vpc_controller.go create mode 100644 controllers/vpc_translations.go create mode 100644 controllers/vpcmeta_controller.go create mode 100644 deploy/charts/netris-operator/crds/k8s.netris.ai_vpcmeta.yaml create mode 100644 deploy/charts/netris-operator/crds/k8s.netris.ai_vpcs.yaml create mode 100644 samples/vpc.yaml diff --git a/api/v1alpha1/vpc_types.go b/api/v1alpha1/vpc_types.go new file mode 100644 index 0000000..51369dd --- /dev/null +++ b/api/v1alpha1/vpc_types.go @@ -0,0 +1,70 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// VPCStatus defines the observed state of VPC +type VPCStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + Status string `json:"status,omitempty"` + Message string `json:"message,omitempty"` + ModifiedDate metav1.Time `json:"modified,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Admin Tenant",type=string,JSONPath=`.spec.adminTenant` +// +kubebuilder:printcolumn:name="Guest Tenants",type=string,JSONPath=`.spec.guestTenants`,priority=1 +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Modified",type=date,JSONPath=`.status.modified`,priority=1 +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// VPC is the Schema for the vpcs API +type VPC struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec VPCSpec `json:"spec"` + Status VPCStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// VPCList contains a list of VPC +type VPCList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []VPC `json:"items"` +} + +// VPCSpec . +type VPCSpec struct { + AdminTenant string `json:"adminTenant"` + GuestTenants []string `json:"guestTenants,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +func init() { + SchemeBuilder.Register(&VPC{}, &VPCList{}) +} + diff --git a/api/v1alpha1/vpcmeta_types.go b/api/v1alpha1/vpcmeta_types.go new file mode 100644 index 0000000..69c9acc --- /dev/null +++ b/api/v1alpha1/vpcmeta_types.go @@ -0,0 +1,73 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// VPCMetaSpec defines the desired state of VPCMeta +type VPCMetaSpec struct { + Imported bool `json:"imported"` + Reclaim bool `json:"reclaimPolicy"` + VPCCRGeneration int64 `json:"vpcGeneration"` + ID int `json:"id"` + Name string `json:"name"` + VPCName string `json:"vpcName"` + AdminTenant string `json:"adminTenant"` + AdminTenantID int `json:"adminTenantId"` + GuestTenants []string `json:"guestTenants"` + GuestTenantIDs []int `json:"guestTenantIds"` + Tags []string `json:"tags"` + IsSystem bool `json:"isSystem,omitempty"` + IsDefault bool `json:"isDefault,omitempty"` + VNI int `json:"vni,omitempty"` +} + +// VPCMetaStatus defines the observed state of VPCMeta +type VPCMetaStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// VPCMeta is the Schema for the vpcmeta API +type VPCMeta struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec VPCMetaSpec `json:"spec,omitempty"` + Status VPCMetaStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// VPCMetaList contains a list of VPCMeta +type VPCMetaList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []VPCMeta `json:"items"` +} + +func init() { + SchemeBuilder.Register(&VPCMeta{}, &VPCMetaList{}) +} + diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8c9e31b..d88cc99 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -2540,3 +2540,207 @@ func (in *VNetSwitchPort) DeepCopy() *VNetSwitchPort { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPCSpec) DeepCopyInto(out *VPCSpec) { + *out = *in + if in.GuestTenants != nil { + in, out := &in.GuestTenants, &out.GuestTenants + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCSpec. +func (in *VPCSpec) DeepCopy() *VPCSpec { + if in == nil { + return nil + } + out := new(VPCSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPCStatus) DeepCopyInto(out *VPCStatus) { + *out = *in + in.ModifiedDate.DeepCopyInto(&out.ModifiedDate) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCStatus. +func (in *VPCStatus) DeepCopy() *VPCStatus { + if in == nil { + return nil + } + out := new(VPCStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPC) DeepCopyInto(out *VPC) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPC. +func (in *VPC) DeepCopy() *VPC { + if in == nil { + return nil + } + out := new(VPC) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VPC) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPCList) DeepCopyInto(out *VPCList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VPC, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCList. +func (in *VPCList) DeepCopy() *VPCList { + if in == nil { + return nil + } + out := new(VPCList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VPCList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPCMetaSpec) DeepCopyInto(out *VPCMetaSpec) { + *out = *in + if in.GuestTenants != nil { + in, out := &in.GuestTenants, &out.GuestTenants + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.GuestTenantIDs != nil { + in, out := &in.GuestTenantIDs, &out.GuestTenantIDs + *out = make([]int, len(*in)) + copy(*out, *in) + } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCMetaSpec. +func (in *VPCMetaSpec) DeepCopy() *VPCMetaSpec { + if in == nil { + return nil + } + out := new(VPCMetaSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPCMetaStatus) DeepCopyInto(out *VPCMetaStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCMetaStatus. +func (in *VPCMetaStatus) DeepCopy() *VPCMetaStatus { + if in == nil { + return nil + } + out := new(VPCMetaStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPCMeta) DeepCopyInto(out *VPCMeta) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCMeta. +func (in *VPCMeta) DeepCopy() *VPCMeta { + if in == nil { + return nil + } + out := new(VPCMeta) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VPCMeta) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPCMetaList) DeepCopyInto(out *VPCMetaList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VPCMeta, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCMetaList. +func (in *VPCMetaList) DeepCopy() *VPCMetaList { + if in == nil { + return nil + } + out := new(VPCMetaList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VPCMetaList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/config/crd/bases/k8s.netris.ai_vpcmeta.yaml b/config/crd/bases/k8s.netris.ai_vpcmeta.yaml new file mode 100644 index 0000000..0fee7b3 --- /dev/null +++ b/config/crd/bases/k8s.netris.ai_vpcmeta.yaml @@ -0,0 +1,101 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: vpcmeta.k8s.netris.ai +spec: + group: k8s.netris.ai + names: + kind: VPCMeta + listKind: VPCMetaList + plural: vpcmeta + singular: vpcmeta + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: VPCMeta is the Schema for the vpcmeta API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: VPCMetaSpec defines the desired state of VPCMeta + properties: + adminTenant: + type: string + adminTenantId: + type: integer + guestTenantIds: + items: + type: integer + type: array + guestTenants: + items: + type: string + type: array + id: + type: integer + imported: + type: boolean + isDefault: + type: boolean + isSystem: + type: boolean + name: + type: string + reclaimPolicy: + type: boolean + tags: + items: + type: string + type: array + vni: + type: integer + vpcGeneration: + format: int64 + type: integer + vpcName: + type: string + required: + - adminTenant + - adminTenantId + - guestTenantIds + - guestTenants + - id + - imported + - name + - reclaimPolicy + - tags + - vpcGeneration + - vpcName + type: object + status: + description: VPCMetaStatus defines the observed state of VPCMeta + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/config/crd/bases/k8s.netris.ai_vpcs.yaml b/config/crd/bases/k8s.netris.ai_vpcs.yaml new file mode 100644 index 0000000..83e7ed2 --- /dev/null +++ b/config/crd/bases/k8s.netris.ai_vpcs.yaml @@ -0,0 +1,97 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: vpcs.k8s.netris.ai +spec: + group: k8s.netris.ai + names: + kind: VPC + listKind: VPCList + plural: vpcs + singular: vpc + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.adminTenant + name: Admin Tenant + type: string + - jsonPath: .spec.guestTenants + name: Guest Tenants + priority: 1 + type: string + - jsonPath: .status.status + name: Status + type: string + - jsonPath: .status.modified + name: Modified + priority: 1 + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: VPC is the Schema for the vpcs API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: VPCSpec . + properties: + adminTenant: + type: string + guestTenants: + items: + type: string + type: array + tags: + items: + type: string + type: array + required: + - adminTenant + type: object + status: + description: VPCStatus defines the observed state of VPC + properties: + message: + type: string + modified: + format: date-time + type: string + status: + description: 'INSERT ADDITIONAL STATUS FIELD - define observed state + of cluster Important: Run "make" to regenerate code after modifying + this file' + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 55f313c..f995265 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -701,3 +701,55 @@ rules: - get - patch - update +- apiGroups: + - k8s.netris.ai + resources: + - vpcmeta + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - k8s.netris.ai + resources: + - vpcmeta/finalizers + verbs: + - update +- apiGroups: + - k8s.netris.ai + resources: + - vpcmeta/status + verbs: + - get + - patch + - update +- apiGroups: + - k8s.netris.ai + resources: + - vpcs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - k8s.netris.ai + resources: + - vpcs/finalizers + verbs: + - update +- apiGroups: + - k8s.netris.ai + resources: + - vpcs/status + verbs: + - get + - patch + - update diff --git a/controllers/controller.go b/controllers/controller.go index 49cb759..4beacf6 100644 --- a/controllers/controller.go +++ b/controllers/controller.go @@ -321,3 +321,18 @@ func (u *uniReconciler) patchController(controller *k8sv1alpha1.Controller) (ctr } return ctrl.Result{RequeueAfter: requeueInterval}, nil } + +func (u *uniReconciler) patchVPCStatus(vpc *k8sv1alpha1.VPC, status, message string) (ctrl.Result, error) { + u.DebugLogger.Info("Patching Status", "status", status, "message", message) + + vpc.Status.Status = status + vpc.Status.Message = message + + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + err := u.Status().Patch(ctx, vpc.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + u.DebugLogger.Info("{r.Status().Patch}", "error", err, "action", "status update") + } + return ctrl.Result{RequeueAfter: requeueInterval}, nil +} diff --git a/controllers/vpc_controller.go b/controllers/vpc_controller.go new file mode 100644 index 0000000..a03c179 --- /dev/null +++ b/controllers/vpc_controller.go @@ -0,0 +1,227 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + + "go.uber.org/zap/zapcore" + "k8s.io/apimachinery/pkg/api/errors" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + k8sv1alpha1 "github.com/netrisai/netris-operator/api/v1alpha1" + "github.com/netrisai/netris-operator/netrisstorage" + "github.com/netrisai/netriswebapi/http" + api "github.com/netrisai/netriswebapi/v2" +) + +// VPCReconciler reconciles a VPC object +type VPCReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + Cred *api.Clientset + NStorage *netrisstorage.Storage +} + +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=vpcs,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=vpcs/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=vpcs/finalizers,verbs=update + +// Reconcile vpc events +func (r *VPCReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + logger := r.Log.WithValues("name", req.NamespacedName) + debugLogger := logger.V(int(zapcore.WarnLevel)) + vpcCR := &k8sv1alpha1.VPC{} + + u := uniReconciler{ + Client: r.Client, + Logger: logger, + DebugLogger: debugLogger, + Cred: r.Cred, + NStorage: r.NStorage, + } + + vpcCtx, vpcCancel := context.WithTimeout(cntxt, contextTimeout) + defer vpcCancel() + if err := r.Get(vpcCtx, req.NamespacedName, vpcCR); err != nil { + if errors.IsNotFound(err) { + debugLogger.Info(err.Error()) + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + vpcMetaNamespaced := req.NamespacedName + vpcMetaNamespaced.Name = string(vpcCR.GetUID()) + vpcMeta := &k8sv1alpha1.VPCMeta{} + metaFound := true + vpcMetaCtx, vpcMetaCancel := context.WithTimeout(cntxt, contextTimeout) + defer vpcMetaCancel() + if err := r.Get(vpcMetaCtx, vpcMetaNamespaced, vpcMeta); err != nil { + if errors.IsNotFound(err) { + debugLogger.Info(err.Error()) + metaFound = false + vpcMeta = nil + } else { + return ctrl.Result{}, err + } + } + + if vpcCR.DeletionTimestamp != nil { + logger.Info("Go to delete") + _, err := r.deleteVPC(vpcCR, vpcMeta) + if err != nil { + logger.Error(fmt.Errorf("{deleteVPC} %s", err), "") + return u.patchVPCStatus(vpcCR, "Failure", err.Error()) + } + logger.Info("VPC deleted") + return ctrl.Result{}, nil + } + + if vpcMustUpdateAnnotations(vpcCR) { + debugLogger.Info("Setting default annotations") + vpcUpdateDefaultAnnotations(vpcCR) + vpcUpdateCtx, vpcUpdateCancel := context.WithTimeout(cntxt, contextTimeout) + defer vpcUpdateCancel() + err := r.Patch(vpcUpdateCtx, vpcCR.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{Patch VPC default annotations} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + return ctrl.Result{}, nil + } + + if metaFound { + debugLogger.Info("Meta found") + if vpcCompareFieldsForNewMeta(vpcCR, vpcMeta) { + debugLogger.Info("Generating New Meta") + vpcID := vpcMeta.Spec.ID + newVpcMeta, err := r.VPCToVPCMeta(vpcCR) + if err != nil { + logger.Error(fmt.Errorf("{VPCToVPCMeta} %s", err), "") + return u.patchVPCStatus(vpcCR, "Failure", err.Error()) + } + vpcMeta.Spec = newVpcMeta.DeepCopy().Spec + vpcMeta.Spec.ID = vpcID + vpcMeta.Spec.VPCCRGeneration = vpcCR.GetGeneration() + + vpcMetaUpdateCtx, vpcMetaUpdateCancel := context.WithTimeout(cntxt, contextTimeout) + defer vpcMetaUpdateCancel() + err = r.Update(vpcMetaUpdateCtx, vpcMeta.DeepCopyObject(), &client.UpdateOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{vpcMeta Update} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + } + } else { + debugLogger.Info("Meta not found") + if vpcCR.GetFinalizers() == nil { + vpcCR.SetFinalizers([]string{"resource.k8s.netris.ai/delete"}) + vpcPatchCtx, vpcPatchCancel := context.WithTimeout(cntxt, contextTimeout) + defer vpcPatchCancel() + err := r.Patch(vpcPatchCtx, vpcCR.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{Patch VPC Finalizer} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + return ctrl.Result{}, nil + } + + vpcMeta, err := r.VPCToVPCMeta(vpcCR) + if err != nil { + logger.Error(fmt.Errorf("{VPCToVPCMeta} %s", err), "") + return u.patchVPCStatus(vpcCR, "Failure", err.Error()) + } + + vpcMeta.Spec.VPCCRGeneration = vpcCR.GetGeneration() + + vpcMetaCreateCtx, vpcMetaCreateCancel := context.WithTimeout(cntxt, contextTimeout) + defer vpcMetaCreateCancel() + if err := r.Create(vpcMetaCreateCtx, vpcMeta.DeepCopyObject(), &client.CreateOptions{}); err != nil { + logger.Error(fmt.Errorf("{vpcMeta Create} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + } + + return ctrl.Result{RequeueAfter: requeueInterval}, nil +} + +func (r *VPCReconciler) deleteVPC(vpcCR *k8sv1alpha1.VPC, vpcMeta *k8sv1alpha1.VPCMeta) (ctrl.Result, error) { + if vpcMeta != nil && vpcMeta.Spec.ID > 0 && !vpcMeta.Spec.Reclaim { + reply, err := r.Cred.VPC().Delete(vpcMeta.Spec.ID) + if err != nil { + return ctrl.Result{}, fmt.Errorf("{deleteVPC} %s", err) + } + resp, err := http.ParseAPIResponse(reply.Data) + if err != nil { + return ctrl.Result{}, err + } + if !resp.IsSuccess { + if resp.Message != "Invalid VPC ID" { + return ctrl.Result{}, fmt.Errorf("{deleteVPC} %s", fmt.Errorf(resp.Message)) + } + } + } + return r.deleteCRs(vpcCR, vpcMeta) +} + +func (r *VPCReconciler) deleteCRs(vpcCR *k8sv1alpha1.VPC, vpcMeta *k8sv1alpha1.VPCMeta) (ctrl.Result, error) { + if vpcMeta != nil { + _, err := r.deleteVPCMetaCR(vpcMeta) + if err != nil { + return ctrl.Result{}, fmt.Errorf("{deleteCRs} %s", err) + } + } + + return r.deleteVPCCR(vpcCR) +} + +func (r *VPCReconciler) deleteVPCCR(vpcCR *k8sv1alpha1.VPC) (ctrl.Result, error) { + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + vpcCR.ObjectMeta.SetFinalizers(nil) + vpcCR.SetFinalizers(nil) + if err := r.Update(ctx, vpcCR.DeepCopyObject(), &client.UpdateOptions{}); err != nil { + return ctrl.Result{}, fmt.Errorf("{deleteVPCCR} %s", err) + } + + return ctrl.Result{}, nil +} + +func (r *VPCReconciler) deleteVPCMetaCR(vpcMeta *k8sv1alpha1.VPCMeta) (ctrl.Result, error) { + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + if err := r.Delete(ctx, vpcMeta.DeepCopyObject(), &client.DeleteOptions{}); err != nil { + return ctrl.Result{}, fmt.Errorf("{deleteVPCMetaCR} %s", err) + } + + return ctrl.Result{}, nil +} + +// SetupWithManager Resources +func (r *VPCReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&k8sv1alpha1.VPC{}). + Complete(r) +} + diff --git a/controllers/vpc_translations.go b/controllers/vpc_translations.go new file mode 100644 index 0000000..8fa5f86 --- /dev/null +++ b/controllers/vpc_translations.go @@ -0,0 +1,194 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "fmt" + + k8sv1alpha1 "github.com/netrisai/netris-operator/api/v1alpha1" + "github.com/netrisai/netriswebapi/v2/types/vpc" + "github.com/r3labs/diff/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// VPCToVPCMeta converts the VPC resource to VPCMeta type and used for add the VPC for Netris API. +func (r *VPCReconciler) VPCToVPCMeta(vpcCR *k8sv1alpha1.VPC) (*k8sv1alpha1.VPCMeta, error) { + adminTenantID := 0 + if tenant, ok := r.NStorage.TenantsStorage.FindByName(vpcCR.Spec.AdminTenant); ok { + adminTenantID = tenant.ID + } else { + return nil, fmt.Errorf("'%s' admin tenant not found", vpcCR.Spec.AdminTenant) + } + + guestTenantIDs := []int{} + guestTenantNames := []string{} + for _, tenantName := range vpcCR.Spec.GuestTenants { + if tenant, ok := r.NStorage.TenantsStorage.FindByName(tenantName); ok { + guestTenantIDs = append(guestTenantIDs, tenant.ID) + guestTenantNames = append(guestTenantNames, tenant.Name) + } else { + return nil, fmt.Errorf("'%s' guest tenant not found", tenantName) + } + } + + imported := false + reclaim := false + if i, ok := vpcCR.GetAnnotations()["resource.k8s.netris.ai/import"]; ok && i == "true" { + imported = true + } + if i, ok := vpcCR.GetAnnotations()["resource.k8s.netris.ai/reclaimPolicy"]; ok && i == "retain" { + reclaim = true + } + + vpcMeta := &k8sv1alpha1.VPCMeta{ + ObjectMeta: metav1.ObjectMeta{ + Name: string(vpcCR.GetUID()), + Namespace: vpcCR.GetNamespace(), + }, + TypeMeta: metav1.TypeMeta{}, + Spec: k8sv1alpha1.VPCMetaSpec{ + Imported: imported, + Reclaim: reclaim, + Name: string(vpcCR.GetUID()), + VPCName: vpcCR.Name, + AdminTenant: vpcCR.Spec.AdminTenant, + AdminTenantID: adminTenantID, + GuestTenants: guestTenantNames, + GuestTenantIDs: guestTenantIDs, + Tags: vpcCR.Spec.Tags, + }, + } + + return vpcMeta, nil +} + +// VPCMetaToNetris converts the k8s VPC resource to Netris type and used for add the VPC for Netris API. +func (r *VPCMetaReconciler) VPCMetaToNetris(vpcMeta *k8sv1alpha1.VPCMeta) (*vpc.VPCw, error) { + adminTenant := vpc.AdminTenant{ID: vpcMeta.Spec.AdminTenantID, Name: vpcMeta.Spec.AdminTenant} + + guestTenants := []vpc.GuestTenant{} + for i, tenantID := range vpcMeta.Spec.GuestTenantIDs { + guestTenants = append(guestTenants, vpc.GuestTenant{ + ID: tenantID, + Name: vpcMeta.Spec.GuestTenants[i], + }) + } + + vpcAdd := &vpc.VPCw{ + Name: vpcMeta.Spec.VPCName, + AdminTenant: adminTenant, + GuestTenant: guestTenants, + Tags: vpcMeta.Spec.Tags, + } + + return vpcAdd, nil +} + +// VPCMetaToNetrisUpdate converts the k8s VPC resource to Netris type and used for update the VPC for Netris API. +func VPCMetaToNetrisUpdate(vpcMeta *k8sv1alpha1.VPCMeta) (*vpc.VPCw, error) { + adminTenant := vpc.AdminTenant{ID: vpcMeta.Spec.AdminTenantID, Name: vpcMeta.Spec.AdminTenant} + + guestTenants := []vpc.GuestTenant{} + for i, tenantID := range vpcMeta.Spec.GuestTenantIDs { + guestTenants = append(guestTenants, vpc.GuestTenant{ + ID: tenantID, + Name: vpcMeta.Spec.GuestTenants[i], + }) + } + + vpcUpdate := &vpc.VPCw{ + Name: vpcMeta.Spec.VPCName, + AdminTenant: adminTenant, + GuestTenant: guestTenants, + Tags: vpcMeta.Spec.Tags, + } + + return vpcUpdate, nil +} + +func compareVPCMetaAPIVPC(vpcMeta *k8sv1alpha1.VPCMeta, apiVPC *vpc.VPC) bool { + if vpcMeta.Spec.VPCName != apiVPC.Name { + return false + } + + if vpcMeta.Spec.AdminTenantID != apiVPC.AdminTenant.ID { + return false + } + + if ok := compareVPCMetaAPIVPCGuestTenants(vpcMeta.Spec.GuestTenantIDs, apiVPC.GuestTenant); !ok { + return false + } + + if ok := compareVPCMetaAPIVPCTags(vpcMeta.Spec.Tags, apiVPC.Tags); !ok { + return false + } + + return true +} + +func compareVPCMetaAPIVPCGuestTenants(vpcMetaTenantIDs []int, apiVPCTenants []vpc.GuestTenant) bool { + tenantIDList := []int{} + for _, tenant := range apiVPCTenants { + tenantIDList = append(tenantIDList, tenant.ID) + } + changelog, _ := diff.Diff(vpcMetaTenantIDs, tenantIDList) + return len(changelog) <= 0 +} + +func compareVPCMetaAPIVPCTags(vpcMetaTags []string, apiVPCTags []string) bool { + changelog, _ := diff.Diff(vpcMetaTags, apiVPCTags) + return len(changelog) <= 0 +} + +func vpcCompareFieldsForNewMeta(vpcCR *k8sv1alpha1.VPC, vpcMeta *k8sv1alpha1.VPCMeta) bool { + imported := false + reclaim := false + if i, ok := vpcCR.GetAnnotations()["resource.k8s.netris.ai/import"]; ok && i == "true" { + imported = true + } + if i, ok := vpcCR.GetAnnotations()["resource.k8s.netris.ai/reclaimPolicy"]; ok && i == "retain" { + reclaim = true + } + return vpcCR.GetGeneration() != vpcMeta.Spec.VPCCRGeneration || imported != vpcMeta.Spec.Imported || reclaim != vpcMeta.Spec.Reclaim +} + +func vpcMustUpdateAnnotations(vpcCR *k8sv1alpha1.VPC) bool { + update := false + if i, ok := vpcCR.GetAnnotations()["resource.k8s.netris.ai/import"]; !(ok && (i == "true" || i == "false")) { + update = true + } + if i, ok := vpcCR.GetAnnotations()["resource.k8s.netris.ai/reclaimPolicy"]; !(ok && (i == "retain" || i == "delete")) { + update = true + } + return update +} + +func vpcUpdateDefaultAnnotations(vpcCR *k8sv1alpha1.VPC) { + imported := "false" + reclaim := "delete" + if i, ok := vpcCR.GetAnnotations()["resource.k8s.netris.ai/import"]; ok && i == "true" { + imported = "true" + } + if i, ok := vpcCR.GetAnnotations()["resource.k8s.netris.ai/reclaimPolicy"]; ok && i == "retain" { + reclaim = "retain" + } + annotations := vpcCR.GetAnnotations() + annotations["resource.k8s.netris.ai/import"] = imported + annotations["resource.k8s.netris.ai/reclaimPolicy"] = reclaim + vpcCR.SetAnnotations(annotations) +} + diff --git a/controllers/vpcmeta_controller.go b/controllers/vpcmeta_controller.go new file mode 100644 index 0000000..ff3f75b --- /dev/null +++ b/controllers/vpcmeta_controller.go @@ -0,0 +1,246 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + "time" + + "go.uber.org/zap/zapcore" + "k8s.io/apimachinery/pkg/api/errors" + + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + k8sv1alpha1 "github.com/netrisai/netris-operator/api/v1alpha1" + "github.com/netrisai/netris-operator/netrisstorage" + "github.com/netrisai/netriswebapi/http" + api "github.com/netrisai/netriswebapi/v2" + "github.com/netrisai/netriswebapi/v2/types/vpc" +) + +// VPCMetaReconciler reconciles a VPCMeta object +type VPCMetaReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + Cred *api.Clientset + NStorage *netrisstorage.Storage +} + +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=vpcmeta,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=vpcmeta/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=vpcmeta/finalizers,verbs=update + +// Reconcile . +func (r *VPCMetaReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + debugLogger := r.Log.WithValues("name", req.NamespacedName).V(int(zapcore.WarnLevel)) + + vpcMeta := &k8sv1alpha1.VPCMeta{} + vpcCR := &k8sv1alpha1.VPC{} + vpcMetaCtx, vpcMetaCancel := context.WithTimeout(cntxt, contextTimeout) + defer vpcMetaCancel() + if err := r.Get(vpcMetaCtx, req.NamespacedName, vpcMeta); err != nil { + if errors.IsNotFound(err) { + debugLogger.Info(err.Error()) + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + logger := r.Log.WithValues("name", fmt.Sprintf("%s/%s", req.NamespacedName.Namespace, vpcMeta.Spec.VPCName)) + debugLogger = logger.V(int(zapcore.WarnLevel)) + + u := uniReconciler{ + Client: r.Client, + Logger: logger, + DebugLogger: debugLogger, + Cred: r.Cred, + NStorage: r.NStorage, + } + + provisionState := "Active" + + vpcNN := req.NamespacedName + vpcNN.Name = vpcMeta.Spec.VPCName + vpcNNCtx, vpcNNCancel := context.WithTimeout(cntxt, contextTimeout) + defer vpcNNCancel() + if err := r.Get(vpcNNCtx, vpcNN, vpcCR); err != nil { + if errors.IsNotFound(err) { + debugLogger.Info(err.Error()) + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + if vpcMeta.DeletionTimestamp != nil { + return ctrl.Result{}, nil + } + + if vpcMeta.Spec.ID == 0 { + debugLogger.Info("ID Not found in meta") + if vpcMeta.Spec.Imported { + logger.Info("Importing vpc") + debugLogger.Info("Imported yaml mode. Finding VPC by name") + if vpc, ok := r.NStorage.VPCStorage.FindByName(vpcMeta.Spec.VPCName); ok { + debugLogger.Info("Imported yaml mode. VPC found") + vpcMeta.Spec.ID = vpc.ID + vpcMeta.Spec.AdminTenantID = vpc.AdminTenant.ID + vpcMeta.Spec.AdminTenant = vpc.AdminTenant.Name + guestTenantIDs := []int{} + guestTenantNames := []string{} + for _, tenant := range vpc.GuestTenant { + guestTenantIDs = append(guestTenantIDs, tenant.ID) + guestTenantNames = append(guestTenantNames, tenant.Name) + } + vpcMeta.Spec.GuestTenantIDs = guestTenantIDs + vpcMeta.Spec.GuestTenants = guestTenantNames + vpcMeta.Spec.Tags = vpc.Tags + vpcMeta.Spec.IsSystem = false // VPC API doesn't expose IsSystem in response + vpcMeta.Spec.IsDefault = vpc.IsDefault + vpcMeta.Spec.VNI = 0 // VPC API doesn't expose VNI in response + vpcCR.Status.ModifiedDate = metav1.NewTime(time.Unix(int64(vpc.ModifiedDate/1000), 0)) + vpcMetaPatchCtx, vpcMetaPatchCancel := context.WithTimeout(cntxt, contextTimeout) + defer vpcMetaPatchCancel() + err := r.Patch(vpcMetaPatchCtx, vpcMeta.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{patch vpcmeta.Spec.ID} %s", err), "") + return u.patchVPCStatus(vpcCR, "Failure", err.Error()) + } + debugLogger.Info("Imported yaml mode. ID patched") + logger.Info("VPC imported") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + logger.Info("VPC not found for import") + debugLogger.Info("Imported yaml mode. VPC not found") + } + + logger.Info("Creating VPC") + if _, err, errMsg := r.createVPC(vpcMeta); err != nil { + logger.Error(fmt.Errorf("{createVPC} %s", err), "") + return u.patchVPCStatus(vpcCR, "Failure", errMsg.Error()) + } + logger.Info("VPC Created") + } else { + apiVPC, _ := r.Cred.VPC().GetByID(vpcMeta.Spec.ID) + if apiVPC == nil { + debugLogger.Info("VPC not found in Netris") + debugLogger.Info("Going to create VPC") + logger.Info("Creating VPC") + if _, err, errMsg := r.createVPC(vpcMeta); err != nil { + logger.Error(fmt.Errorf("{createVPC} %s", err), "") + return u.patchVPCStatus(vpcCR, "Failure", errMsg.Error()) + } + logger.Info("VPC Created") + } else { + vpcCR.Status.ModifiedDate = metav1.NewTime(time.Unix(int64(apiVPC.ModifiedDate/1000), 0)) + debugLogger.Info("Comparing VPCMeta with Netris VPC") + if ok := compareVPCMetaAPIVPC(vpcMeta, apiVPC); ok { + debugLogger.Info("Nothing Changed") + } else { + debugLogger.Info("Something changed") + debugLogger.Info("Go to update VPC in Netris") + logger.Info("Updating VPC") + updateVPC, err := VPCMetaToNetrisUpdate(vpcMeta) + if err != nil { + logger.Error(fmt.Errorf("{VPCMetaToNetrisUpdate} %s", err), "") + return u.patchVPCStatus(vpcCR, "Failure", err.Error()) + } + _, err, errMsg := r.updateVPC(vpcMeta.Spec.ID, updateVPC) + if err != nil { + logger.Error(fmt.Errorf("{updateVPC} %s", err), "") + return u.patchVPCStatus(vpcCR, "Failure", errMsg.Error()) + } + logger.Info("VPC Updated") + } + } + } + return u.patchVPCStatus(vpcCR, provisionState, "Success") +} + +// SetupWithManager . +func (r *VPCMetaReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&k8sv1alpha1.VPCMeta{}). + Complete(r) +} + +func (r *VPCMetaReconciler) createVPC(vpcMeta *k8sv1alpha1.VPCMeta) (ctrl.Result, error, error) { + debugLogger := r.Log.WithValues( + "name", fmt.Sprintf("%s/%s", vpcMeta.Namespace, vpcMeta.Spec.VPCName), + "vpcName", vpcMeta.Spec.VPCName, + ).V(int(zapcore.WarnLevel)) + + vpcAdd, err := r.VPCMetaToNetris(vpcMeta) + if err != nil { + return ctrl.Result{}, err, err + } + reply, err := r.Cred.VPC().Add(vpcAdd) + if err != nil { + return ctrl.Result{}, err, err + } + resp, err := http.ParseAPIResponse(reply.Data) + if err != nil { + return ctrl.Result{}, err, err + } + if !resp.IsSuccess { + return ctrl.Result{}, fmt.Errorf(resp.Message), fmt.Errorf(resp.Message) + } + + idStruct := struct { + ID int `json:"id"` + }{} + err = http.Decode(resp.Data, &idStruct) + if err != nil { + return ctrl.Result{}, err, err + } + + debugLogger.Info("VPC Created", "id", idStruct.ID) + + vpcMeta.Spec.ID = idStruct.ID + + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + err = r.Patch(ctx, vpcMeta.DeepCopyObject(), client.Merge, &client.PatchOptions{}) // requeue + if err != nil { + return ctrl.Result{}, err, err + } + + debugLogger.Info("ID patched to meta", "id", idStruct.ID) + return ctrl.Result{}, nil, nil +} + +func (r *VPCMetaReconciler) updateVPC(id int, vpc *vpc.VPCw) (ctrl.Result, error, error) { + reply, err := r.Cred.VPC().Update(id, vpc) + if err != nil { + return ctrl.Result{}, fmt.Errorf("{updateVPC} %s", err), err + } + resp, err := http.ParseAPIResponse(reply.Data) + if err != nil { + return ctrl.Result{}, err, err + } + if !resp.IsSuccess { + return ctrl.Result{}, fmt.Errorf("{updateVPC} %s", fmt.Errorf(resp.Message)), fmt.Errorf(resp.Message) + } + + return ctrl.Result{}, nil, nil +} + diff --git a/deploy/charts/netris-operator/crds/k8s.netris.ai_vpcmeta.yaml b/deploy/charts/netris-operator/crds/k8s.netris.ai_vpcmeta.yaml new file mode 100644 index 0000000..0fee7b3 --- /dev/null +++ b/deploy/charts/netris-operator/crds/k8s.netris.ai_vpcmeta.yaml @@ -0,0 +1,101 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: vpcmeta.k8s.netris.ai +spec: + group: k8s.netris.ai + names: + kind: VPCMeta + listKind: VPCMetaList + plural: vpcmeta + singular: vpcmeta + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: VPCMeta is the Schema for the vpcmeta API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: VPCMetaSpec defines the desired state of VPCMeta + properties: + adminTenant: + type: string + adminTenantId: + type: integer + guestTenantIds: + items: + type: integer + type: array + guestTenants: + items: + type: string + type: array + id: + type: integer + imported: + type: boolean + isDefault: + type: boolean + isSystem: + type: boolean + name: + type: string + reclaimPolicy: + type: boolean + tags: + items: + type: string + type: array + vni: + type: integer + vpcGeneration: + format: int64 + type: integer + vpcName: + type: string + required: + - adminTenant + - adminTenantId + - guestTenantIds + - guestTenants + - id + - imported + - name + - reclaimPolicy + - tags + - vpcGeneration + - vpcName + type: object + status: + description: VPCMetaStatus defines the observed state of VPCMeta + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/deploy/charts/netris-operator/crds/k8s.netris.ai_vpcs.yaml b/deploy/charts/netris-operator/crds/k8s.netris.ai_vpcs.yaml new file mode 100644 index 0000000..83e7ed2 --- /dev/null +++ b/deploy/charts/netris-operator/crds/k8s.netris.ai_vpcs.yaml @@ -0,0 +1,97 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: vpcs.k8s.netris.ai +spec: + group: k8s.netris.ai + names: + kind: VPC + listKind: VPCList + plural: vpcs + singular: vpc + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.adminTenant + name: Admin Tenant + type: string + - jsonPath: .spec.guestTenants + name: Guest Tenants + priority: 1 + type: string + - jsonPath: .status.status + name: Status + type: string + - jsonPath: .status.modified + name: Modified + priority: 1 + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: VPC is the Schema for the vpcs API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: VPCSpec . + properties: + adminTenant: + type: string + guestTenants: + items: + type: string + type: array + tags: + items: + type: string + type: array + required: + - adminTenant + type: object + status: + description: VPCStatus defines the observed state of VPC + properties: + message: + type: string + modified: + format: date-time + type: string + status: + description: 'INSERT ADDITIONAL STATUS FIELD - define observed state + of cluster Important: Run "make" to regenerate code after modifying + this file' + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/deploy/charts/netris-operator/templates/rbac.yaml b/deploy/charts/netris-operator/templates/rbac.yaml index ed276df..c75b6a5 100644 --- a/deploy/charts/netris-operator/templates/rbac.yaml +++ b/deploy/charts/netris-operator/templates/rbac.yaml @@ -700,6 +700,58 @@ rules: - get - patch - update + - apiGroups: + - k8s.netris.ai + resources: + - vpcmeta + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - k8s.netris.ai + resources: + - vpcmeta/finalizers + verbs: + - update + - apiGroups: + - k8s.netris.ai + resources: + - vpcmeta/status + verbs: + - get + - patch + - update + - apiGroups: + - k8s.netris.ai + resources: + - vpcs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - k8s.netris.ai + resources: + - vpcs/finalizers + verbs: + - update + - apiGroups: + - k8s.netris.ai + resources: + - vpcs/status + verbs: + - get + - patch + - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/main.go b/main.go index 4170b97..25d9168 100644 --- a/main.go +++ b/main.go @@ -126,6 +126,27 @@ func main() { os.Exit(1) } + if err = (&controllers.VPCReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("VPC"), + Scheme: mgr.GetScheme(), + Cred: cred, + NStorage: nStorage, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "VPC") + os.Exit(1) + } + if err = (&controllers.VPCMetaReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("VPCMeta"), + Scheme: mgr.GetScheme(), + Cred: cred, + NStorage: nStorage, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "VPCMeta") + os.Exit(1) + } + if err = (&controllers.BGPReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("BGP"), diff --git a/samples/kustomization.yaml b/samples/kustomization.yaml index 4d011da..ad08b38 100644 --- a/samples/kustomization.yaml +++ b/samples/kustomization.yaml @@ -8,6 +8,7 @@ resources: - switch.yaml - controller.yaml - vnet.yaml + - vpc.yaml - l4lb.yaml - bgp.yaml - link.yaml diff --git a/samples/vpc.yaml b/samples/vpc.yaml new file mode 100644 index 0000000..0a73e41 --- /dev/null +++ b/samples/vpc.yaml @@ -0,0 +1,13 @@ +apiVersion: k8s.netris.ai/v1alpha1 +kind: VPC +metadata: + name: my-vpc +spec: + adminTenant: Admin + guestTenants: + - Unix + - Windows + tags: + - production + - network + From 9e585bc1e0bb32ae024e2a6bb8f8d73ae4bc19ab Mon Sep 17 00:00:00 2001 From: Vasyl Saienko Date: Thu, 27 Nov 2025 18:48:25 +0200 Subject: [PATCH 2/6] Fix null tags for vpc --- controllers/vpc_translations.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/controllers/vpc_translations.go b/controllers/vpc_translations.go index 8fa5f86..693df3f 100644 --- a/controllers/vpc_translations.go +++ b/controllers/vpc_translations.go @@ -69,7 +69,7 @@ func (r *VPCReconciler) VPCToVPCMeta(vpcCR *k8sv1alpha1.VPC) (*k8sv1alpha1.VPCMe AdminTenantID: adminTenantID, GuestTenants: guestTenantNames, GuestTenantIDs: guestTenantIDs, - Tags: vpcCR.Spec.Tags, + Tags: normalizeVPCTags(vpcCR.Spec.Tags), }, } @@ -92,7 +92,7 @@ func (r *VPCMetaReconciler) VPCMetaToNetris(vpcMeta *k8sv1alpha1.VPCMeta) (*vpc. Name: vpcMeta.Spec.VPCName, AdminTenant: adminTenant, GuestTenant: guestTenants, - Tags: vpcMeta.Spec.Tags, + Tags: normalizeVPCTags(vpcMeta.Spec.Tags), } return vpcAdd, nil @@ -114,7 +114,7 @@ func VPCMetaToNetrisUpdate(vpcMeta *k8sv1alpha1.VPCMeta) (*vpc.VPCw, error) { Name: vpcMeta.Spec.VPCName, AdminTenant: adminTenant, GuestTenant: guestTenants, - Tags: vpcMeta.Spec.Tags, + Tags: normalizeVPCTags(vpcMeta.Spec.Tags), } return vpcUpdate, nil @@ -150,10 +150,19 @@ func compareVPCMetaAPIVPCGuestTenants(vpcMetaTenantIDs []int, apiVPCTenants []vp } func compareVPCMetaAPIVPCTags(vpcMetaTags []string, apiVPCTags []string) bool { - changelog, _ := diff.Diff(vpcMetaTags, apiVPCTags) + normalizedMetaTags := normalizeVPCTags(vpcMetaTags) + normalizedAPITags := normalizeVPCTags(apiVPCTags) + changelog, _ := diff.Diff(normalizedMetaTags, normalizedAPITags) return len(changelog) <= 0 } +func normalizeVPCTags(tags []string) []string { + if tags == nil { + return []string{} + } + return tags +} + func vpcCompareFieldsForNewMeta(vpcCR *k8sv1alpha1.VPC, vpcMeta *k8sv1alpha1.VPCMeta) bool { imported := false reclaim := false From a01a9d6e1656f71dd3d23f0e727fb41051a17de9 Mon Sep 17 00:00:00 2001 From: Vasyl Saienko Date: Fri, 28 Nov 2025 09:25:01 +0200 Subject: [PATCH 3/6] Allow to use 64 ports switches --- api/v1alpha1/switch_types.go | 2 +- config/crd/bases/k8s.netris.ai_switches.yaml | 1 + deploy/charts/netris-operator/crds/k8s.netris.ai_switches.yaml | 1 + samples/README.md | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/api/v1alpha1/switch_types.go b/api/v1alpha1/switch_types.go index f87bdbe..8d8f346 100644 --- a/api/v1alpha1/switch_types.go +++ b/api/v1alpha1/switch_types.go @@ -44,7 +44,7 @@ type SwitchSpec struct { // +kubebuilder:validation:Pattern=`^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$` MgmtIP string `json:"mgmtIp,omitempty"` - // +kubebuilder:validation:Enum=16;32;48;54;56 + // +kubebuilder:validation:Enum=16;32;48;54;56;64 PortsCount int `json:"portsCount,omitempty"` // +kubebuilder:validation:Pattern=`^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$` diff --git a/config/crd/bases/k8s.netris.ai_switches.yaml b/config/crd/bases/k8s.netris.ai_switches.yaml index e71115b..d986f92 100644 --- a/config/crd/bases/k8s.netris.ai_switches.yaml +++ b/config/crd/bases/k8s.netris.ai_switches.yaml @@ -96,6 +96,7 @@ spec: - 48 - 54 - 56 + - 64 type: integer profile: type: string diff --git a/deploy/charts/netris-operator/crds/k8s.netris.ai_switches.yaml b/deploy/charts/netris-operator/crds/k8s.netris.ai_switches.yaml index e71115b..d986f92 100644 --- a/deploy/charts/netris-operator/crds/k8s.netris.ai_switches.yaml +++ b/deploy/charts/netris-operator/crds/k8s.netris.ai_switches.yaml @@ -96,6 +96,7 @@ spec: - 48 - 54 - 56 + - 64 type: integer profile: type: string diff --git a/samples/README.md b/samples/README.md index 4015d1b..59c49b9 100644 --- a/samples/README.md +++ b/samples/README.md @@ -96,7 +96,7 @@ Ref | Attribute | Default | Description [6] | profile | "" | Optional. An inventory profile name to define global configuration (NTP, DNS, timezone, etc…). [7] | mainIp | automatically | Optional. A unique IP address which will be used as a loopback address of this unit. If `mainIp` key isn't set the controller will assign automatically from subnets with relevant purpose. [8] | mgmtIp | automatically | Optional. A unique IP address to be used on out of band management interface. If `mgmtIp` key isn't set the controller will assign automatically from subnets with relevant purpose. -[9] | portsCount | nil | Preliminary port count is used for definition of topology. Possible values: `16`, `32`, `48`, `54`, `56`. +[9] | portsCount | nil | Preliminary port count is used for definition of topology. Possible values: `16`, `32`, `48`, `54`, `56`, `64`. ### Softgate Attributes From 5436a590964ba07b78f94f1ae726270779a9b2e4 Mon Sep 17 00:00:00 2001 From: Vasyl Saienko Date: Thu, 27 Nov 2025 13:54:15 +0200 Subject: [PATCH 4/6] Add server support The patch impements server crd similar to switch and softgate to have ability create inventory servers via kubernetes crds. --- api/v1alpha1/server_types.go | 90 ++++ api/v1alpha1/servermeta_types.go | 80 ++++ api/v1alpha1/zz_generated.deepcopy.go | 188 ++++++++ .../crd/bases/k8s.netris.ai_servermeta.yaml | 97 ++++ config/crd/bases/k8s.netris.ai_servers.yaml | 109 +++++ config/rbac/role.yaml | 68 ++- controllers/controller.go | 26 + controllers/server_controller.go | 234 +++++++++ controllers/server_translations.go | 345 +++++++++++++ controllers/servermeta_controller.go | 453 ++++++++++++++++++ .../crds/k8s.netris.ai_servermeta.yaml | 97 ++++ .../crds/k8s.netris.ai_servers.yaml | 109 +++++ .../netris-operator/templates/rbac.yaml | 52 ++ main.go | 20 + netrisstorage/hws.go | 37 ++ samples/kustomization.yaml | 1 + samples/server.yaml | 71 +++ 17 files changed, 2069 insertions(+), 8 deletions(-) create mode 100644 api/v1alpha1/server_types.go create mode 100644 api/v1alpha1/servermeta_types.go create mode 100644 config/crd/bases/k8s.netris.ai_servermeta.yaml create mode 100644 config/crd/bases/k8s.netris.ai_servers.yaml create mode 100644 controllers/server_controller.go create mode 100644 controllers/server_translations.go create mode 100644 controllers/servermeta_controller.go create mode 100644 deploy/charts/netris-operator/crds/k8s.netris.ai_servermeta.yaml create mode 100644 deploy/charts/netris-operator/crds/k8s.netris.ai_servers.yaml create mode 100644 samples/server.yaml diff --git a/api/v1alpha1/server_types.go b/api/v1alpha1/server_types.go new file mode 100644 index 0000000..348fb1b --- /dev/null +++ b/api/v1alpha1/server_types.go @@ -0,0 +1,90 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ServerSpec defines the desired state of Server +type ServerSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + Tenant string `json:"tenant,omitempty"` + Description string `json:"description,omitempty"` + Site string `json:"site,omitempty"` + Profile string `json:"profile,omitempty"` + + // +kubebuilder:validation:Pattern=`^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$` + MainIP string `json:"mainIp,omitempty"` + + // +kubebuilder:validation:Pattern=`^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$` + MgmtIP string `json:"mgmtIp,omitempty"` + + UUID string `json:"uuid,omitempty"` + ASN int `json:"asn,omitempty"` + PortCount int `json:"portsCount,omitempty"` + CustomData string `json:"customData,omitempty"` + Tags []string `json:"tags,omitempty"` + SRVRole string `json:"srvRole,omitempty"` +} + +// ServerStatus defines the observed state of Server +type ServerStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + + Status string `json:"status,omitempty"` + Message string `json:"message,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Tenant",type=string,JSONPath=`.spec.tenant` +// +kubebuilder:printcolumn:name="Site",type=string,JSONPath=`.spec.site` +// +kubebuilder:printcolumn:name="Profile",type=string,JSONPath=`.spec.profile` +// +kubebuilder:printcolumn:name="Main IP",type=string,JSONPath=`.spec.mainIp` +// +kubebuilder:printcolumn:name="Management IP",type=string,JSONPath=`.spec.mgmtIp` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// Server is the Schema for the servers API +type Server struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ServerSpec `json:"spec,omitempty"` + Status ServerStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ServerList contains a list of Server +type ServerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Server `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Server{}, &ServerList{}) +} + diff --git a/api/v1alpha1/servermeta_types.go b/api/v1alpha1/servermeta_types.go new file mode 100644 index 0000000..cdc0ff7 --- /dev/null +++ b/api/v1alpha1/servermeta_types.go @@ -0,0 +1,80 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ServerMetaSpec defines the desired state of ServerMeta +type ServerMetaSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + Imported bool `json:"imported"` + Reclaim bool `json:"reclaimPolicy"` + ServerCRGeneration int64 `json:"serverGeneration"` + ID int `json:"id"` + ServerName string `json:"serverName"` + + TenantID int `json:"tenantid,omitempty"` + Description string `json:"description,omitempty"` + SiteID int `json:"siteid,omitempty"` + ProfileID int `json:"profileid,omitempty"` + MainIP string `json:"mainIp,omitempty"` + MgmtIP string `json:"mgmtIp,omitempty"` + UUID string `json:"uuid,omitempty"` + ASN int `json:"asn,omitempty"` + PortCount int `json:"portsCount,omitempty"` + CustomData string `json:"customData,omitempty"` + Tags []string `json:"tags,omitempty"` + SRVRole string `json:"srvRole,omitempty"` +} + +// ServerMetaStatus defines the observed state of ServerMeta +type ServerMetaStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// ServerMeta is the Schema for the servermeta API +type ServerMeta struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ServerMetaSpec `json:"spec,omitempty"` + Status ServerMetaStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ServerMetaList contains a list of ServerMeta +type ServerMetaList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ServerMeta `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ServerMeta{}, &ServerMetaList{}) +} + diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index d88cc99..67f786b 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1863,6 +1863,194 @@ func (in *SoftgateStatus) DeepCopy() *SoftgateStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Server) DeepCopyInto(out *Server) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Server. +func (in *Server) DeepCopy() *Server { + if in == nil { + return nil + } + out := new(Server) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Server) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerList) DeepCopyInto(out *ServerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Server, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerList. +func (in *ServerList) DeepCopy() *ServerList { + if in == nil { + return nil + } + out := new(ServerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerMeta) DeepCopyInto(out *ServerMeta) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerMeta. +func (in *ServerMeta) DeepCopy() *ServerMeta { + if in == nil { + return nil + } + out := new(ServerMeta) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServerMeta) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerMetaList) DeepCopyInto(out *ServerMetaList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ServerMeta, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerMetaList. +func (in *ServerMetaList) DeepCopy() *ServerMetaList { + if in == nil { + return nil + } + out := new(ServerMetaList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServerMetaList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerMetaSpec) DeepCopyInto(out *ServerMetaSpec) { + *out = *in + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerMetaSpec. +func (in *ServerMetaSpec) DeepCopy() *ServerMetaSpec { + if in == nil { + return nil + } + out := new(ServerMetaSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerMetaStatus) DeepCopyInto(out *ServerMetaStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerMetaStatus. +func (in *ServerMetaStatus) DeepCopy() *ServerMetaStatus { + if in == nil { + return nil + } + out := new(ServerMetaStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerSpec) DeepCopyInto(out *ServerSpec) { + *out = *in + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerSpec. +func (in *ServerSpec) DeepCopy() *ServerSpec { + if in == nil { + return nil + } + out := new(ServerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerStatus) DeepCopyInto(out *ServerStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerStatus. +func (in *ServerStatus) DeepCopy() *ServerStatus { + if in == nil { + return nil + } + out := new(ServerStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Subnet) DeepCopyInto(out *Subnet) { *out = *in diff --git a/config/crd/bases/k8s.netris.ai_servermeta.yaml b/config/crd/bases/k8s.netris.ai_servermeta.yaml new file mode 100644 index 0000000..2f34ac1 --- /dev/null +++ b/config/crd/bases/k8s.netris.ai_servermeta.yaml @@ -0,0 +1,97 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: servermeta.k8s.netris.ai +spec: + group: k8s.netris.ai + names: + kind: ServerMeta + listKind: ServerMetaList + plural: servermeta + singular: servermeta + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ServerMeta is the Schema for the servermeta API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ServerMetaSpec defines the desired state of ServerMeta + properties: + asn: + type: integer + customData: + type: string + description: + type: string + id: + type: integer + imported: + type: boolean + mainIp: + type: string + mgmtIp: + type: string + portsCount: + type: integer + profileid: + type: integer + reclaimPolicy: + type: boolean + serverGeneration: + format: int64 + type: integer + serverName: + type: string + siteid: + type: integer + srvRole: + type: string + tags: + items: + type: string + type: array + tenantid: + type: integer + uuid: + type: string + required: + - id + - imported + - reclaimPolicy + - serverGeneration + - serverName + type: object + status: + description: ServerMetaStatus defines the observed state of ServerMeta + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/config/crd/bases/k8s.netris.ai_servers.yaml b/config/crd/bases/k8s.netris.ai_servers.yaml new file mode 100644 index 0000000..b695f35 --- /dev/null +++ b/config/crd/bases/k8s.netris.ai_servers.yaml @@ -0,0 +1,109 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: servers.k8s.netris.ai +spec: + group: k8s.netris.ai + names: + kind: Server + listKind: ServerList + plural: servers + singular: server + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.tenant + name: Tenant + type: string + - jsonPath: .spec.site + name: Site + type: string + - jsonPath: .spec.profile + name: Profile + type: string + - jsonPath: .spec.mainIp + name: Main IP + type: string + - jsonPath: .spec.mgmtIp + name: Management IP + type: string + - jsonPath: .status.status + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Server is the Schema for the servers API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ServerSpec defines the desired state of Server + properties: + asn: + type: integer + customData: + type: string + description: + type: string + mainIp: + pattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$ + type: string + mgmtIp: + pattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$ + type: string + portsCount: + type: integer + profile: + type: string + site: + type: string + srvRole: + type: string + tags: + items: + type: string + type: array + tenant: + type: string + uuid: + type: string + type: object + status: + description: ServerStatus defines the observed state of Server + properties: + message: + type: string + status: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index f995265..9a079ec 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -541,14 +541,66 @@ rules: - k8s.netris.ai resources: - softgates/status - verbs: - - get - - patch - - update -- apiGroups: - - k8s.netris.ai - resources: - - subnetmeta + verbs: + - get + - patch + - update + - apiGroups: + - k8s.netris.ai + resources: + - servermeta + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - k8s.netris.ai + resources: + - servermeta/finalizers + verbs: + - update + - apiGroups: + - k8s.netris.ai + resources: + - servermeta/status + verbs: + - get + - patch + - update + - apiGroups: + - k8s.netris.ai + resources: + - servers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - k8s.netris.ai + resources: + - servers/finalizers + verbs: + - update + - apiGroups: + - k8s.netris.ai + resources: + - servers/status + verbs: + - get + - patch + - update + - apiGroups: + - k8s.netris.ai + resources: + - subnetmeta verbs: - create - delete diff --git a/controllers/controller.go b/controllers/controller.go index 4beacf6..d612b05 100644 --- a/controllers/controller.go +++ b/controllers/controller.go @@ -336,3 +336,29 @@ func (u *uniReconciler) patchVPCStatus(vpc *k8sv1alpha1.VPC, status, message str } return ctrl.Result{RequeueAfter: requeueInterval}, nil } + +func (u *uniReconciler) patchServerStatus(server *k8sv1alpha1.Server, status, message string) (ctrl.Result, error) { + u.DebugLogger.Info("Patching Status", "status", status, "message", message) + + server.Status.Status = status + server.Status.Message = message + + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + err := u.Status().Patch(ctx, server.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + u.DebugLogger.Info("{r.Status().Patch}", "error", err, "action", "status update") + } + return ctrl.Result{RequeueAfter: requeueInterval}, nil +} + +func (u *uniReconciler) patchServer(server *k8sv1alpha1.Server) (ctrl.Result, error) { + u.DebugLogger.Info("Patching") + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + err := u.Patch(ctx, server.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + u.DebugLogger.Info("{r.Patch()}", "error", err) + } + return ctrl.Result{RequeueAfter: requeueInterval}, nil +} diff --git a/controllers/server_controller.go b/controllers/server_controller.go new file mode 100644 index 0000000..f5f4f59 --- /dev/null +++ b/controllers/server_controller.go @@ -0,0 +1,234 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + + "go.uber.org/zap/zapcore" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/go-logr/logr" + k8sv1alpha1 "github.com/netrisai/netris-operator/api/v1alpha1" + "github.com/netrisai/netris-operator/netrisstorage" + "github.com/netrisai/netriswebapi/http" + api "github.com/netrisai/netriswebapi/v2" +) + +// ServerReconciler reconciles a Server object +type ServerReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + Cred *api.Clientset + NStorage *netrisstorage.Storage +} + +//+kubebuilder:rbac:groups=k8s.netris.ai,resources=servers,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=k8s.netris.ai,resources=servers/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=k8s.netris.ai,resources=servers/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the Server object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.9.2/pkg/reconcile +func (r *ServerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + logger := r.Log.WithValues("name", req.NamespacedName) + debugLogger := logger.V(int(zapcore.WarnLevel)) + server := &k8sv1alpha1.Server{} + + u := uniReconciler{ + Client: r.Client, + Logger: logger, + DebugLogger: debugLogger, + Cred: r.Cred, + NStorage: r.NStorage, + } + + serverCtx, serverCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverCancel() + if err := r.Get(serverCtx, req.NamespacedName, server); err != nil { + if errors.IsNotFound(err) { + debugLogger.Info(err.Error()) + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + serverMetaNamespaced := req.NamespacedName + serverMetaNamespaced.Name = string(server.GetUID()) + serverMeta := &k8sv1alpha1.ServerMeta{} + metaFound := true + + serverMetaCtx, serverMetaCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverMetaCancel() + if err := r.Get(serverMetaCtx, serverMetaNamespaced, serverMeta); err != nil { + if errors.IsNotFound(err) { + debugLogger.Info(err.Error()) + metaFound = false + serverMeta = nil + } else { + return ctrl.Result{}, err + } + } + + if server.DeletionTimestamp != nil { + logger.Info("Go to delete") + _, err := r.deleteServer(server, serverMeta) + if err != nil { + logger.Error(fmt.Errorf("{deleteServer} %s", err), "") + return u.patchServerStatus(server, "Failure", err.Error()) + } + logger.Info("Server deleted") + return ctrl.Result{}, nil + } + + if serverMustUpdateAnnotations(server) { + debugLogger.Info("Setting default annotations") + serverUpdateDefaultAnnotations(server) + serverPatchCtx, serverPatchCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverPatchCancel() + err := r.Patch(serverPatchCtx, server.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{Patch Server default annotations} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + return ctrl.Result{}, nil + } + + if metaFound { + debugLogger.Info("Meta found") + if serverCompareFieldsForNewMeta(server, serverMeta) { + debugLogger.Info("Generating New Meta") + serverID := serverMeta.Spec.ID + newServerMeta, err := r.ServerToServerMeta(server) + if err != nil { + logger.Error(fmt.Errorf("{ServerToServerMeta} %s", err), "") + return u.patchServerStatus(server, "Failure", err.Error()) + } + serverMeta.Spec = newServerMeta.DeepCopy().Spec + serverMeta.Spec.ID = serverID + serverMeta.Spec.ServerCRGeneration = server.GetGeneration() + + serverMetaUpdateCtx, serverMetaUpdateCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverMetaUpdateCancel() + err = r.Update(serverMetaUpdateCtx, serverMeta.DeepCopyObject(), &client.UpdateOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{serverMeta Update} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + } + } else { + debugLogger.Info("Meta not found") + if server.GetFinalizers() == nil { + server.SetFinalizers([]string{"resource.k8s.netris.ai/delete"}) + + serverPatchCtx, serverPatchCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverPatchCancel() + err := r.Patch(serverPatchCtx, server.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{Patch Server Finalizer} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + return ctrl.Result{}, nil + } + + serverMeta, err := r.ServerToServerMeta(server) + if err != nil { + logger.Error(fmt.Errorf("{ServerToServerMeta} %s", err), "") + return u.patchServerStatus(server, "Failure", err.Error()) + } + + serverMeta.Spec.ServerCRGeneration = server.GetGeneration() + + serverMetaCreateCtx, serverMetaCreateCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverMetaCreateCancel() + if err := r.Create(serverMetaCreateCtx, serverMeta.DeepCopyObject(), &client.CreateOptions{}); err != nil { + logger.Error(fmt.Errorf("{serverMeta Create} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + } + + return ctrl.Result{RequeueAfter: requeueInterval}, nil +} + +func (r *ServerReconciler) deleteServer(server *k8sv1alpha1.Server, serverMeta *k8sv1alpha1.ServerMeta) (ctrl.Result, error) { + if serverMeta != nil && serverMeta.Spec.ID > 0 && !serverMeta.Spec.Reclaim { + reply, err := r.Cred.Inventory().Delete("server", serverMeta.Spec.ID) + if err != nil { + return ctrl.Result{}, fmt.Errorf("{deleteServer} %s", err) + } + resp, err := http.ParseAPIResponse(reply.Data) + if err != nil { + return ctrl.Result{}, err + } + if !resp.IsSuccess && resp.Meta.StatusCode != 404 { + return ctrl.Result{}, fmt.Errorf("{deleteServer} %s", fmt.Errorf(resp.Message)) + } + } + return r.deleteCRs(server, serverMeta) +} + +func (r *ServerReconciler) deleteCRs(server *k8sv1alpha1.Server, serverMeta *k8sv1alpha1.ServerMeta) (ctrl.Result, error) { + if serverMeta != nil { + _, err := r.deleteServerMetaCR(serverMeta) + if err != nil { + return ctrl.Result{}, fmt.Errorf("{deleteCRs} %s", err) + } + } + + return r.deleteServerCR(server) +} + +func (r *ServerReconciler) deleteServerCR(server *k8sv1alpha1.Server) (ctrl.Result, error) { + server.ObjectMeta.SetFinalizers(nil) + server.SetFinalizers(nil) + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + if err := r.Update(ctx, server.DeepCopyObject(), &client.UpdateOptions{}); err != nil { + return ctrl.Result{}, fmt.Errorf("{deleteServerCR} %s", err) + } + + return ctrl.Result{}, nil +} + +func (r *ServerReconciler) deleteServerMetaCR(serverMeta *k8sv1alpha1.ServerMeta) (ctrl.Result, error) { + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + if err := r.Delete(ctx, serverMeta.DeepCopyObject(), &client.DeleteOptions{}); err != nil { + return ctrl.Result{}, fmt.Errorf("{deleteServerMetaCR} %s", err) + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ServerReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&k8sv1alpha1.Server{}). + Complete(r) +} + diff --git a/controllers/server_translations.go b/controllers/server_translations.go new file mode 100644 index 0000000..48cf222 --- /dev/null +++ b/controllers/server_translations.go @@ -0,0 +1,345 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "fmt" + + k8sv1alpha1 "github.com/netrisai/netris-operator/api/v1alpha1" + "github.com/netrisai/netriswebapi/v2/types/inventory" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// normalizeTags converts nil tags to empty slice to ensure consistent comparison +func normalizeTags(tags []string) []string { + if tags == nil { + return []string{} + } + return tags +} + +// ServerToServerMeta converts the Server resource to ServerMeta type and used for add the Server for Netris API. +func (r *ServerReconciler) ServerToServerMeta(server *k8sv1alpha1.Server) (*k8sv1alpha1.ServerMeta, error) { + var ( + imported = false + reclaim = false + ) + + if i, ok := server.GetAnnotations()["resource.k8s.netris.ai/import"]; ok && i == "true" { + imported = true + } + if i, ok := server.GetAnnotations()["resource.k8s.netris.ai/reclaimPolicy"]; ok && i == "retain" { + reclaim = true + } + + siteID := 0 + if site, ok := r.NStorage.SitesStorage.FindByName(server.Spec.Site); ok { + siteID = site.ID + } else { + return nil, fmt.Errorf("invalid site '%s'", server.Spec.Site) + } + + tenantID := 0 + if tenant, ok := r.NStorage.TenantsStorage.FindByName(server.Spec.Tenant); ok { + tenantID = tenant.ID + } else { + return nil, fmt.Errorf("invalid tenant '%s'", server.Spec.Tenant) + } + + profileID := 0 + profiles, err := r.Cred.InventoryProfile().Get() + if err != nil { + return nil, err + } + + for _, p := range profiles { + if p.Name == server.Spec.Profile { + profileID = p.ID + } + } + + if profileID == 0 && server.Spec.Profile != "" { + return nil, fmt.Errorf("invalid profile '%s'", server.Spec.Profile) + } + + serverMeta := &k8sv1alpha1.ServerMeta{ + ObjectMeta: metav1.ObjectMeta{ + Name: string(server.GetUID()), + Namespace: server.GetNamespace(), + }, + TypeMeta: metav1.TypeMeta{}, + Spec: k8sv1alpha1.ServerMetaSpec{ + Imported: imported, + Reclaim: reclaim, + ServerName: server.Name, + Description: server.Spec.Description, + TenantID: tenantID, + SiteID: siteID, + ProfileID: profileID, + MainIP: server.Spec.MainIP, + MgmtIP: server.Spec.MgmtIP, + UUID: server.Spec.UUID, + ASN: server.Spec.ASN, + PortCount: server.Spec.PortCount, + CustomData: server.Spec.CustomData, + Tags: normalizeTags(server.Spec.Tags), + SRVRole: server.Spec.SRVRole, + }, + } + + return serverMeta, nil +} + +func serverCompareFieldsForNewMeta(server *k8sv1alpha1.Server, serverMeta *k8sv1alpha1.ServerMeta) bool { + imported := false + reclaim := false + if i, ok := server.GetAnnotations()["resource.k8s.netris.ai/import"]; ok && i == "true" { + imported = true + } + if i, ok := server.GetAnnotations()["resource.k8s.netris.ai/reclaimPolicy"]; ok && i == "retain" { + reclaim = true + } + return server.GetGeneration() != serverMeta.Spec.ServerCRGeneration || imported != serverMeta.Spec.Imported || reclaim != serverMeta.Spec.Reclaim +} + +func serverMustUpdateAnnotations(server *k8sv1alpha1.Server) bool { + update := false + if i, ok := server.GetAnnotations()["resource.k8s.netris.ai/import"]; !(ok && (i == "true" || i == "false")) { + update = true + } + if i, ok := server.GetAnnotations()["resource.k8s.netris.ai/reclaimPolicy"]; !(ok && (i == "retain" || i == "delete")) { + update = true + } + return update +} + +func serverUpdateDefaultAnnotations(server *k8sv1alpha1.Server) { + imported := "false" + reclaim := "delete" + if i, ok := server.GetAnnotations()["resource.k8s.netris.ai/import"]; ok && i == "true" { + imported = "true" + } + if i, ok := server.GetAnnotations()["resource.k8s.netris.ai/reclaimPolicy"]; ok && i == "retain" { + reclaim = "retain" + } + annotations := server.GetAnnotations() + annotations["resource.k8s.netris.ai/import"] = imported + annotations["resource.k8s.netris.ai/reclaimPolicy"] = reclaim + server.SetAnnotations(annotations) +} + +// ServerMetaToNetris converts the k8s Server resource to Netris type and used for add the Server for Netris API. +func ServerMetaToNetris(serverMeta *k8sv1alpha1.ServerMeta) (*inventory.HWServer, error) { + mainIP := serverMeta.Spec.MainIP + if serverMeta.Spec.MainIP == "" { + mainIP = "auto" + } + + mgmtIP := serverMeta.Spec.MgmtIP + if serverMeta.Spec.MgmtIP == "" { + mgmtIP = "auto" + } + + var asn interface{} = serverMeta.Spec.ASN + if serverMeta.Spec.ASN == 0 { + asn = "auto" + } + + tags := normalizeTags(serverMeta.Spec.Tags) + + serverAdd := &inventory.HWServer{ + Name: serverMeta.Spec.ServerName, + Description: serverMeta.Spec.Description, + Tenant: inventory.IDName{ID: serverMeta.Spec.TenantID}, + Site: inventory.IDName{ID: serverMeta.Spec.SiteID}, + Profile: inventory.IDName{ID: serverMeta.Spec.ProfileID}, + MainAddress: mainIP, + MgmtAddress: mgmtIP, + UUID: serverMeta.Spec.UUID, + Asn: asn, + PortCount: serverMeta.Spec.PortCount, + CustomData: serverMeta.Spec.CustomData, + Tags: tags, + SRVRole: serverMeta.Spec.SRVRole, + Links: []inventory.HWLink{}, + } + + return serverAdd, nil +} + +// ServerMetaToNetrisUpdate converts the k8s Server resource to Netris type and used for update the Server for Netris API. +func ServerMetaToNetrisUpdate(serverMeta *k8sv1alpha1.ServerMeta) (*inventory.HWServer, error) { + mainIP := serverMeta.Spec.MainIP + if serverMeta.Spec.MainIP == "" { + mainIP = "auto" + } + + mgmtIP := serverMeta.Spec.MgmtIP + if serverMeta.Spec.MgmtIP == "" { + mgmtIP = "auto" + } + + var asn interface{} = serverMeta.Spec.ASN + if serverMeta.Spec.ASN == 0 { + asn = "auto" + } + + tags := normalizeTags(serverMeta.Spec.Tags) + + serverUpdate := &inventory.HWServer{ + Name: serverMeta.Spec.ServerName, + Description: serverMeta.Spec.Description, + Tenant: inventory.IDName{ID: serverMeta.Spec.TenantID}, + Site: inventory.IDName{ID: serverMeta.Spec.SiteID}, + Profile: inventory.IDName{ID: serverMeta.Spec.ProfileID}, + MainAddress: mainIP, + MgmtAddress: mgmtIP, + UUID: serverMeta.Spec.UUID, + Asn: asn, + PortCount: serverMeta.Spec.PortCount, + CustomData: serverMeta.Spec.CustomData, + Tags: tags, + SRVRole: serverMeta.Spec.SRVRole, + Links: []inventory.HWLink{}, + } + + return serverUpdate, nil +} + +func compareServerMetaAPIServer(serverMeta *k8sv1alpha1.ServerMeta, apiServer *inventory.HW, u uniReconciler) bool { + if apiServer.Name != serverMeta.Spec.ServerName { + u.DebugLogger.Info("Name changed", "netrisValue", apiServer.Name, "k8sValue", serverMeta.Spec.ServerName) + return false + } + + if apiServer.Description != serverMeta.Spec.Description { + u.DebugLogger.Info("Description changed", "netrisValue", apiServer.Description, "k8sValue", serverMeta.Spec.Description) + return false + } + + if apiServer.Tenant.ID != serverMeta.Spec.TenantID { + u.DebugLogger.Info("Tenant changed", "netrisValue", apiServer.Tenant.ID, "k8sValue", serverMeta.Spec.TenantID) + return false + } + + if apiServer.Site.ID != serverMeta.Spec.SiteID { + u.DebugLogger.Info("Site changed", "netrisValue", apiServer.Site.ID, "k8sValue", serverMeta.Spec.SiteID) + return false + } + + // Compare ProfileID: only compare if API actually has a ProfileID set (not 0) + // If API has ProfileID=0, it means the API doesn't support/accept ProfileID for this server + // In that case, we should ignore ProfileID in meta and not try to update it + if apiServer.Profile.ID != 0 { + // API has ProfileID set, so compare it with meta + if apiServer.Profile.ID != serverMeta.Spec.ProfileID { + u.DebugLogger.Info("Profile changed", "netrisValue", apiServer.Profile.ID, "k8sValue", serverMeta.Spec.ProfileID) + return false + } + } else { + // API has ProfileID=0 - API doesn't support ProfileID for this server + // Clear ProfileID in meta to match API and prevent constant updates + if serverMeta.Spec.ProfileID != 0 { + u.DebugLogger.Info("API has ProfileID=0 (not supported), clearing ProfileID in meta", "metaProfileID", serverMeta.Spec.ProfileID) + // Note: We'll clear this in the controller after comparison + } + } + + if apiServer.MainIP.Address != serverMeta.Spec.MainIP { + u.DebugLogger.Info("MainIP changed", "netrisValue", apiServer.MainIP.Address, "k8sValue", serverMeta.Spec.MainIP) + return false + } + + if apiServer.MgmtIP.Address != serverMeta.Spec.MgmtIP { + u.DebugLogger.Info("MgmtIP changed", "netrisValue", apiServer.MgmtIP.Address, "k8sValue", serverMeta.Spec.MgmtIP) + return false + } + + // Only compare UUID if it's set in meta (populated from API if empty) + if serverMeta.Spec.UUID != "" && apiServer.UUID != serverMeta.Spec.UUID { + u.DebugLogger.Info("UUID changed", "netrisValue", apiServer.UUID, "k8sValue", serverMeta.Spec.UUID) + return false + } + + // Only compare SRVRole if it's set in meta (populated from API if empty) + if serverMeta.Spec.SRVRole != "" && apiServer.SRVRole != serverMeta.Spec.SRVRole { + u.DebugLogger.Info("SRVRole changed", "netrisValue", apiServer.SRVRole, "k8sValue", serverMeta.Spec.SRVRole) + return false + } + + // Only compare ASN if it's explicitly set (not 0) + if apiServer.Asn != serverMeta.Spec.ASN && serverMeta.Spec.ASN != 0 { + u.DebugLogger.Info("ASN changed", "netrisValue", apiServer.Asn, "k8sValue", serverMeta.Spec.ASN) + return false + } + + // Only compare PortCount if it's explicitly set (not 0) + if apiServer.PortCount != serverMeta.Spec.PortCount && serverMeta.Spec.PortCount != 0 { + u.DebugLogger.Info("PortCount changed", "netrisValue", apiServer.PortCount, "k8sValue", serverMeta.Spec.PortCount) + return false + } + + // Only compare CustomData if it's set in meta (populated from API if empty) + if serverMeta.Spec.CustomData != "" && apiServer.CustomData != serverMeta.Spec.CustomData { + u.DebugLogger.Info("CustomData changed", "netrisValue", apiServer.CustomData, "k8sValue", serverMeta.Spec.CustomData) + return false + } + + // Compare Tags - normalize nil to empty slice + apiTags := normalizeTags(apiServer.Tags) + metaTags := normalizeTags(serverMeta.Spec.Tags) + + // Compare lengths first + if len(apiTags) != len(metaTags) { + u.DebugLogger.Info("Tags length changed", "netrisValue", len(apiTags), "k8sValue", len(metaTags), "apiTags", apiTags, "metaTags", metaTags) + return false + } + + // If both are empty, they match + if len(apiTags) == 0 && len(metaTags) == 0 { + return true + } + + // Compare both directions: all metaTags should be in apiTags AND all apiTags should be in metaTags + apiTagMap := make(map[string]bool) + for _, tag := range apiTags { + apiTagMap[tag] = true + } + metaTagMap := make(map[string]bool) + for _, tag := range metaTags { + metaTagMap[tag] = true + } + + // Check if all metaTags are in apiTags + for _, tag := range metaTags { + if !apiTagMap[tag] { + u.DebugLogger.Info("Tags changed - meta tag not in API", "tag", tag, "apiTags", apiTags, "metaTags", metaTags) + return false + } + } + + // Check if all apiTags are in metaTags + for _, tag := range apiTags { + if !metaTagMap[tag] { + u.DebugLogger.Info("Tags changed - API tag not in meta", "tag", tag, "apiTags", apiTags, "metaTags", metaTags) + return false + } + } + + return true +} + diff --git a/controllers/servermeta_controller.go b/controllers/servermeta_controller.go new file mode 100644 index 0000000..5eec343 --- /dev/null +++ b/controllers/servermeta_controller.go @@ -0,0 +1,453 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "encoding/json" + "fmt" + + "go.uber.org/zap/zapcore" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/go-logr/logr" + + k8sv1alpha1 "github.com/netrisai/netris-operator/api/v1alpha1" + "github.com/netrisai/netris-operator/netrisstorage" + "github.com/netrisai/netriswebapi/http" + api "github.com/netrisai/netriswebapi/v2" + "github.com/netrisai/netriswebapi/v2/types/inventory" +) + +// ServerMetaReconciler reconciles a ServerMeta object +type ServerMetaReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + Cred *api.Clientset + NStorage *netrisstorage.Storage +} + +//+kubebuilder:rbac:groups=k8s.netris.ai,resources=servermeta,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=k8s.netris.ai,resources=servermeta/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=k8s.netris.ai,resources=servermeta/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the ServerMeta object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.9.2/pkg/reconcile +func (r *ServerMetaReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + debugLogger := r.Log.WithValues("name", req.NamespacedName).V(int(zapcore.WarnLevel)) + + serverMeta := &k8sv1alpha1.ServerMeta{} + serverCR := &k8sv1alpha1.Server{} + serverMetaCtx, serverMetaCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverMetaCancel() + if err := r.Get(serverMetaCtx, req.NamespacedName, serverMeta); err != nil { + if errors.IsNotFound(err) { + debugLogger.Info(err.Error()) + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + logger := r.Log.WithValues("name", fmt.Sprintf("%s/%s", req.NamespacedName.Namespace, serverMeta.Spec.ServerName)) + debugLogger = logger.V(int(zapcore.WarnLevel)) + + u := uniReconciler{ + Client: r.Client, + Logger: logger, + DebugLogger: debugLogger, + Cred: r.Cred, + NStorage: r.NStorage, + } + + provisionState := "OK" + + serverNN := req.NamespacedName + serverNN.Name = serverMeta.Spec.ServerName + serverNNCtx, serverNNCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverNNCancel() + if err := r.Get(serverNNCtx, serverNN, serverCR); err != nil { + if errors.IsNotFound(err) { + debugLogger.Info(err.Error()) + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + if serverMeta.DeletionTimestamp != nil { + return ctrl.Result{}, nil + } + + if serverMeta.Spec.ID == 0 { + debugLogger.Info("ID Not found in meta") + if serverMeta.Spec.Imported { + logger.Info("Importing server") + debugLogger.Info("Imported yaml mode. Finding Server by name") + if server, ok := r.NStorage.HWsStorage.FindServerByName(serverMeta.Spec.ServerName); ok { + debugLogger.Info("Imported yaml mode. Server found") + serverMeta.Spec.ID = server.ID + serverMeta.Spec.MainIP = server.MainIP.Address + serverMeta.Spec.MgmtIP = server.MgmtIP.Address + + serverMetaPatchCtx, serverMetaPatchCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverMetaPatchCancel() + err := r.Patch(serverMetaPatchCtx, serverMeta.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{patch servermeta.Spec.ID} %s", err), "") + return u.patchServerStatus(serverCR, "Failure", err.Error()) + } + debugLogger.Info("Imported yaml mode. ID patched") + logger.Info("Server imported") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + logger.Info("Server not found for import") + debugLogger.Info("Imported yaml mode. Server not found") + } + + logger.Info("Creating Server") + if _, err, errMsg := r.createServer(serverMeta); err != nil { + logger.Error(fmt.Errorf("{createServer} %s", err), "") + return u.patchServerStatus(serverCR, "Failure", errMsg.Error()) + } + logger.Info("Server Created") + } else { + if apiServer, ok := r.NStorage.HWsStorage.FindServerByID(serverMeta.Spec.ID); ok { + debugLogger.Info("Comparing ServerMeta with Netris Server") + + needsPatch := false + if serverMeta.Spec.MainIP == "" && apiServer.MainIP.Address != "" { + serverMeta.Spec.MainIP = apiServer.MainIP.Address + needsPatch = true + } + if serverMeta.Spec.MgmtIP == "" && apiServer.MgmtIP.Address != "" { + serverMeta.Spec.MgmtIP = apiServer.MgmtIP.Address + needsPatch = true + } + if serverMeta.Spec.ASN == 0 && apiServer.Asn > 0 { + serverMeta.Spec.ASN = apiServer.Asn + needsPatch = true + } + if serverMeta.Spec.ProfileID == 0 && apiServer.Profile.ID > 0 { + serverMeta.Spec.ProfileID = apiServer.Profile.ID + needsPatch = true + } + if serverMeta.Spec.UUID == "" && apiServer.UUID != "" { + serverMeta.Spec.UUID = apiServer.UUID + needsPatch = true + } + if serverMeta.Spec.PortCount == 0 && apiServer.PortCount > 0 { + serverMeta.Spec.PortCount = apiServer.PortCount + needsPatch = true + } + if serverMeta.Spec.CustomData == "" && apiServer.CustomData != "" { + serverMeta.Spec.CustomData = apiServer.CustomData + needsPatch = true + } + if serverMeta.Spec.SRVRole == "" && apiServer.SRVRole != "" { + serverMeta.Spec.SRVRole = apiServer.SRVRole + needsPatch = true + } + + if needsPatch { + serverMetaPatchCtx, serverMetaPatchCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverMetaPatchCancel() + err := r.Patch(serverMetaPatchCtx, serverMeta.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{Patch serverMeta populated fields} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + debugLogger.Info("Populated fields patched to serverMeta") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + + // If API has ProfileID=0, clear it in meta to prevent constant updates + // But we still need to compare other fields (like tags), so we clear ProfileID first + // and then continue with comparison + needsProfileIDClear := false + if apiServer.Profile.ID == 0 && serverMeta.Spec.ProfileID != 0 { + debugLogger.Info("API has ProfileID=0 (not supported), will clear ProfileID in meta and Server CR after comparison") + serverMeta.Spec.ProfileID = 0 + needsProfileIDClear = true + } + + if ok := compareServerMetaAPIServer(serverMeta, apiServer, u); ok { + // Comparison passed, but we may still need to clear ProfileID + if needsProfileIDClear { + serverMetaPatchCtx, serverMetaPatchCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverMetaPatchCancel() + err := r.Patch(serverMetaPatchCtx, serverMeta.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{Patch serverMeta ProfileID} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + if serverCR.Spec.Profile != "" { + serverCR.Spec.Profile = "" + serverCRPatchCtx, serverCRPatchCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverCRPatchCancel() + err := r.Patch(serverCRPatchCtx, serverCR.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{Patch Server CR Profile} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + } + debugLogger.Info("ProfileID cleared in meta and Server CR") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + debugLogger.Info("Nothing Changed") + } else { + debugLogger.Info("Comparison failed - differences detected (see previous logs for specific field changes)") + debugLogger.Info("Current API state", + "apiName", apiServer.Name, + "apiDescription", apiServer.Description, + "apiMainIP", apiServer.MainIP.Address, + "apiMgmtIP", apiServer.MgmtIP.Address, + "apiASN", apiServer.Asn, + "apiPortCount", apiServer.PortCount, + "apiProfileID", apiServer.Profile.ID, + "apiUUID", apiServer.UUID, + "apiSRVRole", apiServer.SRVRole, + "apiCustomData", apiServer.CustomData, + "apiTags", apiServer.Tags, + ) + debugLogger.Info("Desired state from serverMeta", + "metaName", serverMeta.Spec.ServerName, + "metaDescription", serverMeta.Spec.Description, + "metaMainIP", serverMeta.Spec.MainIP, + "metaMgmtIP", serverMeta.Spec.MgmtIP, + "metaASN", serverMeta.Spec.ASN, + "metaPortCount", serverMeta.Spec.PortCount, + "metaProfileID", serverMeta.Spec.ProfileID, + "metaUUID", serverMeta.Spec.UUID, + "metaSRVRole", serverMeta.Spec.SRVRole, + "metaCustomData", serverMeta.Spec.CustomData, + "metaTags", serverMeta.Spec.Tags, + ) + logger.Info("Updating Server") + serverUpdate, err := ServerMetaToNetrisUpdate(serverMeta) + if err != nil { + logger.Error(fmt.Errorf("{ServerMetaToNetrisUpdate} %s", err), "") + return u.patchServerStatus(serverCR, "Failure", err.Error()) + } + + js, _ := json.Marshal(serverUpdate) + debugLogger.Info("Update payload being sent to Netris API", "payload", string(js)) + + _, err, errMsg := updateServer(serverMeta.Spec.ID, serverUpdate, r.Cred) + if err != nil { + logger.Error(fmt.Errorf("{updateServer} %s", err), "") + return u.patchServerStatus(serverCR, "Failure", errMsg.Error()) + } + logger.Info("Server Updated") + + // After update, refresh the storage cache and check if ProfileID was actually updated + // If API still has ProfileID=0 but we sent ProfileID=1, the API might not support setting it + // In that case, we should accept API's value and update serverMeta + if err := r.NStorage.HWsStorage.Download(); err != nil { + debugLogger.Info("Failed to refresh HWsStorage cache", "error", err) + } else { + if updatedServer, ok := r.NStorage.HWsStorage.FindServerByID(serverMeta.Spec.ID); ok { + if serverMeta.Spec.ProfileID != 0 && updatedServer.Profile.ID == 0 { + // We tried to set ProfileID but API still has 0 - accept API's value + debugLogger.Info("ProfileID update not accepted by API, accepting API value (0)", + "attemptedValue", serverMeta.Spec.ProfileID, "apiValue", updatedServer.Profile.ID) + serverMeta.Spec.ProfileID = 0 + serverMetaPatchCtx, serverMetaPatchCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverMetaPatchCancel() + err := r.Patch(serverMetaPatchCtx, serverMeta.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{Patch serverMeta ProfileID} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + debugLogger.Info("ProfileID cleared in serverMeta to match API") + + // Also clear Profile in Server CR to prevent it from regenerating serverMeta with ProfileID=1 + if serverCR.Spec.Profile != "" { + debugLogger.Info("Clearing Profile in Server CR to match API") + serverCR.Spec.Profile = "" + serverCRPatchCtx, serverCRPatchCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverCRPatchCancel() + err := r.Patch(serverCRPatchCtx, serverCR.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{Patch Server CR Profile} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + debugLogger.Info("Profile cleared in Server CR") + } + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + } + } + } + + // Clear ProfileID if needed (after comparison and update) + if needsProfileIDClear { + serverMetaPatchCtx, serverMetaPatchCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverMetaPatchCancel() + err := r.Patch(serverMetaPatchCtx, serverMeta.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{Patch serverMeta ProfileID} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + if serverCR.Spec.Profile != "" { + serverCR.Spec.Profile = "" + serverCRPatchCtx, serverCRPatchCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverCRPatchCancel() + err := r.Patch(serverCRPatchCtx, serverCR.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{Patch Server CR Profile} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + } + debugLogger.Info("ProfileID cleared in meta and Server CR after update") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + } else { + debugLogger.Info("Server not found in Netris") + debugLogger.Info("Going to create Server") + logger.Info("Creating Server") + if _, err, errMsg := r.createServer(serverMeta); err != nil { + logger.Error(fmt.Errorf("{createServer} %s", err), "") + return u.patchServerStatus(serverCR, "Failure", errMsg.Error()) + } + logger.Info("Server Created") + } + } + + // Get the API server object to populate profile name if needed + var apiServerForUpdate *inventory.HW + if serverMeta.Spec.ID > 0 { + if apiSrv, ok := r.NStorage.HWsStorage.FindServerByID(serverMeta.Spec.ID); ok { + apiServerForUpdate = apiSrv + } + } + if _, err := u.updateServerIfNeccesarry(serverCR, *serverMeta, apiServerForUpdate); err != nil { + logger.Error(fmt.Errorf("{updateServerIfNeccesarry} %s", err), "") + return u.patchServerStatus(serverCR, "Failure", err.Error()) + } + + return u.patchServerStatus(serverCR, provisionState, "Success") +} + +func (r *ServerMetaReconciler) createServer(serverMeta *k8sv1alpha1.ServerMeta) (ctrl.Result, error, error) { + debugLogger := r.Log.WithValues( + "name", fmt.Sprintf("%s/%s", serverMeta.Namespace, serverMeta.Spec.ServerName), + "serverName", serverMeta.Spec.ServerCRGeneration, + ).V(int(zapcore.WarnLevel)) + + serverAdd, err := ServerMetaToNetris(serverMeta) + if err != nil { + return ctrl.Result{}, err, err + } + + js, _ := json.Marshal(serverAdd) + debugLogger.Info("serverToAdd", "payload", string(js)) + + reply, err := r.Cred.Inventory().AddServer(serverAdd) + if err != nil { + return ctrl.Result{}, err, err + } + + idStruct := struct { + ID int `json:"id"` + }{} + + data, err := reply.Parse() + if err != nil { + return ctrl.Result{}, err, err + } + + if reply.StatusCode != 200 { + return ctrl.Result{}, fmt.Errorf(data.Message), fmt.Errorf(data.Message) + } + + idStruct.ID = int(data.Data.(map[string]interface{})["id"].(float64)) + + debugLogger.Info("Server Created", "id", idStruct.ID) + + serverMeta.Spec.ID = idStruct.ID + + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + err = r.Patch(ctx, serverMeta.DeepCopyObject(), client.Merge, &client.PatchOptions{}) // requeue + if err != nil { + return ctrl.Result{}, err, err + } + + debugLogger.Info("ID patched to meta", "id", idStruct.ID) + return ctrl.Result{}, nil, nil +} + +func updateServer(id int, server *inventory.HWServer, cred *api.Clientset) (ctrl.Result, error, error) { + reply, err := cred.Inventory().UpdateServer(id, server) + if err != nil { + return ctrl.Result{}, fmt.Errorf("{updateServer} %s", err), err + } + resp, err := http.ParseAPIResponse(reply.Data) + if err != nil { + return ctrl.Result{}, err, err + } + if !resp.IsSuccess { + return ctrl.Result{}, fmt.Errorf("{updateServer} %s", fmt.Errorf(resp.Message)), fmt.Errorf(resp.Message) + } + + return ctrl.Result{}, nil, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ServerMetaReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&k8sv1alpha1.ServerMeta{}). + Complete(r) +} + +func (u *uniReconciler) updateServerIfNeccesarry(serverCR *k8sv1alpha1.Server, serverMeta k8sv1alpha1.ServerMeta, apiServer *inventory.HW) (ctrl.Result, error) { + shouldUpdateCR := false + if serverCR.Spec.MainIP == "" && serverCR.Spec.MainIP != serverMeta.Spec.MainIP { + serverCR.Spec.MainIP = serverMeta.Spec.MainIP + shouldUpdateCR = true + } + if serverCR.Spec.MgmtIP == "" && serverCR.Spec.MgmtIP != serverMeta.Spec.MgmtIP { + serverCR.Spec.MgmtIP = serverMeta.Spec.MgmtIP + shouldUpdateCR = true + } + // Populate profile name from API if API has a profile and Server CR doesn't have one set + if apiServer != nil && apiServer.Profile.ID > 0 && apiServer.Profile.Name != "" { + if serverCR.Spec.Profile == "" || serverCR.Spec.Profile != apiServer.Profile.Name { + u.DebugLogger.Info("Populating Profile name from API", "profileName", apiServer.Profile.Name, "currentProfile", serverCR.Spec.Profile) + serverCR.Spec.Profile = apiServer.Profile.Name + shouldUpdateCR = true + } + } + if shouldUpdateCR { + u.DebugLogger.Info("Updating Server CR") + if _, err := u.patchServer(serverCR); err != nil { + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil +} + diff --git a/deploy/charts/netris-operator/crds/k8s.netris.ai_servermeta.yaml b/deploy/charts/netris-operator/crds/k8s.netris.ai_servermeta.yaml new file mode 100644 index 0000000..2f34ac1 --- /dev/null +++ b/deploy/charts/netris-operator/crds/k8s.netris.ai_servermeta.yaml @@ -0,0 +1,97 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: servermeta.k8s.netris.ai +spec: + group: k8s.netris.ai + names: + kind: ServerMeta + listKind: ServerMetaList + plural: servermeta + singular: servermeta + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ServerMeta is the Schema for the servermeta API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ServerMetaSpec defines the desired state of ServerMeta + properties: + asn: + type: integer + customData: + type: string + description: + type: string + id: + type: integer + imported: + type: boolean + mainIp: + type: string + mgmtIp: + type: string + portsCount: + type: integer + profileid: + type: integer + reclaimPolicy: + type: boolean + serverGeneration: + format: int64 + type: integer + serverName: + type: string + siteid: + type: integer + srvRole: + type: string + tags: + items: + type: string + type: array + tenantid: + type: integer + uuid: + type: string + required: + - id + - imported + - reclaimPolicy + - serverGeneration + - serverName + type: object + status: + description: ServerMetaStatus defines the observed state of ServerMeta + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/deploy/charts/netris-operator/crds/k8s.netris.ai_servers.yaml b/deploy/charts/netris-operator/crds/k8s.netris.ai_servers.yaml new file mode 100644 index 0000000..b695f35 --- /dev/null +++ b/deploy/charts/netris-operator/crds/k8s.netris.ai_servers.yaml @@ -0,0 +1,109 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: servers.k8s.netris.ai +spec: + group: k8s.netris.ai + names: + kind: Server + listKind: ServerList + plural: servers + singular: server + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.tenant + name: Tenant + type: string + - jsonPath: .spec.site + name: Site + type: string + - jsonPath: .spec.profile + name: Profile + type: string + - jsonPath: .spec.mainIp + name: Main IP + type: string + - jsonPath: .spec.mgmtIp + name: Management IP + type: string + - jsonPath: .status.status + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Server is the Schema for the servers API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ServerSpec defines the desired state of Server + properties: + asn: + type: integer + customData: + type: string + description: + type: string + mainIp: + pattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$ + type: string + mgmtIp: + pattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$ + type: string + portsCount: + type: integer + profile: + type: string + site: + type: string + srvRole: + type: string + tags: + items: + type: string + type: array + tenant: + type: string + uuid: + type: string + type: object + status: + description: ServerStatus defines the observed state of Server + properties: + message: + type: string + status: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/deploy/charts/netris-operator/templates/rbac.yaml b/deploy/charts/netris-operator/templates/rbac.yaml index c75b6a5..9e55d99 100644 --- a/deploy/charts/netris-operator/templates/rbac.yaml +++ b/deploy/charts/netris-operator/templates/rbac.yaml @@ -544,6 +544,58 @@ rules: - get - patch - update + - apiGroups: + - k8s.netris.ai + resources: + - servermeta + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - k8s.netris.ai + resources: + - servermeta/finalizers + verbs: + - update + - apiGroups: + - k8s.netris.ai + resources: + - servermeta/status + verbs: + - get + - patch + - update + - apiGroups: + - k8s.netris.ai + resources: + - servers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - k8s.netris.ai + resources: + - servers/finalizers + verbs: + - update + - apiGroups: + - k8s.netris.ai + resources: + - servers/status + verbs: + - get + - patch + - update - apiGroups: - k8s.netris.ai resources: diff --git a/main.go b/main.go index 25d9168..ee0ae8b 100644 --- a/main.go +++ b/main.go @@ -275,6 +275,26 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "SoftgateMeta") os.Exit(1) } + if err = (&controllers.ServerReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("Server"), + Scheme: mgr.GetScheme(), + Cred: cred, + NStorage: nStorage, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Server") + os.Exit(1) + } + if err = (&controllers.ServerMetaReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("ServerMeta"), + Scheme: mgr.GetScheme(), + Cred: cred, + NStorage: nStorage, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ServerMeta") + os.Exit(1) + } if err = (&controllers.SwitchReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("Switch"), diff --git a/netrisstorage/hws.go b/netrisstorage/hws.go index 4a8dbf9..80b453f 100644 --- a/netrisstorage/hws.go +++ b/netrisstorage/hws.go @@ -196,6 +196,43 @@ func (p *HWsStorage) findSwitchByID(id int) (*inventory.HW, bool) { return nil, false } +// FindServerByName . +func (p *HWsStorage) FindServerByName(name string) (*inventory.HW, bool) { + p.Lock() + defer p.Unlock() + return p.findServerByName(name) +} + +func (p *HWsStorage) findServerByName(name string) (*inventory.HW, bool) { + for _, hw := range p.HWs { + if hw.Name == name && hw.Type == "server" { + return hw, true + } + } + return nil, false +} + +// FindServerByID . +func (p *HWsStorage) FindServerByID(id int) (*inventory.HW, bool) { + p.Lock() + defer p.Unlock() + item, ok := p.findServerByID(id) + if !ok { + _ = p.download() + return p.findServerByID(id) + } + return item, ok +} + +func (p *HWsStorage) findServerByID(id int) (*inventory.HW, bool) { + for _, hw := range p.HWs { + if hw.ID == id && hw.Type == "server" { + return hw, true + } + } + return nil, false +} + // FindHWsBySite . func (p *HWsStorage) FindHWsBySite(siteID int) []inventory.HW { p.Lock() diff --git a/samples/kustomization.yaml b/samples/kustomization.yaml index ad08b38..95500f3 100644 --- a/samples/kustomization.yaml +++ b/samples/kustomization.yaml @@ -5,6 +5,7 @@ resources: - allocation.yaml - subnet.yaml - softgate.yaml + - server.yaml - switch.yaml - controller.yaml - vnet.yaml diff --git a/samples/server.yaml b/samples/server.yaml new file mode 100644 index 0000000..69c9e52 --- /dev/null +++ b/samples/server.yaml @@ -0,0 +1,71 @@ +apiVersion: k8s.netris.ai/v1alpha1 +kind: Server +metadata: + name: my-server01 +spec: + tenant: Admin + description: My Server01 + site: santa-clara + profile: my-profile + # mainIp: 198.51.100.1 + # mgmtIp: 192.0.2.1 + # uuid: 550e8400-e29b-41d4-a716-446655440000 + # asn: 65000 + # portsCount: 48 + # srvRole: compute + # tags: + # - production + # - compute +--- +apiVersion: k8s.netris.ai/v1alpha1 +kind: Server +metadata: + name: my-server02 +spec: + tenant: Admin + description: My Server02 + site: santa-clara + profile: my-profile + # mainIp: 198.51.100.2 + # mgmtIp: 192.0.2.2 +--- +# Example: Link connecting server01 to switch +apiVersion: k8s.netris.ai/v1alpha1 +kind: Link +metadata: + name: server01-to-sw01 +spec: + ports: + - eth0@my-server01 + - swp1@my-sw01 +--- +# Example: Link connecting server02 to switch +apiVersion: k8s.netris.ai/v1alpha1 +kind: Link +metadata: + name: server02-to-sw01 +spec: + ports: + - eth0@my-server02 + - swp2@my-sw01 +--- +# Example: Link connecting two servers +apiVersion: k8s.netris.ai/v1alpha1 +kind: Link +metadata: + name: server01-to-server02 +spec: + ports: + - eth1@my-server01 + - eth1@my-server02 +--- +# Example: Link connecting server02 to another switch +apiVersion: k8s.netris.ai/v1alpha1 +kind: Link +metadata: + name: server02-to-sw02 +spec: + ports: + - eth2@my-server02 + - swp1@my-sw02 + From 9598e9d7fc2a220bed549f48aa9246af443590b5 Mon Sep 17 00:00:00 2001 From: Vasyl Saienko Date: Fri, 28 Nov 2025 12:27:31 +0200 Subject: [PATCH 5/6] Implement serverclustertemplates management --- api/v1alpha1/serverclustertemplate_types.go | 70 +++++ .../serverclustertemplatemeta_types.go | 69 +++++ api/v1alpha1/zz_generated.deepcopy.go | 203 +++++++++++++++ ...s.netris.ai_serverclustertemplatemeta.yaml | 73 ++++++ .../k8s.netris.ai_serverclustertemplates.yaml | 86 +++++++ config/rbac/role.yaml | 52 ++++ controllers/controller.go | 15 ++ .../serverclustertemplate_controller.go | 227 ++++++++++++++++ .../serverclustertemplate_translations.go | 141 ++++++++++ .../serverclustertemplatemeta_controller.go | 243 ++++++++++++++++++ ...s.netris.ai_serverclustertemplatemeta.yaml | 73 ++++++ .../k8s.netris.ai_serverclustertemplates.yaml | 86 +++++++ .../netris-operator/templates/rbac.yaml | 52 ++++ main.go | 20 ++ samples/kustomization.yaml | 1 + samples/serverclustertemplate.yaml | 11 + 16 files changed, 1422 insertions(+) create mode 100644 api/v1alpha1/serverclustertemplate_types.go create mode 100644 api/v1alpha1/serverclustertemplatemeta_types.go create mode 100644 config/crd/bases/k8s.netris.ai_serverclustertemplatemeta.yaml create mode 100644 config/crd/bases/k8s.netris.ai_serverclustertemplates.yaml create mode 100644 controllers/serverclustertemplate_controller.go create mode 100644 controllers/serverclustertemplate_translations.go create mode 100644 controllers/serverclustertemplatemeta_controller.go create mode 100644 deploy/charts/netris-operator/crds/k8s.netris.ai_serverclustertemplatemeta.yaml create mode 100644 deploy/charts/netris-operator/crds/k8s.netris.ai_serverclustertemplates.yaml create mode 100644 samples/serverclustertemplate.yaml diff --git a/api/v1alpha1/serverclustertemplate_types.go b/api/v1alpha1/serverclustertemplate_types.go new file mode 100644 index 0000000..15ca138 --- /dev/null +++ b/api/v1alpha1/serverclustertemplate_types.go @@ -0,0 +1,70 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ServerClusterTemplateSpec defines the desired state of ServerClusterTemplate +type ServerClusterTemplateSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + Vnets []interface{} `json:"vnets"` +} + +// ServerClusterTemplateStatus defines the observed state of ServerClusterTemplate +type ServerClusterTemplateStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + Status string `json:"status,omitempty"` + Message string `json:"message,omitempty"` + ModifiedDate metav1.Time `json:"modified,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Name",type=string,JSONPath=`.metadata.name` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Modified",type=date,JSONPath=`.status.modified`,priority=1 +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// ServerClusterTemplate is the Schema for the serverclustertemplates API +type ServerClusterTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ServerClusterTemplateSpec `json:"spec"` + Status ServerClusterTemplateStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ServerClusterTemplateList contains a list of ServerClusterTemplate +type ServerClusterTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ServerClusterTemplate `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ServerClusterTemplate{}, &ServerClusterTemplateList{}) +} + diff --git a/api/v1alpha1/serverclustertemplatemeta_types.go b/api/v1alpha1/serverclustertemplatemeta_types.go new file mode 100644 index 0000000..e510976 --- /dev/null +++ b/api/v1alpha1/serverclustertemplatemeta_types.go @@ -0,0 +1,69 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ServerClusterTemplateMetaSpec defines the desired state of ServerClusterTemplateMeta +type ServerClusterTemplateMetaSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + Imported bool `json:"imported"` + Reclaim bool `json:"reclaimPolicy"` + ServerClusterTemplateCRGeneration int64 `json:"serverClusterTemplateGeneration"` + ID int `json:"id"` + Name string `json:"name"` + ServerClusterTemplateName string `json:"serverClusterTemplateName"` + Vnets []interface{} `json:"vnets"` +} + +// ServerClusterTemplateMetaStatus defines the observed state of ServerClusterTemplateMeta +type ServerClusterTemplateMetaStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// ServerClusterTemplateMeta is the Schema for the serverclustertemplatemeta API +type ServerClusterTemplateMeta struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ServerClusterTemplateMetaSpec `json:"spec,omitempty"` + Status ServerClusterTemplateMetaStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ServerClusterTemplateMetaList contains a list of ServerClusterTemplateMeta +type ServerClusterTemplateMetaList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ServerClusterTemplateMeta `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ServerClusterTemplateMeta{}, &ServerClusterTemplateMetaList{}) +} + diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 67f786b..adc2754 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -2932,3 +2932,206 @@ func (in *VPCMetaList) DeepCopyObject() runtime.Object { } return nil } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerClusterTemplateSpec) DeepCopyInto(out *ServerClusterTemplateSpec) { + *out = *in + if in.Vnets != nil { + in, out := &in.Vnets, &out.Vnets + *out = make([]interface{}, len(*in)) + for i := range *in { + // Deep copy interface{} by marshaling/unmarshaling + if (*in)[i] != nil { + // Note: This is a shallow copy for interface{} slices + // For a true deep copy, we'd need to marshal/unmarshal, but that's expensive + (*out)[i] = (*in)[i] + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerClusterTemplateSpec. +func (in *ServerClusterTemplateSpec) DeepCopy() *ServerClusterTemplateSpec { + if in == nil { + return nil + } + out := new(ServerClusterTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerClusterTemplateStatus) DeepCopyInto(out *ServerClusterTemplateStatus) { + *out = *in + in.ModifiedDate.DeepCopyInto(&out.ModifiedDate) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerClusterTemplateStatus. +func (in *ServerClusterTemplateStatus) DeepCopy() *ServerClusterTemplateStatus { + if in == nil { + return nil + } + out := new(ServerClusterTemplateStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerClusterTemplate) DeepCopyInto(out *ServerClusterTemplate) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerClusterTemplate. +func (in *ServerClusterTemplate) DeepCopy() *ServerClusterTemplate { + if in == nil { + return nil + } + out := new(ServerClusterTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServerClusterTemplate) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerClusterTemplateList) DeepCopyInto(out *ServerClusterTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ServerClusterTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerClusterTemplateList. +func (in *ServerClusterTemplateList) DeepCopy() *ServerClusterTemplateList { + if in == nil { + return nil + } + out := new(ServerClusterTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServerClusterTemplateList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerClusterTemplateMetaSpec) DeepCopyInto(out *ServerClusterTemplateMetaSpec) { + *out = *in + if in.Vnets != nil { + in, out := &in.Vnets, &out.Vnets + *out = make([]interface{}, len(*in)) + for i := range *in { + // Deep copy interface{} by marshaling/unmarshaling + if (*in)[i] != nil { + // Note: This is a shallow copy for interface{} slices + // For a true deep copy, we'd need to marshal/unmarshal, but that's expensive + (*out)[i] = (*in)[i] + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerClusterTemplateMetaSpec. +func (in *ServerClusterTemplateMetaSpec) DeepCopy() *ServerClusterTemplateMetaSpec { + if in == nil { + return nil + } + out := new(ServerClusterTemplateMetaSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerClusterTemplateMetaStatus) DeepCopyInto(out *ServerClusterTemplateMetaStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerClusterTemplateMetaStatus. +func (in *ServerClusterTemplateMetaStatus) DeepCopy() *ServerClusterTemplateMetaStatus { + if in == nil { + return nil + } + out := new(ServerClusterTemplateMetaStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerClusterTemplateMeta) DeepCopyInto(out *ServerClusterTemplateMeta) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerClusterTemplateMeta. +func (in *ServerClusterTemplateMeta) DeepCopy() *ServerClusterTemplateMeta { + if in == nil { + return nil + } + out := new(ServerClusterTemplateMeta) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServerClusterTemplateMeta) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerClusterTemplateMetaList) DeepCopyInto(out *ServerClusterTemplateMetaList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ServerClusterTemplateMeta, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerClusterTemplateMetaList. +func (in *ServerClusterTemplateMetaList) DeepCopy() *ServerClusterTemplateMetaList { + if in == nil { + return nil + } + out := new(ServerClusterTemplateMetaList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServerClusterTemplateMetaList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/config/crd/bases/k8s.netris.ai_serverclustertemplatemeta.yaml b/config/crd/bases/k8s.netris.ai_serverclustertemplatemeta.yaml new file mode 100644 index 0000000..a41892a --- /dev/null +++ b/config/crd/bases/k8s.netris.ai_serverclustertemplatemeta.yaml @@ -0,0 +1,73 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: serverclustertemplatemeta.k8s.netris.ai +spec: + group: k8s.netris.ai + names: + kind: ServerClusterTemplateMeta + listKind: ServerClusterTemplateMetaList + plural: serverclustertemplatemeta + singular: serverclustertemplatemeta + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ServerClusterTemplateMeta is the Schema for the serverclustertemplatemeta API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ServerClusterTemplateMetaSpec defines the desired state of ServerClusterTemplateMeta + properties: + id: + format: int64 + type: integer + imported: + type: boolean + name: + type: string + reclaimPolicy: + type: boolean + serverClusterTemplateGeneration: + format: int64 + type: integer + serverClusterTemplateName: + type: string + vnets: + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + type: object + status: + description: ServerClusterTemplateMetaStatus defines the observed state of ServerClusterTemplateMeta + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/config/crd/bases/k8s.netris.ai_serverclustertemplates.yaml b/config/crd/bases/k8s.netris.ai_serverclustertemplates.yaml new file mode 100644 index 0000000..ccc9ed3 --- /dev/null +++ b/config/crd/bases/k8s.netris.ai_serverclustertemplates.yaml @@ -0,0 +1,86 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: serverclustertemplates.k8s.netris.ai +spec: + group: k8s.netris.ai + names: + kind: ServerClusterTemplate + listKind: ServerClusterTemplateList + plural: serverclustertemplates + singular: serverclustertemplate + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.name + name: Name + type: string + - jsonPath: .status.status + name: Status + type: string + - jsonPath: .status.modified + name: Modified + priority: 1 + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ServerClusterTemplate is the Schema for the serverclustertemplates API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ServerClusterTemplateSpec defines the desired state of ServerClusterTemplate + properties: + vnets: + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + type: object + status: + description: ServerClusterTemplateStatus defines the observed state of ServerClusterTemplate + properties: + message: + type: string + modified: + format: date-time + type: string + status: + description: 'INSERT ADDITIONAL STATUS FIELD - define observed state + of cluster Important: Run "make" to regenerate code after modifying + this file' + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 9a079ec..75c6ac9 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -597,6 +597,58 @@ rules: - get - patch - update + - apiGroups: + - k8s.netris.ai + resources: + - serverclustertemplates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - k8s.netris.ai + resources: + - serverclustertemplates/finalizers + verbs: + - update + - apiGroups: + - k8s.netris.ai + resources: + - serverclustertemplates/status + verbs: + - get + - patch + - update + - apiGroups: + - k8s.netris.ai + resources: + - serverclustertemplatemeta + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - k8s.netris.ai + resources: + - serverclustertemplatemeta/finalizers + verbs: + - update + - apiGroups: + - k8s.netris.ai + resources: + - serverclustertemplatemeta/status + verbs: + - get + - patch + - update - apiGroups: - k8s.netris.ai resources: diff --git a/controllers/controller.go b/controllers/controller.go index d612b05..f949ea1 100644 --- a/controllers/controller.go +++ b/controllers/controller.go @@ -352,6 +352,21 @@ func (u *uniReconciler) patchServerStatus(server *k8sv1alpha1.Server, status, me return ctrl.Result{RequeueAfter: requeueInterval}, nil } +func (u *uniReconciler) patchServerClusterTemplateStatus(sct *k8sv1alpha1.ServerClusterTemplate, status, message string) (ctrl.Result, error) { + u.DebugLogger.Info("Patching Status", "status", status, "message", message) + + sct.Status.Status = status + sct.Status.Message = message + + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + err := u.Status().Patch(ctx, sct.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + u.DebugLogger.Info("{r.Status().Patch}", "error", err, "action", "status update") + } + return ctrl.Result{RequeueAfter: requeueInterval}, nil +} + func (u *uniReconciler) patchServer(server *k8sv1alpha1.Server) (ctrl.Result, error) { u.DebugLogger.Info("Patching") ctx, cancel := context.WithTimeout(cntxt, contextTimeout) diff --git a/controllers/serverclustertemplate_controller.go b/controllers/serverclustertemplate_controller.go new file mode 100644 index 0000000..a6e4627 --- /dev/null +++ b/controllers/serverclustertemplate_controller.go @@ -0,0 +1,227 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + + "go.uber.org/zap/zapcore" + "k8s.io/apimachinery/pkg/api/errors" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + k8sv1alpha1 "github.com/netrisai/netris-operator/api/v1alpha1" + "github.com/netrisai/netris-operator/netrisstorage" + "github.com/netrisai/netriswebapi/http" + api "github.com/netrisai/netriswebapi/v2" +) + +// ServerClusterTemplateReconciler reconciles a ServerClusterTemplate object +type ServerClusterTemplateReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + Cred *api.Clientset + NStorage *netrisstorage.Storage +} + +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=serverclustertemplates,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=serverclustertemplates/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=serverclustertemplates/finalizers,verbs=update + +// Reconcile serverclustertemplate events +func (r *ServerClusterTemplateReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + logger := r.Log.WithValues("name", req.NamespacedName) + debugLogger := logger.V(int(zapcore.WarnLevel)) + sctCR := &k8sv1alpha1.ServerClusterTemplate{} + + u := uniReconciler{ + Client: r.Client, + Logger: logger, + DebugLogger: debugLogger, + Cred: r.Cred, + NStorage: r.NStorage, + } + + sctCtx, sctCancel := context.WithTimeout(cntxt, contextTimeout) + defer sctCancel() + if err := r.Get(sctCtx, req.NamespacedName, sctCR); err != nil { + if errors.IsNotFound(err) { + debugLogger.Info(err.Error()) + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + sctMetaNamespaced := req.NamespacedName + sctMetaNamespaced.Name = string(sctCR.GetUID()) + sctMeta := &k8sv1alpha1.ServerClusterTemplateMeta{} + metaFound := true + sctMetaCtx, sctMetaCancel := context.WithTimeout(cntxt, contextTimeout) + defer sctMetaCancel() + if err := r.Get(sctMetaCtx, sctMetaNamespaced, sctMeta); err != nil { + if errors.IsNotFound(err) { + debugLogger.Info(err.Error()) + metaFound = false + sctMeta = nil + } else { + return ctrl.Result{}, err + } + } + + if sctCR.DeletionTimestamp != nil { + logger.Info("Go to delete") + _, err := r.deleteServerClusterTemplate(sctCR, sctMeta) + if err != nil { + logger.Error(fmt.Errorf("{deleteServerClusterTemplate} %s", err), "") + return u.patchServerClusterTemplateStatus(sctCR, "Failure", err.Error()) + } + logger.Info("ServerClusterTemplate deleted") + return ctrl.Result{}, nil + } + + if serverClusterTemplateMustUpdateAnnotations(sctCR) { + debugLogger.Info("Setting default annotations") + serverClusterTemplateUpdateDefaultAnnotations(sctCR) + sctUpdateCtx, sctUpdateCancel := context.WithTimeout(cntxt, contextTimeout) + defer sctUpdateCancel() + err := r.Patch(sctUpdateCtx, sctCR.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{Patch ServerClusterTemplate default annotations} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + return ctrl.Result{}, nil + } + + if metaFound { + debugLogger.Info("Meta found") + if serverClusterTemplateCompareFieldsForNewMeta(sctCR, sctMeta) { + debugLogger.Info("Generating New Meta") + sctID := sctMeta.Spec.ID + newSctMeta, err := r.ServerClusterTemplateToServerClusterTemplateMeta(sctCR) + if err != nil { + logger.Error(fmt.Errorf("{ServerClusterTemplateToServerClusterTemplateMeta} %s", err), "") + return u.patchServerClusterTemplateStatus(sctCR, "Failure", err.Error()) + } + sctMeta.Spec = newSctMeta.DeepCopy().Spec + sctMeta.Spec.ID = sctID + sctMeta.Spec.ServerClusterTemplateCRGeneration = sctCR.GetGeneration() + + sctMetaUpdateCtx, sctMetaUpdateCancel := context.WithTimeout(cntxt, contextTimeout) + defer sctMetaUpdateCancel() + err = r.Update(sctMetaUpdateCtx, sctMeta.DeepCopyObject(), &client.UpdateOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{sctMeta Update} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + } + } else { + debugLogger.Info("Meta not found") + if sctCR.GetFinalizers() == nil { + sctCR.SetFinalizers([]string{"resource.k8s.netris.ai/delete"}) + sctPatchCtx, sctPatchCancel := context.WithTimeout(cntxt, contextTimeout) + defer sctPatchCancel() + err := r.Patch(sctPatchCtx, sctCR.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{Patch ServerClusterTemplate Finalizer} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + return ctrl.Result{}, nil + } + + sctMeta, err := r.ServerClusterTemplateToServerClusterTemplateMeta(sctCR) + if err != nil { + logger.Error(fmt.Errorf("{ServerClusterTemplateToServerClusterTemplateMeta} %s", err), "") + return u.patchServerClusterTemplateStatus(sctCR, "Failure", err.Error()) + } + + sctMeta.Spec.ServerClusterTemplateCRGeneration = sctCR.GetGeneration() + + sctMetaCreateCtx, sctMetaCreateCancel := context.WithTimeout(cntxt, contextTimeout) + defer sctMetaCreateCancel() + if err := r.Create(sctMetaCreateCtx, sctMeta.DeepCopyObject(), &client.CreateOptions{}); err != nil { + logger.Error(fmt.Errorf("{sctMeta Create} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + } + + return ctrl.Result{RequeueAfter: requeueInterval}, nil +} + +func (r *ServerClusterTemplateReconciler) deleteServerClusterTemplate(sctCR *k8sv1alpha1.ServerClusterTemplate, sctMeta *k8sv1alpha1.ServerClusterTemplateMeta) (ctrl.Result, error) { + if sctMeta != nil && sctMeta.Spec.ID > 0 && !sctMeta.Spec.Reclaim { + reply, err := r.Cred.ServerClusterTemplate().Delete(sctMeta.Spec.ID) + if err != nil { + return ctrl.Result{}, fmt.Errorf("{deleteServerClusterTemplate} %s", err) + } + resp, err := http.ParseAPIResponse(reply.Data) + if err != nil { + return ctrl.Result{}, err + } + if !resp.IsSuccess { + if resp.Message != "Invalid ServerClusterTemplate ID" { + return ctrl.Result{}, fmt.Errorf("{deleteServerClusterTemplate} %s", fmt.Errorf(resp.Message)) + } + } + } + return r.deleteCRs(sctCR, sctMeta) +} + +func (r *ServerClusterTemplateReconciler) deleteCRs(sctCR *k8sv1alpha1.ServerClusterTemplate, sctMeta *k8sv1alpha1.ServerClusterTemplateMeta) (ctrl.Result, error) { + if sctMeta != nil { + _, err := r.deleteServerClusterTemplateMetaCR(sctMeta) + if err != nil { + return ctrl.Result{}, fmt.Errorf("{deleteCRs} %s", err) + } + } + + return r.deleteServerClusterTemplateCR(sctCR) +} + +func (r *ServerClusterTemplateReconciler) deleteServerClusterTemplateCR(sctCR *k8sv1alpha1.ServerClusterTemplate) (ctrl.Result, error) { + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + sctCR.ObjectMeta.SetFinalizers(nil) + sctCR.SetFinalizers(nil) + if err := r.Update(ctx, sctCR.DeepCopyObject(), &client.UpdateOptions{}); err != nil { + return ctrl.Result{}, fmt.Errorf("{deleteServerClusterTemplateCR} %s", err) + } + + return ctrl.Result{}, nil +} + +func (r *ServerClusterTemplateReconciler) deleteServerClusterTemplateMetaCR(sctMeta *k8sv1alpha1.ServerClusterTemplateMeta) (ctrl.Result, error) { + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + if err := r.Delete(ctx, sctMeta.DeepCopyObject(), &client.DeleteOptions{}); err != nil { + return ctrl.Result{}, fmt.Errorf("{deleteServerClusterTemplateMetaCR} %s", err) + } + + return ctrl.Result{}, nil +} + +// SetupWithManager Resources +func (r *ServerClusterTemplateReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&k8sv1alpha1.ServerClusterTemplate{}). + Complete(r) +} + diff --git a/controllers/serverclustertemplate_translations.go b/controllers/serverclustertemplate_translations.go new file mode 100644 index 0000000..a5df1a9 --- /dev/null +++ b/controllers/serverclustertemplate_translations.go @@ -0,0 +1,141 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "encoding/json" + + k8sv1alpha1 "github.com/netrisai/netris-operator/api/v1alpha1" + "github.com/netrisai/netriswebapi/v2/types/serverclustertemplate" + "github.com/r3labs/diff/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ServerClusterTemplateToServerClusterTemplateMeta converts the ServerClusterTemplate resource to ServerClusterTemplateMeta type. +func (r *ServerClusterTemplateReconciler) ServerClusterTemplateToServerClusterTemplateMeta(sctCR *k8sv1alpha1.ServerClusterTemplate) (*k8sv1alpha1.ServerClusterTemplateMeta, error) { + imported := false + reclaim := false + if i, ok := sctCR.GetAnnotations()["resource.k8s.netris.ai/import"]; ok && i == "true" { + imported = true + } + if i, ok := sctCR.GetAnnotations()["resource.k8s.netris.ai/reclaimPolicy"]; ok && i == "retain" { + reclaim = true + } + + sctMeta := &k8sv1alpha1.ServerClusterTemplateMeta{ + ObjectMeta: metav1.ObjectMeta{ + Name: string(sctCR.GetUID()), + Namespace: sctCR.GetNamespace(), + }, + TypeMeta: metav1.TypeMeta{}, + Spec: k8sv1alpha1.ServerClusterTemplateMetaSpec{ + Imported: imported, + Reclaim: reclaim, + Name: string(sctCR.GetUID()), + ServerClusterTemplateName: sctCR.Name, + Vnets: sctCR.Spec.Vnets, + ServerClusterTemplateCRGeneration: sctCR.GetGeneration(), + }, + } + + return sctMeta, nil +} + +// ServerClusterTemplateMetaToNetris converts the ServerClusterTemplateMeta resource to Netris ServerClusterTemplateW type for adding. +func ServerClusterTemplateMetaToNetris(sctMeta *k8sv1alpha1.ServerClusterTemplateMeta) (*serverclustertemplate.ServerClusterTemplateW, error) { + sctAdd := &serverclustertemplate.ServerClusterTemplateW{ + Name: sctMeta.Spec.ServerClusterTemplateName, + Vnets: sctMeta.Spec.Vnets, + } + return sctAdd, nil +} + +// ServerClusterTemplateMetaToNetrisUpdate converts the ServerClusterTemplateMeta resource to Netris ServerClusterTemplateW type for updating. +func ServerClusterTemplateMetaToNetrisUpdate(sctMeta *k8sv1alpha1.ServerClusterTemplateMeta) (*serverclustertemplate.ServerClusterTemplateW, error) { + sctUpdate := &serverclustertemplate.ServerClusterTemplateW{ + Name: sctMeta.Spec.ServerClusterTemplateName, + Vnets: sctMeta.Spec.Vnets, + } + return sctUpdate, nil +} + +func compareServerClusterTemplateMetaAPIServerClusterTemplate(sctMeta *k8sv1alpha1.ServerClusterTemplateMeta, apiSCT *serverclustertemplate.ServerClusterTemplate) bool { + if sctMeta.Spec.ServerClusterTemplateName != apiSCT.Name { + return false + } + + // Compare Vnets by marshaling to JSON and comparing + metaVnetsJSON, err := json.Marshal(sctMeta.Spec.Vnets) + if err != nil { + return false + } + apiVnetsJSON, err := json.Marshal(apiSCT.Vnets) + if err != nil { + return false + } + + // Compare as JSON strings + var metaVnets, apiVnets interface{} + if err := json.Unmarshal(metaVnetsJSON, &metaVnets); err != nil { + return false + } + if err := json.Unmarshal(apiVnetsJSON, &apiVnets); err != nil { + return false + } + + changelog, _ := diff.Diff(metaVnets, apiVnets) + return len(changelog) <= 0 +} + +func serverClusterTemplateCompareFieldsForNewMeta(sctCR *k8sv1alpha1.ServerClusterTemplate, sctMeta *k8sv1alpha1.ServerClusterTemplateMeta) bool { + imported := false + reclaim := false + if i, ok := sctCR.GetAnnotations()["resource.k8s.netris.ai/import"]; ok && i == "true" { + imported = true + } + if i, ok := sctCR.GetAnnotations()["resource.k8s.netris.ai/reclaimPolicy"]; ok && i == "retain" { + reclaim = true + } + return sctCR.GetGeneration() != sctMeta.Spec.ServerClusterTemplateCRGeneration || imported != sctMeta.Spec.Imported || reclaim != sctMeta.Spec.Reclaim +} + +func serverClusterTemplateMustUpdateAnnotations(sctCR *k8sv1alpha1.ServerClusterTemplate) bool { + update := false + if i, ok := sctCR.GetAnnotations()["resource.k8s.netris.ai/import"]; !(ok && (i == "true" || i == "false")) { + update = true + } + if i, ok := sctCR.GetAnnotations()["resource.k8s.netris.ai/reclaimPolicy"]; !(ok && (i == "retain" || i == "delete")) { + update = true + } + return update +} + +func serverClusterTemplateUpdateDefaultAnnotations(sctCR *k8sv1alpha1.ServerClusterTemplate) { + imported := "false" + reclaim := "delete" + if i, ok := sctCR.GetAnnotations()["resource.k8s.netris.ai/import"]; ok && i == "true" { + imported = "true" + } + if i, ok := sctCR.GetAnnotations()["resource.k8s.netris.ai/reclaimPolicy"]; ok && i == "retain" { + reclaim = "retain" + } + annotations := sctCR.GetAnnotations() + annotations["resource.k8s.netris.ai/import"] = imported + annotations["resource.k8s.netris.ai/reclaimPolicy"] = reclaim + sctCR.SetAnnotations(annotations) +} + diff --git a/controllers/serverclustertemplatemeta_controller.go b/controllers/serverclustertemplatemeta_controller.go new file mode 100644 index 0000000..76799a6 --- /dev/null +++ b/controllers/serverclustertemplatemeta_controller.go @@ -0,0 +1,243 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + "time" + + "go.uber.org/zap/zapcore" + "k8s.io/apimachinery/pkg/api/errors" + + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + k8sv1alpha1 "github.com/netrisai/netris-operator/api/v1alpha1" + "github.com/netrisai/netris-operator/netrisstorage" + "github.com/netrisai/netriswebapi/http" + api "github.com/netrisai/netriswebapi/v2" + "github.com/netrisai/netriswebapi/v2/types/serverclustertemplate" +) + +// ServerClusterTemplateMetaReconciler reconciles a ServerClusterTemplateMeta object +type ServerClusterTemplateMetaReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + Cred *api.Clientset + NStorage *netrisstorage.Storage +} + +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=serverclustertemplatemeta,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=serverclustertemplatemeta/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=serverclustertemplatemeta/finalizers,verbs=update + +// Reconcile . +func (r *ServerClusterTemplateMetaReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + debugLogger := r.Log.WithValues("name", req.NamespacedName).V(int(zapcore.WarnLevel)) + + sctMeta := &k8sv1alpha1.ServerClusterTemplateMeta{} + sctCR := &k8sv1alpha1.ServerClusterTemplate{} + sctMetaCtx, sctMetaCancel := context.WithTimeout(cntxt, contextTimeout) + defer sctMetaCancel() + if err := r.Get(sctMetaCtx, req.NamespacedName, sctMeta); err != nil { + if errors.IsNotFound(err) { + debugLogger.Info(err.Error()) + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + logger := r.Log.WithValues("name", fmt.Sprintf("%s/%s", req.NamespacedName.Namespace, sctMeta.Spec.ServerClusterTemplateName)) + debugLogger = logger.V(int(zapcore.WarnLevel)) + + u := uniReconciler{ + Client: r.Client, + Logger: logger, + DebugLogger: debugLogger, + Cred: r.Cred, + NStorage: r.NStorage, + } + + provisionState := "Provisioning" + + sctNN := req.NamespacedName + sctNN.Name = sctMeta.Spec.ServerClusterTemplateName + sctNNCtx, sctNNCancel := context.WithTimeout(cntxt, contextTimeout) + defer sctNNCancel() + if err := r.Get(sctNNCtx, sctNN, sctCR); err != nil { + if errors.IsNotFound(err) { + debugLogger.Info(err.Error()) + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + if sctMeta.DeletionTimestamp != nil { + return ctrl.Result{}, nil + } + + if sctMeta.Spec.ID == 0 { + debugLogger.Info("ID Not found in meta") + if sctMeta.Spec.Imported { + logger.Info("Importing serverclustertemplate") + debugLogger.Info("Imported yaml mode. Finding ServerClusterTemplate by name") + // Note: ServerClusterTemplate doesn't have a storage, so we need to fetch from API + templates, err := r.Cred.ServerClusterTemplate().Get() + if err != nil { + logger.Error(fmt.Errorf("{Get ServerClusterTemplates} %s", err), "") + return u.patchServerClusterTemplateStatus(sctCR, "Failure", err.Error()) + } + for _, template := range templates { + if template.Name == sctMeta.Spec.ServerClusterTemplateName { + debugLogger.Info("Imported yaml mode. ServerClusterTemplate found") + sctMeta.Spec.ID = template.ID + sctMeta.Spec.Name = template.Name + sctMeta.Spec.Vnets = template.Vnets + sctCR.Status.ModifiedDate = metav1.NewTime(time.Unix(int64(template.ModifiedDate/1000), 0)) + sctMetaPatchCtx, sctMetaPatchCancel := context.WithTimeout(cntxt, contextTimeout) + defer sctMetaPatchCancel() + err := r.Patch(sctMetaPatchCtx, sctMeta.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{patch sctmeta.Spec.ID} %s", err), "") + return u.patchServerClusterTemplateStatus(sctCR, "Failure", err.Error()) + } + debugLogger.Info("Imported yaml mode. ID patched") + logger.Info("ServerClusterTemplate imported") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + } + logger.Info("ServerClusterTemplate not found for import") + debugLogger.Info("Imported yaml mode. ServerClusterTemplate not found") + } + + logger.Info("Creating ServerClusterTemplate") + if _, err, errMsg := r.createServerClusterTemplate(sctMeta); err != nil { + logger.Error(fmt.Errorf("{createServerClusterTemplate} %s", err), "") + return u.patchServerClusterTemplateStatus(sctCR, "Failure", errMsg.Error()) + } + logger.Info("ServerClusterTemplate Created") + } else { + apiSCT, err := r.Cred.ServerClusterTemplate().GetByID(sctMeta.Spec.ID) + if err != nil || apiSCT == nil { + debugLogger.Info("ServerClusterTemplate not found in Netris") + debugLogger.Info("Going to create ServerClusterTemplate") + logger.Info("Creating ServerClusterTemplate") + if _, err, errMsg := r.createServerClusterTemplate(sctMeta); err != nil { + logger.Error(fmt.Errorf("{createServerClusterTemplate} %s", err), "") + return u.patchServerClusterTemplateStatus(sctCR, "Failure", errMsg.Error()) + } + logger.Info("ServerClusterTemplate Created") + } else { + provisionState = "Active" + sctCR.Status.ModifiedDate = metav1.NewTime(time.Unix(int64(apiSCT.ModifiedDate/1000), 0)) + debugLogger.Info("Comparing ServerClusterTemplateMeta with Netris ServerClusterTemplate") + if ok := compareServerClusterTemplateMetaAPIServerClusterTemplate(sctMeta, apiSCT); ok { + debugLogger.Info("Nothing Changed") + } else { + debugLogger.Info("Something changed") + debugLogger.Info("Go to update ServerClusterTemplate in Netris") + logger.Info("Updating ServerClusterTemplate") + updateSCT, err := ServerClusterTemplateMetaToNetrisUpdate(sctMeta) + if err != nil { + logger.Error(fmt.Errorf("{ServerClusterTemplateMetaToNetrisUpdate} %s", err), "") + return u.patchServerClusterTemplateStatus(sctCR, "Failure", err.Error()) + } + _, err, errMsg := r.updateServerClusterTemplate(sctMeta.Spec.ID, updateSCT) + if err != nil { + logger.Error(fmt.Errorf("{updateServerClusterTemplate} %s", err), "") + return u.patchServerClusterTemplateStatus(sctCR, "Failure", errMsg.Error()) + } + logger.Info("ServerClusterTemplate Updated") + } + } + } + return u.patchServerClusterTemplateStatus(sctCR, provisionState, "Success") +} + +// SetupWithManager . +func (r *ServerClusterTemplateMetaReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&k8sv1alpha1.ServerClusterTemplateMeta{}). + Complete(r) +} + +func (r *ServerClusterTemplateMetaReconciler) createServerClusterTemplate(sctMeta *k8sv1alpha1.ServerClusterTemplateMeta) (ctrl.Result, error, error) { + debugLogger := r.Log.WithValues( + "name", fmt.Sprintf("%s/%s", sctMeta.Namespace, sctMeta.Spec.ServerClusterTemplateName), + "sctName", sctMeta.Spec.ServerClusterTemplateName, + ).V(int(zapcore.WarnLevel)) + + sctAdd, err := ServerClusterTemplateMetaToNetris(sctMeta) + if err != nil { + return ctrl.Result{}, err, err + } + reply, err := r.Cred.ServerClusterTemplate().Add(sctAdd) + if err != nil { + return ctrl.Result{}, err, err + } + resp, err := http.ParseAPIResponse(reply.Data) + if err != nil { + return ctrl.Result{}, err, err + } + if !resp.IsSuccess { + return ctrl.Result{}, fmt.Errorf(resp.Message), fmt.Errorf(resp.Message) + } + + idStruct := struct { + ID int `json:"id"` + }{} + err = http.Decode(resp.Data, &idStruct) + if err != nil { + return ctrl.Result{}, err, err + } + + debugLogger.Info("ServerClusterTemplate Created", "id", idStruct.ID) + + sctMeta.Spec.ID = idStruct.ID + + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + err = r.Patch(ctx, sctMeta.DeepCopyObject(), client.Merge, &client.PatchOptions{}) // requeue + if err != nil { + return ctrl.Result{}, err, err + } + + debugLogger.Info("ID patched to meta", "id", idStruct.ID) + return ctrl.Result{}, nil, nil +} + +func (r *ServerClusterTemplateMetaReconciler) updateServerClusterTemplate(id int, sct *serverclustertemplate.ServerClusterTemplateW) (ctrl.Result, error, error) { + reply, err := r.Cred.ServerClusterTemplate().Update(id, sct) + if err != nil { + return ctrl.Result{}, fmt.Errorf("{updateServerClusterTemplate} %s", err), err + } + resp, err := http.ParseAPIResponse(reply.Data) + if err != nil { + return ctrl.Result{}, err, err + } + if !resp.IsSuccess { + return ctrl.Result{}, fmt.Errorf("{updateServerClusterTemplate} %s", fmt.Errorf(resp.Message)), fmt.Errorf(resp.Message) + } + + return ctrl.Result{}, nil, nil +} + diff --git a/deploy/charts/netris-operator/crds/k8s.netris.ai_serverclustertemplatemeta.yaml b/deploy/charts/netris-operator/crds/k8s.netris.ai_serverclustertemplatemeta.yaml new file mode 100644 index 0000000..a41892a --- /dev/null +++ b/deploy/charts/netris-operator/crds/k8s.netris.ai_serverclustertemplatemeta.yaml @@ -0,0 +1,73 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: serverclustertemplatemeta.k8s.netris.ai +spec: + group: k8s.netris.ai + names: + kind: ServerClusterTemplateMeta + listKind: ServerClusterTemplateMetaList + plural: serverclustertemplatemeta + singular: serverclustertemplatemeta + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ServerClusterTemplateMeta is the Schema for the serverclustertemplatemeta API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ServerClusterTemplateMetaSpec defines the desired state of ServerClusterTemplateMeta + properties: + id: + format: int64 + type: integer + imported: + type: boolean + name: + type: string + reclaimPolicy: + type: boolean + serverClusterTemplateGeneration: + format: int64 + type: integer + serverClusterTemplateName: + type: string + vnets: + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + type: object + status: + description: ServerClusterTemplateMetaStatus defines the observed state of ServerClusterTemplateMeta + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/deploy/charts/netris-operator/crds/k8s.netris.ai_serverclustertemplates.yaml b/deploy/charts/netris-operator/crds/k8s.netris.ai_serverclustertemplates.yaml new file mode 100644 index 0000000..ccc9ed3 --- /dev/null +++ b/deploy/charts/netris-operator/crds/k8s.netris.ai_serverclustertemplates.yaml @@ -0,0 +1,86 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: serverclustertemplates.k8s.netris.ai +spec: + group: k8s.netris.ai + names: + kind: ServerClusterTemplate + listKind: ServerClusterTemplateList + plural: serverclustertemplates + singular: serverclustertemplate + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.name + name: Name + type: string + - jsonPath: .status.status + name: Status + type: string + - jsonPath: .status.modified + name: Modified + priority: 1 + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ServerClusterTemplate is the Schema for the serverclustertemplates API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ServerClusterTemplateSpec defines the desired state of ServerClusterTemplate + properties: + vnets: + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + type: object + status: + description: ServerClusterTemplateStatus defines the observed state of ServerClusterTemplate + properties: + message: + type: string + modified: + format: date-time + type: string + status: + description: 'INSERT ADDITIONAL STATUS FIELD - define observed state + of cluster Important: Run "make" to regenerate code after modifying + this file' + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/deploy/charts/netris-operator/templates/rbac.yaml b/deploy/charts/netris-operator/templates/rbac.yaml index 9e55d99..b0a8db6 100644 --- a/deploy/charts/netris-operator/templates/rbac.yaml +++ b/deploy/charts/netris-operator/templates/rbac.yaml @@ -596,6 +596,58 @@ rules: - get - patch - update + - apiGroups: + - k8s.netris.ai + resources: + - serverclustertemplates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - k8s.netris.ai + resources: + - serverclustertemplates/finalizers + verbs: + - update + - apiGroups: + - k8s.netris.ai + resources: + - serverclustertemplates/status + verbs: + - get + - patch + - update + - apiGroups: + - k8s.netris.ai + resources: + - serverclustertemplatemeta + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - k8s.netris.ai + resources: + - serverclustertemplatemeta/finalizers + verbs: + - update + - apiGroups: + - k8s.netris.ai + resources: + - serverclustertemplatemeta/status + verbs: + - get + - patch + - update - apiGroups: - k8s.netris.ai resources: diff --git a/main.go b/main.go index ee0ae8b..5a6f6e2 100644 --- a/main.go +++ b/main.go @@ -295,6 +295,26 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "ServerMeta") os.Exit(1) } + if err = (&controllers.ServerClusterTemplateReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("ServerClusterTemplate"), + Scheme: mgr.GetScheme(), + Cred: cred, + NStorage: nStorage, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ServerClusterTemplate") + os.Exit(1) + } + if err = (&controllers.ServerClusterTemplateMetaReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("ServerClusterTemplateMeta"), + Scheme: mgr.GetScheme(), + Cred: cred, + NStorage: nStorage, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ServerClusterTemplateMeta") + os.Exit(1) + } if err = (&controllers.SwitchReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("Switch"), diff --git a/samples/kustomization.yaml b/samples/kustomization.yaml index 95500f3..e17f361 100644 --- a/samples/kustomization.yaml +++ b/samples/kustomization.yaml @@ -6,6 +6,7 @@ resources: - subnet.yaml - softgate.yaml - server.yaml + - serverclustertemplate.yaml - switch.yaml - controller.yaml - vnet.yaml diff --git a/samples/serverclustertemplate.yaml b/samples/serverclustertemplate.yaml new file mode 100644 index 0000000..8f86e13 --- /dev/null +++ b/samples/serverclustertemplate.yaml @@ -0,0 +1,11 @@ +apiVersion: k8s.netris.ai/v1alpha1 +kind: ServerClusterTemplate +metadata: + name: my-server-cluster-template +spec: + vnets: + - name: vnet1 + site: site1 + - name: vnet2 + site: site2 + From 249d7335a4b7f235166aa60da4734f972cd771db Mon Sep 17 00:00:00 2001 From: Vasyl Saienko Date: Fri, 28 Nov 2025 14:55:40 +0200 Subject: [PATCH 6/6] Implement serverclusters --- api/v1alpha1/servercluster_types.go | 77 +++++ api/v1alpha1/serverclustermeta_types.go | 77 +++++ api/v1alpha1/zz_generated.deepcopy.go | 189 ++++++++++++ .../k8s.netris.ai_serverclustermeta.yaml | 84 ++++++ .../bases/k8s.netris.ai_serverclusters.yaml | 107 +++++++ config/rbac/role.yaml | 56 +++- controllers/controller.go | 15 + controllers/servercluster_controller.go | 227 +++++++++++++++ controllers/servercluster_translations.go | 204 +++++++++++++ controllers/serverclustermeta_controller.go | 275 ++++++++++++++++++ .../serverclustertemplate_translations.go | 35 +-- .../serverclustertemplatemeta_controller.go | 26 +- .../crds/k8s.netris.ai_serverclustermeta.yaml | 84 ++++++ .../crds/k8s.netris.ai_serverclusters.yaml | 107 +++++++ .../netris-operator/templates/rbac.yaml | 52 ++++ main.go | 21 ++ samples/kustomization.yaml | 1 + samples/servercluster.yaml | 13 + 18 files changed, 1620 insertions(+), 30 deletions(-) create mode 100644 api/v1alpha1/servercluster_types.go create mode 100644 api/v1alpha1/serverclustermeta_types.go create mode 100644 config/crd/bases/k8s.netris.ai_serverclustermeta.yaml create mode 100644 config/crd/bases/k8s.netris.ai_serverclusters.yaml create mode 100644 controllers/servercluster_controller.go create mode 100644 controllers/servercluster_translations.go create mode 100644 controllers/serverclustermeta_controller.go create mode 100644 deploy/charts/netris-operator/crds/k8s.netris.ai_serverclustermeta.yaml create mode 100644 deploy/charts/netris-operator/crds/k8s.netris.ai_serverclusters.yaml create mode 100644 samples/servercluster.yaml diff --git a/api/v1alpha1/servercluster_types.go b/api/v1alpha1/servercluster_types.go new file mode 100644 index 0000000..2d3ed66 --- /dev/null +++ b/api/v1alpha1/servercluster_types.go @@ -0,0 +1,77 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ServerClusterSpec defines the desired state of ServerCluster +type ServerClusterSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + Admin string `json:"admin"` + Site string `json:"site"` + VPC string `json:"vpc"` + Template string `json:"template"` + Tags []string `json:"tags,omitempty"` +} + +// ServerClusterStatus defines the observed state of ServerCluster +type ServerClusterStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + Status string `json:"status,omitempty"` + Message string `json:"message,omitempty"` + ModifiedDate metav1.Time `json:"modified,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Admin",type=string,JSONPath=`.spec.admin` +// +kubebuilder:printcolumn:name="Site",type=string,JSONPath=`.spec.site` +// +kubebuilder:printcolumn:name="VPC",type=string,JSONPath=`.spec.vpc` +// +kubebuilder:printcolumn:name="Template",type=string,JSONPath=`.spec.template` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Modified",type=date,JSONPath=`.status.modified`,priority=1 +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// ServerCluster is the Schema for the serverclusters API +type ServerCluster struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ServerClusterSpec `json:"spec"` + Status ServerClusterStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ServerClusterList contains a list of ServerCluster +type ServerClusterList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ServerCluster `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ServerCluster{}, &ServerClusterList{}) +} + diff --git a/api/v1alpha1/serverclustermeta_types.go b/api/v1alpha1/serverclustermeta_types.go new file mode 100644 index 0000000..3c0e5d8 --- /dev/null +++ b/api/v1alpha1/serverclustermeta_types.go @@ -0,0 +1,77 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ServerClusterMetaSpec defines the desired state of ServerClusterMeta +type ServerClusterMetaSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + Imported bool `json:"imported"` + Reclaim bool `json:"reclaimPolicy"` + ServerClusterCRGeneration int64 `json:"serverClusterGeneration"` + ID int `json:"id"` + Name string `json:"name"` + ServerClusterName string `json:"serverClusterName"` + AdminID int `json:"adminId"` + Admin string `json:"admin"` + SiteID int `json:"siteId"` + Site string `json:"site"` + VPCID int `json:"vpcId"` + VPC string `json:"vpc"` + TemplateID int `json:"templateId"` + Template string `json:"template"` + Tags []string `json:"tags"` +} + +// ServerClusterMetaStatus defines the observed state of ServerClusterMeta +type ServerClusterMetaStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// ServerClusterMeta is the Schema for the serverclustermeta API +type ServerClusterMeta struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ServerClusterMetaSpec `json:"spec,omitempty"` + Status ServerClusterMetaStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ServerClusterMetaList contains a list of ServerClusterMeta +type ServerClusterMetaList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ServerClusterMeta `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ServerClusterMeta{}, &ServerClusterMetaList{}) +} + diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index adc2754..c9877d6 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -3135,3 +3135,192 @@ func (in *ServerClusterTemplateMetaList) DeepCopyObject() runtime.Object { } return nil } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerClusterSpec) DeepCopyInto(out *ServerClusterSpec) { + *out = *in + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerClusterSpec. +func (in *ServerClusterSpec) DeepCopy() *ServerClusterSpec { + if in == nil { + return nil + } + out := new(ServerClusterSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerClusterStatus) DeepCopyInto(out *ServerClusterStatus) { + *out = *in + in.ModifiedDate.DeepCopyInto(&out.ModifiedDate) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerClusterStatus. +func (in *ServerClusterStatus) DeepCopy() *ServerClusterStatus { + if in == nil { + return nil + } + out := new(ServerClusterStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerCluster) DeepCopyInto(out *ServerCluster) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerCluster. +func (in *ServerCluster) DeepCopy() *ServerCluster { + if in == nil { + return nil + } + out := new(ServerCluster) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServerCluster) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerClusterList) DeepCopyInto(out *ServerClusterList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ServerCluster, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerClusterList. +func (in *ServerClusterList) DeepCopy() *ServerClusterList { + if in == nil { + return nil + } + out := new(ServerClusterList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServerClusterList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerClusterMetaSpec) DeepCopyInto(out *ServerClusterMetaSpec) { + *out = *in + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerClusterMetaSpec. +func (in *ServerClusterMetaSpec) DeepCopy() *ServerClusterMetaSpec { + if in == nil { + return nil + } + out := new(ServerClusterMetaSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerClusterMetaStatus) DeepCopyInto(out *ServerClusterMetaStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerClusterMetaStatus. +func (in *ServerClusterMetaStatus) DeepCopy() *ServerClusterMetaStatus { + if in == nil { + return nil + } + out := new(ServerClusterMetaStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerClusterMeta) DeepCopyInto(out *ServerClusterMeta) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerClusterMeta. +func (in *ServerClusterMeta) DeepCopy() *ServerClusterMeta { + if in == nil { + return nil + } + out := new(ServerClusterMeta) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServerClusterMeta) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerClusterMetaList) DeepCopyInto(out *ServerClusterMetaList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ServerClusterMeta, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerClusterMetaList. +func (in *ServerClusterMetaList) DeepCopy() *ServerClusterMetaList { + if in == nil { + return nil + } + out := new(ServerClusterMetaList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServerClusterMetaList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/config/crd/bases/k8s.netris.ai_serverclustermeta.yaml b/config/crd/bases/k8s.netris.ai_serverclustermeta.yaml new file mode 100644 index 0000000..cf3a069 --- /dev/null +++ b/config/crd/bases/k8s.netris.ai_serverclustermeta.yaml @@ -0,0 +1,84 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: serverclustermeta.k8s.netris.ai +spec: + group: k8s.netris.ai + names: + kind: ServerClusterMeta + listKind: ServerClusterMetaList + plural: serverclustermeta + singular: serverclustermeta + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ServerClusterMeta is the Schema for the serverclustermeta API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ServerClusterMetaSpec defines the desired state of ServerClusterMeta + properties: + admin: + type: string + adminId: + type: integer + imported: + type: boolean + name: + type: string + reclaimPolicy: + type: boolean + serverClusterGeneration: + type: integer + serverClusterName: + type: string + site: + type: string + siteId: + type: integer + tags: + items: + type: string + type: array + template: + type: string + templateId: + type: integer + vpc: + type: string + vpcId: + type: integer + type: object + status: + description: ServerClusterMetaStatus defines the observed state of ServerClusterMeta + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/config/crd/bases/k8s.netris.ai_serverclusters.yaml b/config/crd/bases/k8s.netris.ai_serverclusters.yaml new file mode 100644 index 0000000..ff81de9 --- /dev/null +++ b/config/crd/bases/k8s.netris.ai_serverclusters.yaml @@ -0,0 +1,107 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: serverclusters.k8s.netris.ai +spec: + group: k8s.netris.ai + names: + kind: ServerCluster + listKind: ServerClusterList + plural: serverclusters + singular: servercluster + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.admin + name: Admin + type: string + - jsonPath: .spec.site + name: Site + type: string + - jsonPath: .spec.vpc + name: VPC + type: string + - jsonPath: .spec.template + name: Template + type: string + - jsonPath: .status.status + name: Status + type: string + - jsonPath: .status.modified + name: Modified + priority: 1 + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ServerCluster is the Schema for the serverclusters API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ServerClusterSpec defines the desired state of ServerCluster + properties: + admin: + type: string + site: + type: string + tags: + items: + type: string + type: array + template: + type: string + vpc: + type: string + required: + - admin + - site + - template + - vpc + type: object + status: + description: ServerClusterStatus defines the observed state of ServerCluster + properties: + message: + type: string + modified: + format: date-time + type: string + status: + description: 'INSERT ADDITIONAL STATUS FIELD - define observed state + of cluster Important: Run "make" to regenerate code after modifying + this file' + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 75c6ac9..72bf039 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -649,12 +649,64 @@ rules: - get - patch - update + - apiGroups: + - k8s.netris.ai + resources: + - serverclusters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - k8s.netris.ai + resources: + - serverclusters/finalizers + verbs: + - update + - apiGroups: + - k8s.netris.ai + resources: + - serverclusters/status + verbs: + - get + - patch + - update + - apiGroups: + - k8s.netris.ai + resources: + - serverclustermeta + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - k8s.netris.ai + resources: + - serverclustermeta/finalizers + verbs: + - update + - apiGroups: + - k8s.netris.ai + resources: + - serverclustermeta/status + verbs: + - get + - patch + - update - apiGroups: - k8s.netris.ai resources: - subnetmeta - verbs: - - create + verbs: + - create - delete - get - list diff --git a/controllers/controller.go b/controllers/controller.go index f949ea1..91d0cb9 100644 --- a/controllers/controller.go +++ b/controllers/controller.go @@ -367,6 +367,21 @@ func (u *uniReconciler) patchServerClusterTemplateStatus(sct *k8sv1alpha1.Server return ctrl.Result{RequeueAfter: requeueInterval}, nil } +func (u *uniReconciler) patchServerClusterStatus(sc *k8sv1alpha1.ServerCluster, status, message string) (ctrl.Result, error) { + u.DebugLogger.Info("Patching Status", "status", status, "message", message) + + sc.Status.Status = status + sc.Status.Message = message + + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + err := u.Status().Patch(ctx, sc.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + u.DebugLogger.Info("{r.Status().Patch}", "error", err, "action", "status update") + } + return ctrl.Result{RequeueAfter: requeueInterval}, nil +} + func (u *uniReconciler) patchServer(server *k8sv1alpha1.Server) (ctrl.Result, error) { u.DebugLogger.Info("Patching") ctx, cancel := context.WithTimeout(cntxt, contextTimeout) diff --git a/controllers/servercluster_controller.go b/controllers/servercluster_controller.go new file mode 100644 index 0000000..c9afa3e --- /dev/null +++ b/controllers/servercluster_controller.go @@ -0,0 +1,227 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + + "go.uber.org/zap/zapcore" + "k8s.io/apimachinery/pkg/api/errors" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + k8sv1alpha1 "github.com/netrisai/netris-operator/api/v1alpha1" + "github.com/netrisai/netris-operator/netrisstorage" + "github.com/netrisai/netriswebapi/http" + api "github.com/netrisai/netriswebapi/v2" +) + +// ServerClusterReconciler reconciles a ServerCluster object +type ServerClusterReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + Cred *api.Clientset + NStorage *netrisstorage.Storage +} + +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=serverclusters,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=serverclusters/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=serverclusters/finalizers,verbs=update + +// Reconcile servercluster events +func (r *ServerClusterReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + logger := r.Log.WithValues("name", req.NamespacedName) + debugLogger := logger.V(int(zapcore.WarnLevel)) + scCR := &k8sv1alpha1.ServerCluster{} + + u := uniReconciler{ + Client: r.Client, + Logger: logger, + DebugLogger: debugLogger, + Cred: r.Cred, + NStorage: r.NStorage, + } + + scCtx, scCancel := context.WithTimeout(cntxt, contextTimeout) + defer scCancel() + if err := r.Get(scCtx, req.NamespacedName, scCR); err != nil { + if errors.IsNotFound(err) { + debugLogger.Info(err.Error()) + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + scMetaNamespaced := req.NamespacedName + scMetaNamespaced.Name = string(scCR.GetUID()) + scMeta := &k8sv1alpha1.ServerClusterMeta{} + metaFound := true + scMetaCtx, scMetaCancel := context.WithTimeout(cntxt, contextTimeout) + defer scMetaCancel() + if err := r.Get(scMetaCtx, scMetaNamespaced, scMeta); err != nil { + if errors.IsNotFound(err) { + debugLogger.Info(err.Error()) + metaFound = false + scMeta = nil + } else { + return ctrl.Result{}, err + } + } + + if scCR.DeletionTimestamp != nil { + logger.Info("Go to delete") + _, err := r.deleteServerCluster(scCR, scMeta) + if err != nil { + logger.Error(fmt.Errorf("{deleteServerCluster} %s", err), "") + return u.patchServerClusterStatus(scCR, "Failure", err.Error()) + } + logger.Info("ServerCluster deleted") + return ctrl.Result{}, nil + } + + if serverClusterMustUpdateAnnotations(scCR) { + debugLogger.Info("Setting default annotations") + serverClusterUpdateDefaultAnnotations(scCR) + scUpdateCtx, scUpdateCancel := context.WithTimeout(cntxt, contextTimeout) + defer scUpdateCancel() + err := r.Patch(scUpdateCtx, scCR.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{Patch ServerCluster default annotations} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + return ctrl.Result{}, nil + } + + if metaFound { + debugLogger.Info("Meta found") + if serverClusterCompareFieldsForNewMeta(scCR, scMeta) { + debugLogger.Info("Generating New Meta") + scID := scMeta.Spec.ID + newScMeta, err := r.ServerClusterToServerClusterMeta(scCR) + if err != nil { + logger.Error(fmt.Errorf("{ServerClusterToServerClusterMeta} %s", err), "") + return u.patchServerClusterStatus(scCR, "Failure", err.Error()) + } + scMeta.Spec = newScMeta.DeepCopy().Spec + scMeta.Spec.ID = scID + scMeta.Spec.ServerClusterCRGeneration = scCR.GetGeneration() + + scMetaUpdateCtx, scMetaUpdateCancel := context.WithTimeout(cntxt, contextTimeout) + defer scMetaUpdateCancel() + err = r.Update(scMetaUpdateCtx, scMeta.DeepCopyObject(), &client.UpdateOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{scMeta Update} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + } + } else { + debugLogger.Info("Meta not found") + if scCR.GetFinalizers() == nil { + scCR.SetFinalizers([]string{"resource.k8s.netris.ai/delete"}) + scPatchCtx, scPatchCancel := context.WithTimeout(cntxt, contextTimeout) + defer scPatchCancel() + err := r.Patch(scPatchCtx, scCR.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{Patch ServerCluster Finalizer} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + return ctrl.Result{}, nil + } + + scMeta, err := r.ServerClusterToServerClusterMeta(scCR) + if err != nil { + logger.Error(fmt.Errorf("{ServerClusterToServerClusterMeta} %s", err), "") + return u.patchServerClusterStatus(scCR, "Failure", err.Error()) + } + + scMeta.Spec.ServerClusterCRGeneration = scCR.GetGeneration() + + scMetaCreateCtx, scMetaCreateCancel := context.WithTimeout(cntxt, contextTimeout) + defer scMetaCreateCancel() + if err := r.Create(scMetaCreateCtx, scMeta.DeepCopyObject(), &client.CreateOptions{}); err != nil { + logger.Error(fmt.Errorf("{scMeta Create} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + } + + return ctrl.Result{RequeueAfter: requeueInterval}, nil +} + +func (r *ServerClusterReconciler) deleteServerCluster(scCR *k8sv1alpha1.ServerCluster, scMeta *k8sv1alpha1.ServerClusterMeta) (ctrl.Result, error) { + if scMeta != nil && scMeta.Spec.ID > 0 && !scMeta.Spec.Reclaim { + reply, err := r.Cred.ServerCluster().Delete(scMeta.Spec.ID) + if err != nil { + return ctrl.Result{}, fmt.Errorf("{deleteServerCluster} %s", err) + } + resp, err := http.ParseAPIResponse(reply.Data) + if err != nil { + return ctrl.Result{}, err + } + if !resp.IsSuccess { + if resp.Message != "Invalid ServerCluster ID" { + return ctrl.Result{}, fmt.Errorf("{deleteServerCluster} %s", fmt.Errorf(resp.Message)) + } + } + } + return r.deleteCRs(scCR, scMeta) +} + +func (r *ServerClusterReconciler) deleteCRs(scCR *k8sv1alpha1.ServerCluster, scMeta *k8sv1alpha1.ServerClusterMeta) (ctrl.Result, error) { + if scMeta != nil { + _, err := r.deleteServerClusterMetaCR(scMeta) + if err != nil { + return ctrl.Result{}, fmt.Errorf("{deleteCRs} %s", err) + } + } + + return r.deleteServerClusterCR(scCR) +} + +func (r *ServerClusterReconciler) deleteServerClusterCR(scCR *k8sv1alpha1.ServerCluster) (ctrl.Result, error) { + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + scCR.ObjectMeta.SetFinalizers(nil) + scCR.SetFinalizers(nil) + if err := r.Update(ctx, scCR.DeepCopyObject(), &client.UpdateOptions{}); err != nil { + return ctrl.Result{}, fmt.Errorf("{deleteServerClusterCR} %s", err) + } + + return ctrl.Result{}, nil +} + +func (r *ServerClusterReconciler) deleteServerClusterMetaCR(scMeta *k8sv1alpha1.ServerClusterMeta) (ctrl.Result, error) { + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + if err := r.Delete(ctx, scMeta.DeepCopyObject(), &client.DeleteOptions{}); err != nil { + return ctrl.Result{}, fmt.Errorf("{deleteServerClusterMetaCR} %s", err) + } + + return ctrl.Result{}, nil +} + +// SetupWithManager Resources +func (r *ServerClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&k8sv1alpha1.ServerCluster{}). + Complete(r) +} + diff --git a/controllers/servercluster_translations.go b/controllers/servercluster_translations.go new file mode 100644 index 0000000..c36a9fb --- /dev/null +++ b/controllers/servercluster_translations.go @@ -0,0 +1,204 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "fmt" + + k8sv1alpha1 "github.com/netrisai/netris-operator/api/v1alpha1" + "github.com/netrisai/netriswebapi/v2/types/servercluster" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ServerClusterToServerClusterMeta converts the ServerCluster resource to ServerClusterMeta type. +func (r *ServerClusterReconciler) ServerClusterToServerClusterMeta(scCR *k8sv1alpha1.ServerCluster) (*k8sv1alpha1.ServerClusterMeta, error) { + imported := false + reclaim := false + if i, ok := scCR.GetAnnotations()["resource.k8s.netris.ai/import"]; ok && i == "true" { + imported = true + } + if i, ok := scCR.GetAnnotations()["resource.k8s.netris.ai/reclaimPolicy"]; ok && i == "retain" { + reclaim = true + } + + adminID := 0 + if tenant, ok := r.NStorage.TenantsStorage.FindByName(scCR.Spec.Admin); ok { + adminID = tenant.ID + } else { + return nil, fmt.Errorf("'%s' admin tenant not found", scCR.Spec.Admin) + } + + siteID := 0 + if site, ok := r.NStorage.SitesStorage.FindByName(scCR.Spec.Site); ok { + siteID = site.ID + } else { + return nil, fmt.Errorf("'%s' site not found", scCR.Spec.Site) + } + + vpcID := 0 + if vpc, ok := r.NStorage.VPCStorage.FindByName(scCR.Spec.VPC); ok { + vpcID = vpc.ID + } else { + return nil, fmt.Errorf("'%s' vpc not found", scCR.Spec.VPC) + } + + templateID := 0 + templates, err := r.Cred.ServerClusterTemplate().Get() + if err != nil { + return nil, err + } + for _, template := range templates { + if template.Name == scCR.Spec.Template { + templateID = template.ID + break + } + } + if templateID == 0 && scCR.Spec.Template != "" { + return nil, fmt.Errorf("'%s' template not found", scCR.Spec.Template) + } + + scMeta := &k8sv1alpha1.ServerClusterMeta{ + ObjectMeta: metav1.ObjectMeta{ + Name: string(scCR.GetUID()), + Namespace: scCR.GetNamespace(), + }, + TypeMeta: metav1.TypeMeta{}, + Spec: k8sv1alpha1.ServerClusterMetaSpec{ + Imported: imported, + Reclaim: reclaim, + Name: string(scCR.GetUID()), + ServerClusterName: scCR.Name, + AdminID: adminID, + Admin: scCR.Spec.Admin, + SiteID: siteID, + Site: scCR.Spec.Site, + VPCID: vpcID, + VPC: scCR.Spec.VPC, + TemplateID: templateID, + Template: scCR.Spec.Template, + Tags: normalizeTags(scCR.Spec.Tags), + ServerClusterCRGeneration: scCR.GetGeneration(), + }, + } + + return scMeta, nil +} + +// ServerClusterMetaToNetris converts the ServerClusterMeta resource to Netris ServerClusterW type for adding. +func ServerClusterMetaToNetris(scMeta *k8sv1alpha1.ServerClusterMeta) (*servercluster.ServerClusterW, error) { + scAdd := &servercluster.ServerClusterW{ + Name: scMeta.Spec.ServerClusterName, + Admin: servercluster.IDName{ID: scMeta.Spec.AdminID, Name: scMeta.Spec.Admin}, + Site: servercluster.IDName{ID: scMeta.Spec.SiteID, Name: scMeta.Spec.Site}, + VPC: servercluster.IDName{ID: scMeta.Spec.VPCID, Name: scMeta.Spec.VPC}, + SrvClusterTemplate: servercluster.IDName{ID: scMeta.Spec.TemplateID, Name: scMeta.Spec.Template}, + Tags: normalizeTags(scMeta.Spec.Tags), + Servers: []servercluster.Servers{}, + } + return scAdd, nil +} + +// ServerClusterMetaToNetrisUpdate converts the ServerClusterMeta resource to Netris ServerClusterU type for updating. +func ServerClusterMetaToNetrisUpdate(scMeta *k8sv1alpha1.ServerClusterMeta) (*servercluster.ServerClusterU, error) { + scUpdate := &servercluster.ServerClusterU{ + Tags: normalizeTags(scMeta.Spec.Tags), + Servers: []servercluster.Servers{}, + } + return scUpdate, nil +} + +func compareServerClusterMetaAPIServerCluster(scMeta *k8sv1alpha1.ServerClusterMeta, apiSC *servercluster.ServerCluster) bool { + // Note: Update API only supports Tags and Servers, so we only compare those + // Admin, Site, VPC, Template are set on creation and cannot be changed via Update API + + // Compare Tags + apiTags := normalizeTags(apiSC.Tags) + metaTags := normalizeTags(scMeta.Spec.Tags) + + if len(apiTags) != len(metaTags) { + return false + } + + // Check if all metaTags are in apiTags + for _, tag := range metaTags { + found := false + for _, apiTag := range apiTags { + if tag == apiTag { + found = true + break + } + } + if !found { + return false + } + } + + // Check if all apiTags are in metaTags + for _, apiTag := range apiTags { + found := false + for _, tag := range metaTags { + if apiTag == tag { + found = true + break + } + } + if !found { + return false + } + } + + return true +} + +func serverClusterCompareFieldsForNewMeta(scCR *k8sv1alpha1.ServerCluster, scMeta *k8sv1alpha1.ServerClusterMeta) bool { + imported := false + reclaim := false + if i, ok := scCR.GetAnnotations()["resource.k8s.netris.ai/import"]; ok && i == "true" { + imported = true + } + if i, ok := scCR.GetAnnotations()["resource.k8s.netris.ai/reclaimPolicy"]; ok && i == "retain" { + reclaim = true + } + return scCR.GetGeneration() != scMeta.Spec.ServerClusterCRGeneration || imported != scMeta.Spec.Imported || reclaim != scMeta.Spec.Reclaim +} + +func serverClusterMustUpdateAnnotations(scCR *k8sv1alpha1.ServerCluster) bool { + update := false + if i, ok := scCR.GetAnnotations()["resource.k8s.netris.ai/import"]; !(ok && (i == "true" || i == "false")) { + update = true + } + if i, ok := scCR.GetAnnotations()["resource.k8s.netris.ai/reclaimPolicy"]; !(ok && (i == "retain" || i == "delete")) { + update = true + } + return update +} + +func serverClusterUpdateDefaultAnnotations(scCR *k8sv1alpha1.ServerCluster) { + imported := "false" + reclaim := "delete" + if i, ok := scCR.GetAnnotations()["resource.k8s.netris.ai/import"]; ok && i == "true" { + imported = "true" + } + if i, ok := scCR.GetAnnotations()["resource.k8s.netris.ai/reclaimPolicy"]; ok && i == "retain" { + reclaim = "retain" + } + annotations := scCR.GetAnnotations() + annotations["resource.k8s.netris.ai/import"] = imported + annotations["resource.k8s.netris.ai/reclaimPolicy"] = reclaim + scCR.SetAnnotations(annotations) +} + diff --git a/controllers/serverclustermeta_controller.go b/controllers/serverclustermeta_controller.go new file mode 100644 index 0000000..646709c --- /dev/null +++ b/controllers/serverclustermeta_controller.go @@ -0,0 +1,275 @@ +/* +Copyright 2021. Netris, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + "time" + + "go.uber.org/zap/zapcore" + "k8s.io/apimachinery/pkg/api/errors" + + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + k8sv1alpha1 "github.com/netrisai/netris-operator/api/v1alpha1" + "github.com/netrisai/netris-operator/netrisstorage" + "github.com/netrisai/netriswebapi/http" + api "github.com/netrisai/netriswebapi/v2" + "github.com/netrisai/netriswebapi/v2/types/servercluster" +) + +// ServerClusterMetaReconciler reconciles a ServerClusterMeta object +type ServerClusterMetaReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + Cred *api.Clientset + NStorage *netrisstorage.Storage +} + +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=serverclustermeta,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=serverclustermeta/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=k8s.netris.ai,resources=serverclustermeta/finalizers,verbs=update + +// Reconcile . +func (r *ServerClusterMetaReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + debugLogger := r.Log.WithValues("name", req.NamespacedName).V(int(zapcore.WarnLevel)) + + scMeta := &k8sv1alpha1.ServerClusterMeta{} + scCR := &k8sv1alpha1.ServerCluster{} + scMetaCtx, scMetaCancel := context.WithTimeout(cntxt, contextTimeout) + defer scMetaCancel() + if err := r.Get(scMetaCtx, req.NamespacedName, scMeta); err != nil { + if errors.IsNotFound(err) { + debugLogger.Info(err.Error()) + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + logger := r.Log.WithValues("name", fmt.Sprintf("%s/%s", req.NamespacedName.Namespace, scMeta.Spec.ServerClusterName)) + debugLogger = logger.V(int(zapcore.WarnLevel)) + + u := uniReconciler{ + Client: r.Client, + Logger: logger, + DebugLogger: debugLogger, + Cred: r.Cred, + NStorage: r.NStorage, + } + + provisionState := "Provisioning" + + scNN := req.NamespacedName + scNN.Name = scMeta.Spec.ServerClusterName + scNNCtx, scNNCancel := context.WithTimeout(cntxt, contextTimeout) + defer scNNCancel() + if err := r.Get(scNNCtx, scNN, scCR); err != nil { + if errors.IsNotFound(err) { + debugLogger.Info("ServerCluster CR not found, deleting ServerClusterMeta") + // ServerCluster CR was deleted, clean up ServerClusterMeta + if scMeta.Spec.ID > 0 && !scMeta.Spec.Reclaim { + debugLogger.Info("Deleting ServerCluster from Netris", "id", scMeta.Spec.ID) + reply, err := r.Cred.ServerCluster().Delete(scMeta.Spec.ID) + if err != nil { + logger.Error(fmt.Errorf("{deleteServerCluster} %s", err), "") + } else { + resp, err := http.ParseAPIResponse(reply.Data) + if err == nil && !resp.IsSuccess { + if resp.Message != "Invalid ServerCluster ID" { + debugLogger.Info("Failed to delete ServerCluster from Netris", "error", resp.Message) + } + } + } + } + // Delete the ServerClusterMeta object + scMetaDeleteCtx, scMetaDeleteCancel := context.WithTimeout(cntxt, contextTimeout) + defer scMetaDeleteCancel() + if err := r.Delete(scMetaDeleteCtx, scMeta.DeepCopyObject(), &client.DeleteOptions{}); err != nil { + if !errors.IsNotFound(err) { + logger.Error(fmt.Errorf("{deleteServerClusterMeta} %s", err), "") + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + } + debugLogger.Info("ServerClusterMeta deleted") + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + if scMeta.DeletionTimestamp != nil { + return ctrl.Result{}, nil + } + + if scMeta.Spec.ID == 0 { + debugLogger.Info("ID Not found in meta") + // First, try to find existing ServerCluster by name (for both import and non-import cases) + debugLogger.Info("Checking if ServerCluster exists in Netris by name") + clusters, err := r.Cred.ServerCluster().Get() + if err != nil { + logger.Error(fmt.Errorf("{Get ServerClusters} %s", err), "") + return u.patchServerClusterStatus(scCR, "Failure", err.Error()) + } + for _, cluster := range clusters { + if cluster.Name == scMeta.Spec.ServerClusterName { + debugLogger.Info("ServerCluster found in Netris by name, importing") + scMeta.Spec.ID = cluster.ID + scMeta.Spec.AdminID = cluster.Admin.ID + scMeta.Spec.Admin = cluster.Admin.Name + scMeta.Spec.SiteID = cluster.Site.ID + scMeta.Spec.Site = cluster.Site.Name + scMeta.Spec.VPCID = cluster.VPC.ID + scMeta.Spec.VPC = cluster.VPC.Name + scMeta.Spec.TemplateID = cluster.SrvClusterTemplate.ID + scMeta.Spec.Template = cluster.SrvClusterTemplate.Name + scMeta.Spec.Tags = cluster.Tags + scCR.Status.ModifiedDate = metav1.NewTime(time.Unix(int64(cluster.ModifiedDate/1000), 0)) + scMetaPatchCtx, scMetaPatchCancel := context.WithTimeout(cntxt, contextTimeout) + defer scMetaPatchCancel() + err := r.Patch(scMetaPatchCtx, scMeta.DeepCopyObject(), client.Merge, &client.PatchOptions{}) + if err != nil { + logger.Error(fmt.Errorf("{patch scmeta.Spec.ID} %s", err), "") + return u.patchServerClusterStatus(scCR, "Failure", err.Error()) + } + debugLogger.Info("ServerCluster ID patched from existing Netris resource") + if scMeta.Spec.Imported { + logger.Info("ServerCluster imported") + } else { + logger.Info("ServerCluster found in Netris and linked") + } + return ctrl.Result{RequeueAfter: requeueInterval}, nil + } + } + debugLogger.Info("ServerCluster not found in Netris, will create new one") + + logger.Info("Creating ServerCluster") + if _, err, errMsg := r.createServerCluster(scMeta); err != nil { + logger.Error(fmt.Errorf("{createServerCluster} %s", err), "") + return u.patchServerClusterStatus(scCR, "Failure", errMsg.Error()) + } + logger.Info("ServerCluster Created") + } else { + apiSC, err := r.Cred.ServerCluster().GetByID(scMeta.Spec.ID) + if err != nil || apiSC == nil { + debugLogger.Info("ServerCluster not found in Netris") + debugLogger.Info("Going to create ServerCluster") + logger.Info("Creating ServerCluster") + if _, err, errMsg := r.createServerCluster(scMeta); err != nil { + logger.Error(fmt.Errorf("{createServerCluster} %s", err), "") + return u.patchServerClusterStatus(scCR, "Failure", errMsg.Error()) + } + logger.Info("ServerCluster Created") + } else { + provisionState = "Active" + scCR.Status.ModifiedDate = metav1.NewTime(time.Unix(int64(apiSC.ModifiedDate/1000), 0)) + debugLogger.Info("Comparing ServerClusterMeta with Netris ServerCluster") + if ok := compareServerClusterMetaAPIServerCluster(scMeta, apiSC); ok { + debugLogger.Info("Nothing Changed") + } else { + debugLogger.Info("Something changed") + debugLogger.Info("Go to update ServerCluster in Netris") + logger.Info("Updating ServerCluster") + updateSC, err := ServerClusterMetaToNetrisUpdate(scMeta) + if err != nil { + logger.Error(fmt.Errorf("{ServerClusterMetaToNetrisUpdate} %s", err), "") + return u.patchServerClusterStatus(scCR, "Failure", err.Error()) + } + _, err, errMsg := r.updateServerCluster(scMeta.Spec.ID, updateSC) + if err != nil { + logger.Error(fmt.Errorf("{updateServerCluster} %s", err), "") + return u.patchServerClusterStatus(scCR, "Failure", errMsg.Error()) + } + logger.Info("ServerCluster Updated") + } + } + } + return u.patchServerClusterStatus(scCR, provisionState, "Success") +} + +// SetupWithManager . +func (r *ServerClusterMetaReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&k8sv1alpha1.ServerClusterMeta{}). + Complete(r) +} + +func (r *ServerClusterMetaReconciler) createServerCluster(scMeta *k8sv1alpha1.ServerClusterMeta) (ctrl.Result, error, error) { + debugLogger := r.Log.WithValues( + "name", fmt.Sprintf("%s/%s", scMeta.Namespace, scMeta.Spec.ServerClusterName), + "scName", scMeta.Spec.ServerClusterName, + ).V(int(zapcore.WarnLevel)) + + scAdd, err := ServerClusterMetaToNetris(scMeta) + if err != nil { + return ctrl.Result{}, err, err + } + reply, err := r.Cred.ServerCluster().Add(scAdd) + if err != nil { + return ctrl.Result{}, err, err + } + resp, err := http.ParseAPIResponse(reply.Data) + if err != nil { + return ctrl.Result{}, err, err + } + if !resp.IsSuccess { + return ctrl.Result{}, fmt.Errorf(resp.Message), fmt.Errorf(resp.Message) + } + + idStruct := struct { + ID int `json:"id"` + }{} + err = http.Decode(resp.Data, &idStruct) + if err != nil { + return ctrl.Result{}, err, err + } + + debugLogger.Info("ServerCluster Created", "id", idStruct.ID) + + scMeta.Spec.ID = idStruct.ID + + ctx, cancel := context.WithTimeout(cntxt, contextTimeout) + defer cancel() + err = r.Patch(ctx, scMeta.DeepCopyObject(), client.Merge, &client.PatchOptions{}) // requeue + if err != nil { + return ctrl.Result{}, err, err + } + + debugLogger.Info("ID patched to meta", "id", idStruct.ID) + return ctrl.Result{}, nil, nil +} + +func (r *ServerClusterMetaReconciler) updateServerCluster(id int, sc *servercluster.ServerClusterU) (ctrl.Result, error, error) { + reply, err := r.Cred.ServerCluster().Update(id, sc) + if err != nil { + return ctrl.Result{}, fmt.Errorf("{updateServerCluster} %s", err), err + } + resp, err := http.ParseAPIResponse(reply.Data) + if err != nil { + return ctrl.Result{}, err, err + } + if !resp.IsSuccess { + return ctrl.Result{}, fmt.Errorf("{updateServerCluster} %s", fmt.Errorf(resp.Message)), fmt.Errorf(resp.Message) + } + + return ctrl.Result{}, nil, nil +} + diff --git a/controllers/serverclustertemplate_translations.go b/controllers/serverclustertemplate_translations.go index a5df1a9..53f0ab1 100644 --- a/controllers/serverclustertemplate_translations.go +++ b/controllers/serverclustertemplate_translations.go @@ -17,12 +17,11 @@ limitations under the License. package controllers import ( - "encoding/json" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/go-logr/logr" k8sv1alpha1 "github.com/netrisai/netris-operator/api/v1alpha1" "github.com/netrisai/netriswebapi/v2/types/serverclustertemplate" - "github.com/r3labs/diff/v2" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // ServerClusterTemplateToServerClusterTemplateMeta converts the ServerClusterTemplate resource to ServerClusterTemplateMeta type. @@ -73,32 +72,18 @@ func ServerClusterTemplateMetaToNetrisUpdate(sctMeta *k8sv1alpha1.ServerClusterT return sctUpdate, nil } -func compareServerClusterTemplateMetaAPIServerClusterTemplate(sctMeta *k8sv1alpha1.ServerClusterTemplateMeta, apiSCT *serverclustertemplate.ServerClusterTemplate) bool { +func compareServerClusterTemplateMetaAPIServerClusterTemplate(sctMeta *k8sv1alpha1.ServerClusterTemplateMeta, apiSCT *serverclustertemplate.ServerClusterTemplate, debugLogger logr.InfoLogger) bool { if sctMeta.Spec.ServerClusterTemplateName != apiSCT.Name { + debugLogger.Info("Name changed", "metaName", sctMeta.Spec.ServerClusterTemplateName, "apiName", apiSCT.Name) return false } - // Compare Vnets by marshaling to JSON and comparing - metaVnetsJSON, err := json.Marshal(sctMeta.Spec.Vnets) - if err != nil { - return false - } - apiVnetsJSON, err := json.Marshal(apiSCT.Vnets) - if err != nil { - return false - } - - // Compare as JSON strings - var metaVnets, apiVnets interface{} - if err := json.Unmarshal(metaVnetsJSON, &metaVnets); err != nil { - return false - } - if err := json.Unmarshal(apiVnetsJSON, &apiVnets); err != nil { - return false - } - - changelog, _ := diff.Diff(metaVnets, apiVnets) - return len(changelog) <= 0 + // Vnets field is ignored in comparison - API returns Vnets with IDs that are assigned by the API + // and not present in the CR, causing false positives. Since Vnets cannot be updated via the API + // when the template is in use, we skip comparing them. + debugLogger.Info("Skipping Vnets comparison (field ignored)") + + return true } func serverClusterTemplateCompareFieldsForNewMeta(sctCR *k8sv1alpha1.ServerClusterTemplate, sctMeta *k8sv1alpha1.ServerClusterTemplateMeta) bool { diff --git a/controllers/serverclustertemplatemeta_controller.go b/controllers/serverclustertemplatemeta_controller.go index 76799a6..501f85b 100644 --- a/controllers/serverclustertemplatemeta_controller.go +++ b/controllers/serverclustertemplatemeta_controller.go @@ -149,11 +149,31 @@ func (r *ServerClusterTemplateMetaReconciler) Reconcile(req ctrl.Request) (ctrl. } else { provisionState = "Active" sctCR.Status.ModifiedDate = metav1.NewTime(time.Unix(int64(apiSCT.ModifiedDate/1000), 0)) - debugLogger.Info("Comparing ServerClusterTemplateMeta with Netris ServerClusterTemplate") - if ok := compareServerClusterTemplateMetaAPIServerClusterTemplate(sctMeta, apiSCT); ok { + debugLogger.Info("Comparing ServerClusterTemplateMeta with Netris ServerClusterTemplate", + "metaName", sctMeta.Spec.ServerClusterTemplateName, + "apiName", apiSCT.Name) + if ok := compareServerClusterTemplateMetaAPIServerClusterTemplate(sctMeta, apiSCT, debugLogger); ok { debugLogger.Info("Nothing Changed") } else { - debugLogger.Info("Something changed") + // Check if template is in use by any ServerCluster + serverClusterList := &k8sv1alpha1.ServerClusterList{} + serverClusterListCtx, serverClusterListCancel := context.WithTimeout(cntxt, contextTimeout) + defer serverClusterListCancel() + if err := r.List(serverClusterListCtx, serverClusterList, &client.ListOptions{}); err != nil { + debugLogger.Info("Failed to list ServerClusters", "error", err) + // Continue with update attempt if we can't check + } else { + for _, sc := range serverClusterList.Items { + if sc.Spec.Template == sctMeta.Spec.ServerClusterTemplateName { + logger.Info("ServerClusterTemplate is in use by ServerCluster, skipping update", + "serverCluster", fmt.Sprintf("%s/%s", sc.Namespace, sc.Name)) + return u.patchServerClusterTemplateStatus(sctCR, "Active", + fmt.Sprintf("Template is in use by ServerCluster %s/%s and cannot be updated", sc.Namespace, sc.Name)) + } + } + } + + debugLogger.Info("Something changed - see previous debug logs for details") debugLogger.Info("Go to update ServerClusterTemplate in Netris") logger.Info("Updating ServerClusterTemplate") updateSCT, err := ServerClusterTemplateMetaToNetrisUpdate(sctMeta) diff --git a/deploy/charts/netris-operator/crds/k8s.netris.ai_serverclustermeta.yaml b/deploy/charts/netris-operator/crds/k8s.netris.ai_serverclustermeta.yaml new file mode 100644 index 0000000..cf3a069 --- /dev/null +++ b/deploy/charts/netris-operator/crds/k8s.netris.ai_serverclustermeta.yaml @@ -0,0 +1,84 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: serverclustermeta.k8s.netris.ai +spec: + group: k8s.netris.ai + names: + kind: ServerClusterMeta + listKind: ServerClusterMetaList + plural: serverclustermeta + singular: serverclustermeta + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ServerClusterMeta is the Schema for the serverclustermeta API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ServerClusterMetaSpec defines the desired state of ServerClusterMeta + properties: + admin: + type: string + adminId: + type: integer + imported: + type: boolean + name: + type: string + reclaimPolicy: + type: boolean + serverClusterGeneration: + type: integer + serverClusterName: + type: string + site: + type: string + siteId: + type: integer + tags: + items: + type: string + type: array + template: + type: string + templateId: + type: integer + vpc: + type: string + vpcId: + type: integer + type: object + status: + description: ServerClusterMetaStatus defines the observed state of ServerClusterMeta + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/deploy/charts/netris-operator/crds/k8s.netris.ai_serverclusters.yaml b/deploy/charts/netris-operator/crds/k8s.netris.ai_serverclusters.yaml new file mode 100644 index 0000000..ff81de9 --- /dev/null +++ b/deploy/charts/netris-operator/crds/k8s.netris.ai_serverclusters.yaml @@ -0,0 +1,107 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: serverclusters.k8s.netris.ai +spec: + group: k8s.netris.ai + names: + kind: ServerCluster + listKind: ServerClusterList + plural: serverclusters + singular: servercluster + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.admin + name: Admin + type: string + - jsonPath: .spec.site + name: Site + type: string + - jsonPath: .spec.vpc + name: VPC + type: string + - jsonPath: .spec.template + name: Template + type: string + - jsonPath: .status.status + name: Status + type: string + - jsonPath: .status.modified + name: Modified + priority: 1 + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ServerCluster is the Schema for the serverclusters API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ServerClusterSpec defines the desired state of ServerCluster + properties: + admin: + type: string + site: + type: string + tags: + items: + type: string + type: array + template: + type: string + vpc: + type: string + required: + - admin + - site + - template + - vpc + type: object + status: + description: ServerClusterStatus defines the observed state of ServerCluster + properties: + message: + type: string + modified: + format: date-time + type: string + status: + description: 'INSERT ADDITIONAL STATUS FIELD - define observed state + of cluster Important: Run "make" to regenerate code after modifying + this file' + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + diff --git a/deploy/charts/netris-operator/templates/rbac.yaml b/deploy/charts/netris-operator/templates/rbac.yaml index b0a8db6..a3ca7aa 100644 --- a/deploy/charts/netris-operator/templates/rbac.yaml +++ b/deploy/charts/netris-operator/templates/rbac.yaml @@ -648,6 +648,58 @@ rules: - get - patch - update + - apiGroups: + - k8s.netris.ai + resources: + - serverclusters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - k8s.netris.ai + resources: + - serverclusters/finalizers + verbs: + - update + - apiGroups: + - k8s.netris.ai + resources: + - serverclusters/status + verbs: + - get + - patch + - update + - apiGroups: + - k8s.netris.ai + resources: + - serverclustermeta + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - k8s.netris.ai + resources: + - serverclustermeta/finalizers + verbs: + - update + - apiGroups: + - k8s.netris.ai + resources: + - serverclustermeta/status + verbs: + - get + - patch + - update - apiGroups: - k8s.netris.ai resources: diff --git a/main.go b/main.go index 5a6f6e2..bcc08e0 100644 --- a/main.go +++ b/main.go @@ -315,6 +315,27 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "ServerClusterTemplateMeta") os.Exit(1) } + // ServerCluster controllers + if err = (&controllers.ServerClusterReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("ServerCluster"), + Scheme: mgr.GetScheme(), + Cred: cred, + NStorage: nStorage, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ServerCluster") + os.Exit(1) + } + if err = (&controllers.ServerClusterMetaReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("ServerClusterMeta"), + Scheme: mgr.GetScheme(), + Cred: cred, + NStorage: nStorage, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ServerClusterMeta") + os.Exit(1) + } if err = (&controllers.SwitchReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("Switch"), diff --git a/samples/kustomization.yaml b/samples/kustomization.yaml index e17f361..e815340 100644 --- a/samples/kustomization.yaml +++ b/samples/kustomization.yaml @@ -7,6 +7,7 @@ resources: - softgate.yaml - server.yaml - serverclustertemplate.yaml + - servercluster.yaml - switch.yaml - controller.yaml - vnet.yaml diff --git a/samples/servercluster.yaml b/samples/servercluster.yaml new file mode 100644 index 0000000..40aa66a --- /dev/null +++ b/samples/servercluster.yaml @@ -0,0 +1,13 @@ +apiVersion: k8s.netris.ai/v1alpha1 +kind: ServerCluster +metadata: + name: my-server-cluster +spec: + admin: Admin + site: santa-clara + vpc: my-vpc + template: my-server-cluster-template + tags: + - production + - compute +