From 1c39bafb9d0b5481a39864961bbf04e4fd16c68e Mon Sep 17 00:00:00 2001 From: vireshnavalli Date: Thu, 25 May 2023 09:11:59 +0000 Subject: [PATCH 1/9] AMF_Workload_Controller --- .../workload.nephio.org_amfdeployments.yaml | 400 ++++++++++++ .../controllers/amfdeployment_controller.go | 578 ++++++++++++++++++ .../amfdeployment_controller_test.go | 530 ++++++++++++++++ .../controllers/free5gc_amfcfg.go | 136 +++++ .../controllers/free5gc_amfsvc.go | 24 + free5gc-operator/test/amfdeploy.yaml | 22 + free5gc-operator/test/nad/amf-n2-nad.yaml | 38 ++ 7 files changed, 1728 insertions(+) create mode 100644 free5gc-operator/config/crd/bases/workload.nephio.org_amfdeployments.yaml create mode 100644 free5gc-operator/controllers/amfdeployment_controller.go create mode 100644 free5gc-operator/controllers/amfdeployment_controller_test.go create mode 100644 free5gc-operator/controllers/free5gc_amfcfg.go create mode 100644 free5gc-operator/controllers/free5gc_amfsvc.go create mode 100644 free5gc-operator/test/amfdeploy.yaml create mode 100644 free5gc-operator/test/nad/amf-n2-nad.yaml diff --git a/free5gc-operator/config/crd/bases/workload.nephio.org_amfdeployments.yaml b/free5gc-operator/config/crd/bases/workload.nephio.org_amfdeployments.yaml new file mode 100644 index 0000000..fc5d62e --- /dev/null +++ b/free5gc-operator/config/crd/bases/workload.nephio.org_amfdeployments.yaml @@ -0,0 +1,400 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: amfdeployments.workload.nephio.org +spec: + group: workload.nephio.org + names: + kind: AMFDeployment + listKind: AMFDeploymentList + plural: amfdeployments + singular: amfdeployment + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + 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: + properties: + capacity: + description: capacity defines the capacity characteristics of the + NF deployment + properties: + maxDownlinkThroughput: + anyOf: + - type: integer + - type: string + description: MaxDownlinkThroughput defines the max downlink dataplane + throughput + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + maxNFConnections: + description: MaxNFConnections defines the max NF(s) that can be + connected to this NF/device + type: integer + maxSessions: + description: MaxSessions defines the max sessions of the control + plane expressed in unit of 1000s + type: integer + maxSubscribers: + description: MaxSubscribers defines the max subscribers expressed + in unit of 1000s + type: integer + maxUplinkThroughput: + anyOf: + - type: integer + - type: string + description: MaxUplinkThroughput defines the max uplink dataplane + throughput + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + configRefs: + description: configRef defines addiitonal configuration references + the nf depends upon + items: + description: "ObjectReference contains enough information to let + you inspect or modify the referred object. --- New uses of this + type are discouraged because of difficulty describing its usage + when embedded in APIs. 1. Ignored fields. It includes many fields + which are not generally honored. For instance, ResourceVersion + and FieldPath are both very rarely valid in actual usage. 2. Invalid + usage help. It is impossible to add specific help for individual + usage. In most embedded usages, there are particular restrictions + like, \"must refer only to types A and B\" or \"UID not honored\" + or \"name must be restricted\". Those cannot be well described + when embedded. 3. Inconsistent validation. Because the usages + are different, the validation rules are different by usage, which + makes it hard for users to predict what will happen. 4. The fields + are both imprecise and overly precise. Kind is not a precise + mapping to a URL. This can produce ambiguity during interpretation + and require a REST mapping. In most cases, the dependency is + on the group,resource tuple and the version of the actual struct + is irrelevant. 5. We cannot easily change it. Because this type + is embedded in many locations, updates to this type will affect + numerous schemas. Don't make new APIs embed an underspecified + API type they do not control. \n Instead of using this type, create + a locally provided and used type that is well-focused on your + reference. For example, ServiceReferences for admission registration: + https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 + ." + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + type: array + interfaces: + description: Interfaces defines the interfaces associated with the + NF deployment + items: + description: InterfaceConfig defines the configuration of the interface + properties: + ipv4: + description: IPv4 defines the ipv4 configuration of the interface + properties: + address: + description: Address defines the IPv4 address and prefix + length in CIDR notation [IP prefix, range IPv4 with host + bits] + type: string + gateway: + description: Gateway defines the IPv4 address associated + to the interface as a gateway + type: string + required: + - address + type: object + ipv6: + description: IPv6Config defines the ipv6 configuration of the + interface + properties: + address: + description: Address defines the IPv6 address and prefix + length in CIDR notation [IP prefix, range IPv6 with host + bits] + type: string + gateway: + description: Gateway defines the IPv6 address associated + to the interface as a gateway + type: string + required: + - address + type: object + name: + description: Name defines the name of the interface + maxLength: 253 + minLength: 1 + type: string + vlanID: + description: VLANID defines the specific vlan id associated + on this interface + type: integer + required: + - name + type: object + type: array + networkInstances: + description: NetworkInstances defines the network instances associated + with the NF deployment + items: + description: A networkInstance is a Layer 3 forwarding construct + such as a virtual routing and forwarding (VRF) instance, + properties: + bgp: + description: BGP defines the BGP configuration associated with + the network instance + properties: + autonomousSystem: + description: AutonomousSystem defines the AS number of the + bgp process + type: integer + bgpNeighbors: + description: BGPNeigbors defines the configuration of the + BGP neighbor + items: + properties: + address: + description: Address defines the IPv4 or IPv6 address + of the BGP neighbor + type: string + name: + description: BGP interface name, MUST match the one + use in InterfaceConfig + type: string + peerAS: + description: PeerAS defines the AS number of the bgp + peer + type: integer + required: + - address + - peerAS + type: object + type: array + routerID: + description: RouterID defines the router ID of the bgp process + type: string + required: + - autonomousSystem + - bgpNeighbors + - routerID + type: object + dataNetworks: + description: DataNetworks defines the data networks assocated + with the network instance + items: + description: A DataNetwork defines the Data Network name defined + by 3GPP + properties: + name: + description: Name defines the name of the data network + maxLength: 253 + minLength: 1 + type: string + pool: + description: Pool defines the list of address pools associated + with the data network + items: + properties: + prefix: + description: Prefix defines the ip cidr in prefix + notation. It is defines as a subnet + pattern: (([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/(([0-9])|([1-2][0-9])|(3[0-2]))|((:|[0-9a-fA-F]{0,4}):)([0-9a-fA-F]{0,4}:){0,5}((([0-9a-fA-F]{0,4}:)?(:|[0-9a-fA-F]{0,4}))|(((25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])))(/(([0-9])|([0-9]{2})|(1[0-1][0-9])|(12[0-8]))) + type: string + required: + - prefix + type: object + type: array + type: object + type: array + interfaces: + description: interfaces defines the interfaces associated with + the network instance + items: + type: string + type: array + name: + description: Name defines the name of the network instance + maxLength: 253 + minLength: 1 + type: string + peers: + description: Peers defines the peer configuration associated + with the network instance + items: + description: A PeerConfig defines the peer configuration + properties: + ipv4: + description: IPv4 defines the ipv4 configuration of the + peer + properties: + address: + description: Address defines the IPv4 address and + prefix length in CIDR notation [IP prefix, range + IPv4 with host bits] + type: string + gateway: + description: Gateway defines the IPv4 address associated + to the interface as a gateway + type: string + required: + - address + type: object + ipv6: + description: IPv6 defines the ipv6 configuration of the + peer + properties: + address: + description: Address defines the IPv6 address and + prefix length in CIDR notation [IP prefix, range + IPv6 with host bits] + type: string + gateway: + description: Gateway defines the IPv6 address associated + to the interface as a gateway + type: string + required: + - address + type: object + name: + description: Name defines the name of the data network + maxLength: 253 + minLength: 1 + type: string + type: object + type: array + required: + - name + type: object + type: array + type: object + status: + properties: + conditions: + description: Conditions define the current state of the NF deployment + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: The generation observed by the deployment controller. + format: int32 + type: integer + required: + - observedGeneration + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/free5gc-operator/controllers/amfdeployment_controller.go b/free5gc-operator/controllers/amfdeployment_controller.go new file mode 100644 index 0000000..976a980 --- /dev/null +++ b/free5gc-operator/controllers/amfdeployment_controller.go @@ -0,0 +1,578 @@ +/* +Copyright 2023. + +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 ( + "bytes" + "context" + "encoding/json" + //"errors" + "fmt" + "html/template" + "sort" + "time" + + appsv1 "k8s.io/api/apps/v1" + apiv1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + resource "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/go-logr/logr" + workloadv1alpha1 "github.com/nephio-project/api/nf_deployments/v1alpha1" +) + +// AMFDeploymentReconciler reconciles a AMFDeployment object +type AMFDeploymentReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=workload.nephio.org,resources=amfdeployments,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=workload.nephio.org,resources=amfdeployments/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=workload.nephio.org,resources=amfdeployments/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 AMFDeployment 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.14.1/pkg/reconcile +//func (r *AMFDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +// _ = log.FromContext(ctx) +// +// TODO(user): your logic here + +// return ctrl.Result{}, nil +//} + +// SetupWithManager sets up the controller with the Manager. +//func (r *AMFDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error { +// return ctrl.NewControllerManagedBy(mgr). +// For(&workloadv1alpha1.AMFDeployment{}). +// Complete(r) +//} + +type AMFcfgStruct struct { + N2_IP string + // N11_IP string + // N6cfg []workloadv1alpha1.NetworkInstance + // N6gw string +} + +type AMFAnnotation struct { + Name string `json:"name"` + Interface string `json:"interface"` + IP string `json:"ip"` + Gateway string `json:"gateway"` +} + +func getAMFResourceParams(amfSpec workloadv1alpha1.AMFDeploymentSpec) (int32, *apiv1.ResourceRequirements, error) { + // Placeholder for Capacity calculation. Reqiurce requirements houlw be calculated based on DL, UL. + + // TODO: increase number of recpicas based on NFDeployment.Capacity.MaxSessions + var replicas int32 = 1 + + // downlink := resource.MustParse("5G") + // uplink := resource.MustParse("1G") + var cpuLimit string + var cpuRequest string + var memoryLimit string + var memoryRequest string + + if amfSpec.Capacity.MaxSubscribers > 1000 { + cpuLimit = "300m" + memoryLimit = "256Mi" + cpuRequest = "300m" + memoryRequest = "256Mi" + } else { + cpuLimit = "150m" + memoryLimit = "128Mi" + cpuRequest = "150m" + memoryRequest = "128Mi" + } + resources := apiv1.ResourceRequirements{} + resources.Limits = make(apiv1.ResourceList) + resources.Limits[apiv1.ResourceCPU] = resource.MustParse(cpuLimit) + resources.Limits[apiv1.ResourceMemory] = resource.MustParse(memoryLimit) + resources.Requests = make(apiv1.ResourceList) + resources.Requests[apiv1.ResourceCPU] = resource.MustParse(cpuRequest) + resources.Requests[apiv1.ResourceMemory] = resource.MustParse(memoryRequest) + return replicas, &resources, nil +} + +func constructAMFNadName(templateName string, suffix string) string { + return templateName + "-" + suffix +} + +// getNads retursn NAD label string composed based on the Nx interfaces configuration provided in AMFDeploymentSpec +func getAMFNad(templateName string, spec *workloadv1alpha1.AMFDeploymentSpec) string { + var ret string + + n2CfgSlice := getIntConfigSlice(spec.Interfaces, "N2") + // n11CfgSlice := getIntConfigSlice(spec.Interfaces, "N11") + // n4CfgSlice := getIntConfigSlice(spec.Interfaces, "N4") + // n9CfgSlice := getIntConfigSlice(spec.Interfaces, "N9") + + ret = `[` + intfMap := map[string][]workloadv1alpha1.InterfaceConfig{ + "n2": n2CfgSlice, + // "n11": n11CfgSlice, + // "n6": n6CfgSlice, + // "n9": n9CfgSlice, + } + // Need to sort inftMap by key otherwise unitTests might fail as order of intefaces in intfMap is not guaranteed + inftMapKeys := make([]string, 0, len(intfMap)) + for interfaceName := range intfMap { + inftMapKeys = append(inftMapKeys, interfaceName) + } + sort.Strings(inftMapKeys) + + noComma := true + for _, key := range inftMapKeys { + for _, intf := range intfMap[key] { + newNad := fmt.Sprintf(` + {"name": "%s", + "interface": "%s", + "ips": ["%s"], + "gateways": ["%s"] + }`, constructNadName(templateName, key), intf.Name, intf.IPv4.Address, *intf.IPv4.Gateway) + if noComma { + ret = ret + newNad + noComma = false + } else { + ret = ret + "," + newNad + } + } + } + ret = ret + ` + ]` + return ret +} + +// checkNADExists gets deployment object and checks "k8s.v1.cni.cncf.io/networks" NADs. +// returns True if all requred NADs are present +// returns False if any NAD doesn't exists in deployment namespace +func (r *AMFDeploymentReconciler) checkAMFNADexist(log logr.Logger, ctx context.Context, deployment *appsv1.Deployment) bool { + amfAnnotations := []AMFAnnotation{} + annotationsString, ok := deployment.Spec.Template.GetAnnotations()["k8s.v1.cni.cncf.io/networks"] + if !ok { + log.Info("Annotations k8s.v1.cni.cncf.io/networks not found", "AMFDeployment.namespace", deployment.Namespace) + return false + } + if err := json.Unmarshal([]byte(annotationsString), &amfAnnotations); err != nil { + log.Info("Failed to parse AMFDeployment annotations", "AMFDeployment.namespace", deployment.Namespace) + return false + } + for _, amfAnnotation := range amfAnnotations { + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "k8s.cni.cncf.io", + Kind: "NetworkAttachmentDefinition", + Version: "v1", + }) + key := client.ObjectKey{Namespace: deployment.ObjectMeta.Namespace, Name: amfAnnotation.Name} + if err := r.Get(ctx, key, u); err != nil { + log.Info(fmt.Sprintf("Failed to get NAD %s", amfAnnotation.Name), "AMFDeployment.namespace", deployment.Namespace) + return false + } + } + + return true +} + +func free5gcAMFDeployment(log logr.Logger, amfDeploy *workloadv1alpha1.AMFDeployment) (*appsv1.Deployment, error) { + //TODO(jbelamaric): Update to use ImageConfig spec.ImagePaths["amf"], + amfImage := "towards5gs/free5gc-amf:v3.2.0" + + instanceName := amfDeploy.ObjectMeta.Name + namespace := amfDeploy.ObjectMeta.Namespace + amfspec := amfDeploy.Spec + //var wrapperMode int32 = 511 // 777 octal + replicas, resourceReq, err := getAMFResourceParams(amfspec) + if err != nil { + return nil, err + } + instanceNadLabel := getAMFNad(amfDeploy.ObjectMeta.Name, &amfspec) + instanceNad := make(map[string]string) + instanceNad["k8s.v1.cni.cncf.io/networks"] = instanceNadLabel + securityContext := &apiv1.SecurityContext{ + Capabilities: &apiv1.Capabilities{ + Add: []apiv1.Capability{"NET_ADMIN"}, + Drop: nil, + }, + } + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: instanceName, + Namespace: namespace, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "name": instanceName, + }, + }, + Template: apiv1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: instanceNad, + Labels: map[string]string{ + "name": instanceName, + }, + }, + Spec: apiv1.PodSpec{ + Containers: []apiv1.Container{ + { + Name: "amf", + Image: amfImage, + ImagePullPolicy: "Always", + SecurityContext: securityContext, + Ports: []apiv1.ContainerPort{ + { + Name: "n2", + Protocol: apiv1.ProtocolUDP, + ContainerPort: 8805, + }, + }, + // Command: []string{ + // "/free5gc/config//wrapper.sh", + // }, + VolumeMounts: []apiv1.VolumeMount{ + { + MountPath: "/free5gc/config/", + Name: "amf-volume", + }, + }, + Resources: *resourceReq, + }, + }, // Containers + DNSPolicy: "ClusterFirst", + RestartPolicy: "Always", + Volumes: []apiv1.Volume{ + { + Name: "amf-volume", + VolumeSource: apiv1.VolumeSource{ + Projected: &apiv1.ProjectedVolumeSource{ + Sources: []apiv1.VolumeProjection{ + { + ConfigMap: &apiv1.ConfigMapProjection{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: instanceName + "-amf-configmap", + }, + Items: []apiv1.KeyToPath{ + { + Key: "amfcfg.yaml", + Path: "amfcfg.yaml", + }, + // { + // Key: "wrapper.sh", + // Path: "wrapper.sh", + // Mode: &wrapperMode, + // }, + }, + }, + }, + }, + }, + }, + }, + }, // Volumes + }, // PodSpec + }, // PodTemplateSpec + }, // PodTemplateSpec + } + return deployment, nil +} + +func free5gcAMFCreateConfigmap(logger logr.Logger, amfDeploy *workloadv1alpha1.AMFDeployment) (*apiv1.ConfigMap, error) { + namespace := amfDeploy.ObjectMeta.Namespace + instanceName := amfDeploy.ObjectMeta.Name + + n2IP, err := getIPv4(amfDeploy.Spec.Interfaces, "n2") + if err != nil { + log.Log.Info("Interface N2 not found in NFDeployment Spec") + return nil, err + } + // n11IP, err := getIPv4(amfDeploy.Spec.Interfaces, "N11") + // if err != nil { + // log.Log.Info("Interface N3 not found in NFDeployment Spec") + // return nil, err + // } + + // n6IP, err := getIntConfig(amfDeploy.Spec.Interfaces, "N6") + // if err != nil { + // log.Log.Info("Interface N2 not found in NFDeployment Spec") + // return nil, err + // } + + amfcfgStruct := AMFcfgStruct{} + amfcfgStruct.N2_IP = n2IP + // AMFcfgStruct.N11_IP = n11IP + // AMFcfgStruct.N6gw = string(*n6IP.IPv4.Gateway) + + // n6Instances, ok := getNetworkInsance(AMFDeploy.Spec, "N6") + // if !ok { + // log.Log.Info("No N6 interface in NFDeployment Spec.") + // return nil, errors.New("No N6 intefaces in NFDeployment Spec.") + // } + // AMFcfgStruct.N6cfg = n6Instances + + amfcfgTemplate := template.New("AMFCfg") + amfcfgTemplate, err = amfcfgTemplate.Parse(AMFCfgTemplate) + if err != nil { + log.Log.Info("Could not parse AMFCfgTemplate template.") + return nil, err + } + // amfwrapperTemplate := template.New("AMFCfg") + // amfwrapperTemplate, _ = amfwrapperTemplate.Parse(AMFWrapperScript) + // if err != nil { + // log.Log.Info("Could not parse AMFWrapperScript template.") + // return nil, err + // } + + // var wrapper bytes.Buffer + // if err := amfwrapperTemplate.Execute(&wrapper, amfcfgStruct); err != nil { + // log.Log.Info("Could not render AMFWrapperScript template.") + // return nil, err + // } + + var amfcfg bytes.Buffer + if err := amfcfgTemplate.Execute(&amfcfg, amfcfgStruct); err != nil { + log.Log.Info("Could not render AMFCfgTemplate template.") + return nil, err + } + + configMap := &apiv1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: instanceName + "-amf-configmap", + Namespace: namespace, + }, + Data: map[string]string{ + "amfcfg.yaml": amfcfg.String(), + // "wrapper.sh": wrapper.String(), + }, + } + return configMap, nil +} + +func (r *AMFDeploymentReconciler) syncAMFStatus(ctx context.Context, d *appsv1.Deployment, amfDeploy *workloadv1alpha1.AMFDeployment) error { + newAMFStatus, update := calculateAMFStatus(d, amfDeploy) + + if update { + // Update AMFDeployment status according to underlying deployment status + newAmf := amfDeploy + newAmf.Status.NFDeploymentStatus = newAMFStatus + err := r.Status().Update(ctx, newAmf) + return err + } + + return nil +} + +func calculateAMFStatus(deployment *appsv1.Deployment, amfDeploy *workloadv1alpha1.AMFDeployment) (workloadv1alpha1.NFDeploymentStatus, bool) { + amfstatus := workloadv1alpha1.NFDeploymentStatus{ + ObservedGeneration: int32(deployment.Generation), + Conditions: amfDeploy.Status.Conditions, + } + condition := metav1.Condition{} + + // Return initial status if there are no status update happened for the AMFdeployment + if len(amfDeploy.Status.Conditions) == 0 { + condition.Type = string(workloadv1alpha1.Reconciling) + condition.Status = metav1.ConditionFalse + condition.Reason = "MinimumReplicasNotAvailable" + condition.Message = "AMFDeployment pod(s) is(are) starting." + condition.LastTransitionTime = metav1.Now() + + amfstatus.Conditions = append(amfstatus.Conditions, condition) + + return amfstatus, true + + } else if len(deployment.Status.Conditions) == 0 && len(amfDeploy.Status.Conditions) > 0 { + return amfstatus, false + } + + // Check the last underlying Deployment status and deduct condition from it. + lastDeploymentStatus := deployment.Status.Conditions[0] + lastAMFDeploymentStatus := amfDeploy.Status.Conditions[len(amfDeploy.Status.Conditions)-1] + + // Deployemnt and AMFDeployment have different names for processing state, hence we check if one is processing another is reconciling, then state is equal + if lastDeploymentStatus.Type == appsv1.DeploymentProgressing && lastAMFDeploymentStatus.Type == string(workloadv1alpha1.Reconciling) { + return amfstatus, false + } + + // if both status types are Available, don't update. + if string(lastDeploymentStatus.Type) == string(lastAMFDeploymentStatus.Type) { + return amfstatus, false + } + + condition.LastTransitionTime = lastDeploymentStatus.DeepCopy().LastTransitionTime + if lastDeploymentStatus.Type == appsv1.DeploymentAvailable { + condition.Type = string(workloadv1alpha1.Available) + condition.Status = metav1.ConditionTrue + condition.Reason = "MinimumReplicasAvailable" + condition.Message = "AMFDeployment pods are available." + condition.LastTransitionTime = metav1.Now() + } else if lastDeploymentStatus.Type == appsv1.DeploymentProgressing { + condition.Type = string(workloadv1alpha1.Reconciling) + condition.Status = metav1.ConditionFalse + condition.Reason = "MinimumReplicasNotAvailable" + condition.Message = "AMFDeployment pod(s) is(are) starting." + condition.LastTransitionTime = metav1.Now() + } else if lastDeploymentStatus.Type == appsv1.DeploymentReplicaFailure { + condition.Type = string(workloadv1alpha1.Stalled) + condition.Status = metav1.ConditionFalse + condition.Reason = "MinimumReplicasNotAvailable" + condition.Message = "AMFDeployment pod(s) is(are) failing." + condition.LastTransitionTime = metav1.Now() + } + + amfstatus.Conditions = append(amfstatus.Conditions, condition) + + return amfstatus, true +} + +//+kubebuilder:rbac:groups=workload.nephio.org,resources=AMFdeployments,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=workload.nephio.org,resources=AMFdeployments/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=workload.nephio.org,resources=AMFdeployments/finalizers,verbs=update +//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get +//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch +//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups="k8s.cni.cncf.io",resources=network-attachment-definitions,verbs=get;list;watch +//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch + +// 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 AMFDeployment 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.14.1/pkg/reconcile +func (r *AMFDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx).WithValues("AMFDeployment", req.NamespacedName) + + amfDeploy := &workloadv1alpha1.AMFDeployment{} + err := r.Client.Get(ctx, req.NamespacedName, amfDeploy) + if err != nil { + if k8serrors.IsNotFound(err) { + log.Info("AMFDeployment resource not found. Ignoring since object must be deleted") + return reconcile.Result{}, nil + } + log.Error(err, "Error: failed to get AMFDeployment") + return reconcile.Result{}, err + } + + namespace := amfDeploy.ObjectMeta.Namespace + // see if we are dealing with create or update + cmFound := false + configmapName := amfDeploy.ObjectMeta.Name + "-amf-configmap" + currConfigmap := &apiv1.ConfigMap{} + if err := r.Client.Get(ctx, types.NamespacedName{Name: configmapName, Namespace: namespace}, currConfigmap); err == nil { + cmFound = true + } + + dmFound := false + dmName := amfDeploy.ObjectMeta.Name + currDeployment := &appsv1.Deployment{} + if err := r.Client.Get(ctx, types.NamespacedName{Name: dmName, Namespace: namespace}, currDeployment); err == nil { + dmFound = true + } + + if dmFound { + d := currDeployment.DeepCopy() + + // Updating AMFDeployment status. On the first sets the first Condition to Reconciling. + // On the subsequent runs it gets undelying depoyment Conditions and use the last one to decide if status has to be updated. + if d.DeletionTimestamp == nil { + if err := r.syncAMFStatus(ctx, d, amfDeploy); err != nil { + log.Error(err, "Failed to update AMFDeployment status", "AMFDeployment.namespace", namespace, "AMFDeployment.name", amfDeploy.Name) + return reconcile.Result{}, err + } + } + } + + // first set up the configmap + if cm, err := free5gcAMFCreateConfigmap(log, amfDeploy); err != nil { + log.Error(err, fmt.Sprintf("Error: failed to generate configmap %s\n", err.Error())) + return reconcile.Result{}, err + } else { + if !cmFound { + log.Info("Creating AMFDeployment configmap", "AMFDeployment.namespace", namespace, "Confirmap.name", cm.ObjectMeta.Name) + // Set the controller reference, specifying that AMFDeployment controling underlying deployment + if err := ctrl.SetControllerReference(amfDeploy, cm, r.Scheme); err != nil { + log.Error(err, "Got error while setting Owner reference on configmap.", "AMFDeployment.namespace", namespace) + } + if err := r.Client.Create(ctx, cm); err != nil { + log.Error(err, fmt.Sprintf("Error: failed to create configmap %s\n", err.Error())) + return reconcile.Result{}, err + } + } + } + + if deployment, err := free5gcAMFDeployment(log, amfDeploy); err != nil { + log.Error(err, fmt.Sprintf("Error: failed to generate deployment %s\n", err.Error())) + return reconcile.Result{}, err + } else { + if !dmFound { + // only create deployment in case all required NADs are present. Otherwse Requeue in 10 sec. + if ok := r.checkAMFNADexist(log, ctx, deployment); !ok { + log.Info("Not all NetworkAttachDefinitions available in current namespace. Requeue in 10 sec.", "AMFDeployment.namespace", namespace) + return reconcile.Result{RequeueAfter: time.Duration(10) * time.Second}, nil + } else { + // Set the controller reference, specifying that AMFDeployment controling underlying deployment + if err := ctrl.SetControllerReference(amfDeploy, deployment, r.Scheme); err != nil { + log.Error(err, "Got error while setting Owner reference on deployment.", "AMFDeployment.namespace", namespace) + } + log.Info("Creating AMFDeployment", "AMFDeployment.namespace", namespace, "AMFDeployment.name", amfDeploy.Name) + if err := r.Client.Create(ctx, deployment); err != nil { + log.Error(err, "Failed to create new Deployment", "AMFDeployment.namespace", namespace, "AMFDeployment.name", amfDeploy.Name) + } + return reconcile.Result{RequeueAfter: time.Duration(30) * time.Second}, nil + } + } + } + return reconcile.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *AMFDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&workloadv1alpha1.AMFDeployment{}). + Owns(&appsv1.Deployment{}). + Owns(&apiv1.ConfigMap{}). + Complete(r) +} diff --git a/free5gc-operator/controllers/amfdeployment_controller_test.go b/free5gc-operator/controllers/amfdeployment_controller_test.go new file mode 100644 index 0000000..abc752a --- /dev/null +++ b/free5gc-operator/controllers/amfdeployment_controller_test.go @@ -0,0 +1,530 @@ +/* +Copyright 2023 The Nephio Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "bytes" + "context" + "html/template" + "reflect" + "testing" + + appsv1 "k8s.io/api/apps/v1" + apiv1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/log" + + workloadv1alpha1 "github.com/nephio-project/api/nf_deployments/v1alpha1" + nephioreqv1alpha1 "github.com/nephio-project/api/nf_requirements/v1alpha1" +) + +func newAMFNxInterface(name string) workloadv1alpha1.InterfaceConfig { + switch name { + case "n2": + gw := "10.10.10.1" + n2int := workloadv1alpha1.InterfaceConfig{ + Name: "N2", + IPv4: &workloadv1alpha1.IPv4{ + Address: "10.10.10.10/24", + Gateway: &gw, + }, + } + return n2int + } + return workloadv1alpha1.InterfaceConfig{} +} + +func newAmfDeployInstance(name string) *workloadv1alpha1.AMFDeployment { + interfaces := []workloadv1alpha1.InterfaceConfig{} + n2int := newAMFNxInterface("n2") + interfaces = append(interfaces, n2int) + dnnName := "apn-test" + + amfDeployInstance := &workloadv1alpha1.AMFDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: name + "-ns", + }, + Spec: workloadv1alpha1.AMFDeploymentSpec{ + NFDeploymentSpec: workloadv1alpha1.NFDeploymentSpec{ + ConfigRefs: []apiv1.ObjectReference{}, + Capacity: &nephioreqv1alpha1.CapacitySpec{ + MaxUplinkThroughput: resource.MustParse("1G"), + MaxDownlinkThroughput: resource.MustParse("5G"), + MaxSessions: 1000, + MaxSubscribers: 1000, + MaxNFConnections: 2000, + }, + Interfaces: interfaces, + NetworkInstances: []workloadv1alpha1.NetworkInstance{ + { + Name: "vpc-internet", + Interfaces: []string{ + "N2", + }, + DataNetworks: []workloadv1alpha1.DataNetwork{ + { + Name: &dnnName, + Pool: []workloadv1alpha1.Pool{ + { + Prefix: "100.100.0.0/16", + }, + }, + }, + }, + BGP: nil, + Peers: []workloadv1alpha1.PeerConfig{}, + }, + }, + }, + }, + } + + return amfDeployInstance +} + +func TestGetAMFResourceParams(t *testing.T) { + amfDeploymentInstance := newAmfDeployInstance("test-amf-deployment") + + replicas, got, err := getAMFResourceParams(amfDeploymentInstance.Spec) + if err != nil { + t.Errorf("getAMFResourceParams() returned unexpected error %v", err) + } + if replicas != 1 { + t.Errorf("getAMFResourceParams returned number of replicas = %d, want %d", replicas, 1) + } + want := &apiv1.ResourceRequirements{ + Limits: apiv1.ResourceList{ + "cpu": resource.MustParse("150m"), + "memory": resource.MustParse("128Mi"), + }, + Requests: apiv1.ResourceList{ + "cpu": resource.MustParse("150m"), + "memory": resource.MustParse("128Mi"), + }, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("getResourceParams(%+v) returned %+v, want %+v", amfDeploymentInstance.Spec, got, want) + } +} + +func TestConstructAMFNadName(t *testing.T) { + var tests = []struct { + args []string + want string + }{ + {[]string{"test-amf-deployment", "n2"}, "test-amf-deployment-n2"}, + } + for _, test := range tests { + if got := constructAMFNadName(test.args[0], test.args[1]); got != test.want { + t.Errorf("constructAMFNadNAme(%s, %s) = %v, want %s", test.args[0], test.args[1], got, test.want) + } + } +} + +func TestGetAMFNad(t *testing.T) { + amfDeploymentInstance := newAmfDeployInstance("test-amf-deployment") + got := getAMFNad("test-amf-deployment", &amfDeploymentInstance.DeepCopy().Spec) + + want := `[ + {"name": "test-amf-deployment-n2", + "interface": "N2", + "ips": ["10.10.10.10/24"], + "gateways": ["10.10.10.1"] + } + ]` + + if got != want { + t.Errorf("getNad(%v) returned %v, want %v", amfDeploymentInstance.Spec, got, want) + } +} + +func TestFree5gcAMFCreateConfigmap(t *testing.T) { + log := log.FromContext(context.TODO()) + amfDeploymentInstance := newAmfDeployInstance("test-amf-deployment") + got, err := free5gcAMFCreateConfigmap(log, amfDeploymentInstance) + if err != nil { + t.Errorf("free5gcAMFCreateConfigmap() returned unexpected error %v", err) + } + + n2IP, _ := getIPv4(amfDeploymentInstance.Spec.Interfaces, "N2") + + amfcfgStruct := AMFcfgStruct{} + amfcfgStruct.N2_IP = n2IP + + amfcfgTemplate := template.New("AMFCfg") + amfcfgTemplate, err = amfcfgTemplate.Parse(AMFCfgTemplate) + if err != nil { + t.Error("Could not parse AMFCfgTemplate template.") + } + // amfwrapperTemplate, _ = amfwrapperTemplate.Parse(AMFWrapperScript) + // if err != nil { + // t.Error("Could not parse UPFWrapperScript template.") + // } + + // var wrapper bytes.Buffer + // if err := upfwrapperTemplate.Execute(&wrapper, upfcfgStruct); err != nil { + // t.Error("Could not render UPFWrapperScript template.") + // } + + var amfcfg bytes.Buffer + if err := amfcfgTemplate.Execute(&amfcfg, amfcfgStruct); err != nil { + t.Error("Could not render AMFConfig template.") + } + + want := &apiv1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: amfDeploymentInstance.ObjectMeta.Name + "-amf-configmap", + Namespace: amfDeploymentInstance.ObjectMeta.Namespace, + }, + Data: map[string]string{ + "amfcfg.yaml": amfcfg.String(), + // "wrapper.sh": wrapper.String(), + }, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("free5gcUSMCreateConfigmap(%+v) returned %+v, want %+v", amfDeploymentInstance, got, want) + } +} + +func TestCaclculateAMFStatusFirstReconcile(t *testing.T) { + upfDeploymentInstance := newUpfDeployInstance("test-upf-deployment") + deployment := &appsv1.Deployment{} + + want := workloadv1alpha1.NFDeploymentStatus{ + ObservedGeneration: int32(deployment.Generation), + Conditions: upfDeploymentInstance.Status.Conditions, + } + + condition := metav1.Condition{} + condition.Type = string(workloadv1alpha1.Reconciling) + condition.Status = metav1.ConditionFalse + condition.Reason = "MinimumReplicasNotAvailable" + condition.Message = "UPFDeployment pod(s) is(are) starting." + // condition.LastTransitionTime = metav1.Now() + want.Conditions = append(want.Conditions, condition) + + got, b := calculateStatus(deployment, upfDeploymentInstance) + + gotCondition := got.Conditions[0] + gotCondition.LastTransitionTime = metav1.Time{} + + if !reflect.DeepEqual(gotCondition, condition) { + t.Errorf("calculateStatus(%+v, %+v) returned %+v, want %+v", deployment, upfDeploymentInstance, got, want) + } + if b != true { + t.Errorf("calculateStatus(%+v, %+v) returned %+v, want %+v", deployment, upfDeploymentInstance, b, true) + } +} + +func TestCaclculateAMFStatusDeployemntNotReady(t *testing.T) { + amfDeploymentInstance := newAmfDeployInstance("test-amf-deployment") + deployment := &appsv1.Deployment{} + + condition := metav1.Condition{} + condition.Type = string(workloadv1alpha1.Reconciling) + condition.Status = metav1.ConditionFalse + condition.Reason = "MinimumReplicasNotAvailable" + condition.Message = "AMFDeployment pod(s) is(are) starting." + condition.LastTransitionTime = metav1.Now() + amfDeploymentInstance.Status.Conditions = append(amfDeploymentInstance.Status.Conditions, condition) + + want := amfDeploymentInstance.Status.NFDeploymentStatus + + got, b := calculateAMFStatus(deployment, amfDeploymentInstance) + + if !reflect.DeepEqual(got, want) { + t.Errorf("calculateSMFStatus(%+v, %+v) returned %+v, want %+v", deployment, amfDeploymentInstance, got, want) + } + if b != false { + t.Errorf("calculateSMFStatus(%+v, %+v) returned %+v, want %+v", deployment, amfDeploymentInstance, b, false) + } +} + +func TestCaclculateAMFStatusProcessing(t *testing.T) { + amfDeploymentInstance := newAmfDeployInstance("test-amf-deployment") + deployment := &appsv1.Deployment{} + + condition := metav1.Condition{} + condition.Type = string(workloadv1alpha1.Reconciling) + deploymentCondition := &appsv1.DeploymentCondition{} + deploymentCondition.Type = appsv1.DeploymentProgressing + amfDeploymentInstance.Status.Conditions = append(amfDeploymentInstance.Status.Conditions, condition) + deployment.Status.Conditions = append(deployment.Status.Conditions, *deploymentCondition) + + want := amfDeploymentInstance.Status.NFDeploymentStatus + + got, b := calculateAMFStatus(deployment, amfDeploymentInstance) + + if !reflect.DeepEqual(got, want) { + t.Errorf("calculateAMFStatus(%+v, %+v) returned %+v, want %+v", deployment, amfDeploymentInstance, got, want) + } + if b != false { + t.Errorf("calculateAMFStatus(%+v, %+v) returned %+v, want %+v", deployment, amfDeploymentInstance, b, false) + } +} + +func TestCaclculateAMFStatusAvailable(t *testing.T) { + amfDeploymentInstance := newAmfDeployInstance("test-amf-deployment") + deployment := &appsv1.Deployment{} + + condition := metav1.Condition{} + condition.Type = string(workloadv1alpha1.Available) + deploymentCondition := &appsv1.DeploymentCondition{} + deploymentCondition.Type = appsv1.DeploymentAvailable + amfDeploymentInstance.Status.Conditions = append(amfDeploymentInstance.Status.Conditions, condition) + deployment.Status.Conditions = append(deployment.Status.Conditions, *deploymentCondition) + + want := amfDeploymentInstance.Status.NFDeploymentStatus + + got, b := calculateAMFStatus(deployment, amfDeploymentInstance) + + if !reflect.DeepEqual(got, want) { + t.Errorf("calculateAMFtatus(%+v, %+v) returned %+v, want %+v", deployment, amfDeploymentInstance, got, want) + } + if b != false { + t.Errorf("calculateAMFStatus(%+v, %+v) returned %+v, want %+v", deployment, amfDeploymentInstance, b, false) + } +} + +func TestCaclculateAMFStatusDeploymentAvailable(t *testing.T) { + amfDeploymentInstance := newAmfDeployInstance("test-amf-deployment") + deployment := &appsv1.Deployment{} + + condition := metav1.Condition{} + condition.Type = string(workloadv1alpha1.Reconciling) + condition.Status = metav1.ConditionFalse + condition.Reason = "MinimumReplicasNotAvailable" + condition.Message = "AMFDeployment pod(s) is(are) starting." + deploymentCondition := &appsv1.DeploymentCondition{} + deploymentCondition.Type = appsv1.DeploymentAvailable + deploymentCondition.Reason = "MinimumReplicasAvailable" + amfDeploymentInstance.Status.Conditions = append(amfDeploymentInstance.Status.Conditions, condition) + deployment.Status.Conditions = append(deployment.Status.Conditions, *deploymentCondition) + + want := amfDeploymentInstance.Status.NFDeploymentStatus + condition.Type = string(workloadv1alpha1.Available) + condition.Status = metav1.ConditionTrue + condition.Reason = "MinimumReplicasAvailable" + condition.Message = "AMFDeployment pods are available." + want.Conditions = append(want.Conditions, condition) + + got, b := calculateAMFStatus(deployment, amfDeploymentInstance) + + gotCondition := got.Conditions[1] + gotCondition.LastTransitionTime = metav1.Time{} + got.Conditions = got.Conditions[:len(got.Conditions)-1] + got.Conditions = append(got.Conditions, gotCondition) + + if !reflect.DeepEqual(got, want) { + t.Errorf("calculateAMFStatus(%+v, %+v) returned %+v, want %+v", deployment, amfDeploymentInstance, got, want) + } + if b != true { + t.Errorf("calculateAMFStatus(%+v, %+v) returned %+v, want %+v", deployment, amfDeploymentInstance, b, true) + } +} + +func TestCaclculateAMFStatusDeploymentProcessing(t *testing.T) { + amfDeploymentInstance := newAmfDeployInstance("test-amf-deployment") + deployment := &appsv1.Deployment{} + + condition := metav1.Condition{} + condition.Type = string(workloadv1alpha1.Available) + condition.Status = metav1.ConditionTrue + condition.Reason = "MinimumReplicasAvailable" + condition.Message = "AMFDeployment pods are available" + deploymentCondition := &appsv1.DeploymentCondition{} + deploymentCondition.Type = appsv1.DeploymentProgressing + amfDeploymentInstance.Status.Conditions = append(amfDeploymentInstance.Status.Conditions, condition) + deployment.Status.Conditions = append(deployment.Status.Conditions, *deploymentCondition) + + want := amfDeploymentInstance.Status.NFDeploymentStatus + condition.Type = string(workloadv1alpha1.Reconciling) + condition.Status = metav1.ConditionFalse + condition.Reason = "MinimumReplicasNotAvailable" + condition.Message = "AMFDeployment pod(s) is(are) starting." + want.Conditions = append(want.Conditions, condition) + + got, b := calculateAMFStatus(deployment, amfDeploymentInstance) + + gotCondition := got.Conditions[1] + gotCondition.LastTransitionTime = metav1.Time{} + got.Conditions = got.Conditions[:len(got.Conditions)-1] + got.Conditions = append(got.Conditions, gotCondition) + + if !reflect.DeepEqual(got, want) { + t.Errorf("calculateAMFStatus(%+v, %+v) returned %+v, want %+v", deployment, amfDeploymentInstance, got, want) + } + if b != true { + t.Errorf("calculateAMFStatus(%+v, %+v) returned %+v, want %+v", deployment, amfDeploymentInstance, b, true) + } +} + +func TestCaclculateAMFStatusReplicaFailure(t *testing.T) { + amfDeploymentInstance := newAmfDeployInstance("test-amf-deployment") + deployment := &appsv1.Deployment{} + + condition := metav1.Condition{} + condition.Type = string(workloadv1alpha1.Available) + condition.Status = metav1.ConditionTrue + condition.Reason = "MinimumReplicasAvailable" + condition.Message = "AMFDeployment pods are available" + deploymentCondition := &appsv1.DeploymentCondition{} + deploymentCondition.Type = appsv1.DeploymentReplicaFailure + amfDeploymentInstance.Status.Conditions = append(amfDeploymentInstance.Status.Conditions, condition) + deployment.Status.Conditions = append(deployment.Status.Conditions, *deploymentCondition) + + want := amfDeploymentInstance.Status.NFDeploymentStatus + condition.Type = string(workloadv1alpha1.Stalled) + condition.Status = metav1.ConditionFalse + condition.Reason = "MinimumReplicasNotAvailable" + condition.Message = "AMFDeployment pod(s) is(are) failing." + want.Conditions = append(want.Conditions, condition) + + got, b := calculateAMFStatus(deployment, amfDeploymentInstance) + + gotCondition := got.Conditions[1] + gotCondition.LastTransitionTime = metav1.Time{} + got.Conditions = got.Conditions[:len(got.Conditions)-1] + got.Conditions = append(got.Conditions, gotCondition) + + if !reflect.DeepEqual(got, want) { + t.Errorf("calculateAMFStatus(%+v, %+v) returned %+v, want %+v", deployment, amfDeploymentInstance, got, want) + } + if b != true { + t.Errorf("calculateAMFStatus(%+v, %+v) returned %+v, want %+v", deployment, amfDeploymentInstance, b, true) + } +} + +func TestFree5gcAMFDeployment(t *testing.T) { + log := log.FromContext(context.TODO()) + amfDeploymentInstance := newAmfDeployInstance("test-amf-deployment") + got, err := free5gcAMFDeployment(log, amfDeploymentInstance) + if err != nil { + t.Errorf("free5gcAMFDeployment() returned unexpected error %v", err) + } + + // var wrapperMode int32 = 511 // 777 octal + var replicas int32 = 1 + want := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-amf-deployment", + Namespace: "test-amf-deployment-ns", + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "name": "test-amf-deployment", + }, + }, + Template: apiv1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[ + {"name": "test-amf-deployment-n2", + "interface": "N2", + "ips": ["10.10.10.10/24"], + "gateways": ["10.10.10.1"] + } + ]`, + }, + Labels: map[string]string{ + "name": "test-amf-deployment", + }, + }, + Spec: apiv1.PodSpec{ + Containers: []apiv1.Container{ + { + Name: "amf", + Image: "towards5gs/free5gc-amf:v3.2.0", + ImagePullPolicy: "Always", + SecurityContext: &apiv1.SecurityContext{ + Capabilities: &apiv1.Capabilities{ + Add: []apiv1.Capability{"NET_ADMIN"}, + Drop: nil, + }, + }, + Ports: []apiv1.ContainerPort{ + { + Name: "n2", + Protocol: apiv1.ProtocolUDP, + ContainerPort: 8805, + }, + }, + // Command: []string{ + // "/free5gc/config//wrapper.sh", + // }, + VolumeMounts: []apiv1.VolumeMount{ + { + MountPath: "/free5gc/config/", + Name: "amf-volume", + }, + }, + Resources: apiv1.ResourceRequirements{ + Limits: apiv1.ResourceList{ + "cpu": resource.MustParse("150m"), + "memory": resource.MustParse("128Mi"), + }, + Requests: apiv1.ResourceList{ + "cpu": resource.MustParse("150m"), + "memory": resource.MustParse("128Mi"), + }, + }, + }, + }, // Containers + DNSPolicy: "ClusterFirst", + RestartPolicy: "Always", + Volumes: []apiv1.Volume{ + { + Name: "amf-volume", + VolumeSource: apiv1.VolumeSource{ + Projected: &apiv1.ProjectedVolumeSource{ + Sources: []apiv1.VolumeProjection{ + { + ConfigMap: &apiv1.ConfigMapProjection{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: "test-amf-deployment-amf-configmap", + }, + Items: []apiv1.KeyToPath{ + { + Key: "amfcfg.yaml", + Path: "amfcfg.yaml", + }, + // { + // Key: "wrapper.sh", + // Path: "wrapper.sh", + // Mode: &wrapperMode, + // }, + }, + }, + }, + }, + }, + }, + }, + }, // Volumes + }, // PodSpec + }, // PodTemplateSpec + }, // PodTemplateSpec + } + if !reflect.DeepEqual(got, want) { + t.Errorf("free5gcAMFDeployment(%v) returned %v, want %v", amfDeploymentInstance, got, want) + } +} diff --git a/free5gc-operator/controllers/free5gc_amfcfg.go b/free5gc-operator/controllers/free5gc_amfcfg.go new file mode 100644 index 0000000..2b29474 --- /dev/null +++ b/free5gc-operator/controllers/free5gc_amfcfg.go @@ -0,0 +1,136 @@ +/* +Free5GC Config +N2: NF Deployment IF + +sbi: + + scheme: http + registerIPv4: amf-namf + bindingIPv4: 0.0.0.0 + port: 80 + tls: + key: config/TLS/smf.key + pem: config/TLS/smf.pem + + nrfUri: http://nrf-nnrf:8000 +*/ +package controllers + +var AMFCfgTemplate string = ` +info: + version: 1.0.0 + description: AMF configuration + +configuration: + ReportCaller: false + debugLevel: info + serviceNameList: + - namf-comm + - namf-evts + - namf-oam + - namf-mt + - namf-loc + ngapIpList: + - addr: {{ .N2_IP }} + + sbi: + scheme: http + registerIPv4: amf-namf + bindingIPv4: 0.0.0.0 + port: 80 + tls: + key: config/TLS/amf.key + pem: config/TLS/amf.pem + + nrfUri: http://nrf-nnrf:8000 + + servedGuamiList: + - plmnId: + mcc: 208 + mnc: 93 + amfId: cafe00 + supportTaiList: + - plmnId: + mcc: 208 + mnc: 93 + tac: 1 + plmnSupportList: + - plmnId: + mcc: 208 + mnc: 93 + snssaiList: + - sst: 1 + sd: 010203 + - sst: 1 + sd: 112233 + supportDnnList: + - internet + security: + integrityOrder: + - NIA2 + cipheringOrder: + - NEA0 + networkName: + full: free5GC + short: free + locality: area1 # Name of the location where a set of AMF, SMF and UPFs are located + networkFeatureSupport5GS: # 5gs Network Feature Support IE, refer to TS 24.501 + enable: true # append this IE in Registration accept or not + length: 1 # IE content length (uinteger, range: 1~3) + imsVoPS: 0 # IMS voice over PS session indicator (uinteger, range: 0~1) + emc: 0 # Emergency service support indicator for 3GPP access (uinteger, range: 0~3) + emf: 0 # Emergency service fallback indicator for 3GPP access (uinteger, range: 0~3) + iwkN26: 0 # Interworking without N26 interface indicator (uinteger, range: 0~1) + mpsi: 0 # MPS indicator (uinteger, range: 0~1) + emcN3: 0 # Emergency service support indicator for Non-3GPP access (uinteger, range: 0~1) + mcsi: 0 # MCS indicator (uinteger, range: 0~1) + t3502Value: 720 + t3512Value: 3600 + non3gppDeregistrationTimerValue: 3240 + # retransmission timer for paging message + t3513: + enable: true # true or false + expireTime: 6s # default is 6 seconds + maxRetryTimes: 4 # the max number of retransmission + # retransmission timer for NAS Registration Accept message + t3522: + enable: true # true or false + expireTime: 6s # default is 6 seconds + maxRetryTimes: 4 # the max number of retransmission + # retransmission timer for NAS Registration Accept message + t3550: + enable: true # true or false + expireTime: 6s # default is 6 seconds + maxRetryTimes: 4 # the max number of retransmission + # retransmission timer for NAS Authentication Request/Security Mode Command message + t3560: + enable: true # true or false + expireTime: 6s # default is 6 seconds + maxRetryTimes: 4 # the max number of retransmission + # retransmission timer for NAS Notification message + t3565: + enable: true # true or false + expireTime: 6s # default is 6 seconds + maxRetryTimes: 4 # the max number of retransmission + t3570: + enable: true # true or false + expireTime: 6s # default is 6 seconds + maxRetryTimes: 4 # the max number of retransmission + + logger: + AMF: + ReportCaller: false + debugLevel: info + Aper: + ReportCaller: false + debugLevel: info + FSM: + ReportCaller: false + debugLevel: info + NAS: + ReportCaller: false + debugLevel: info + NGAP: + ReportCaller: false + debugLevel: info +` diff --git a/free5gc-operator/controllers/free5gc_amfsvc.go b/free5gc-operator/controllers/free5gc_amfsvc.go new file mode 100644 index 0000000..ff993b2 --- /dev/null +++ b/free5gc-operator/controllers/free5gc_amfsvc.go @@ -0,0 +1,24 @@ +package controllers + +var AMFService string = ` +info: + version: 1.0.2 + description: AMF service + +service: + serviceNameList: + - amf-namf + +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: http + selector: + app.kubernetes.io/name: free5gc-amf + app.kubernetes.io/instance: amf + project: free5gc + nf: amf +` diff --git a/free5gc-operator/test/amfdeploy.yaml b/free5gc-operator/test/amfdeploy.yaml new file mode 100644 index 0000000..5d42b36 --- /dev/null +++ b/free5gc-operator/test/amfdeploy.yaml @@ -0,0 +1,22 @@ +# TODO +apiVersion: workload.nephio.org/v1alpha1 +kind: AMFDeployment +metadata: + name: free5gc-amf-1 + namespace: amf-1 + annotations: + config.kubernetes.io/local-config: "false" +spec: + capacity: + maxSubscribers: 1000 + interfaces: + - name: n2 + ipv4: + address: 10.10.10.10/24 + gateway: 10.10.10.1 + vlanID: 14 + networkInstances: + - name: vpc-internal + interfaces: + - n2 + configRefs: diff --git a/free5gc-operator/test/nad/amf-n2-nad.yaml b/free5gc-operator/test/nad/amf-n2-nad.yaml new file mode 100644 index 0000000..156839c --- /dev/null +++ b/free5gc-operator/test/nad/amf-n2-nad.yaml @@ -0,0 +1,38 @@ +--- +# Source: free5gc/charts/free5gc-upf/templates/upf-n4-nad.yaml +# +# Software Name : towards5gs-helm +# SPDX-FileCopyrightText: Copyright (c) 2021 Orange +# SPDX-License-Identifier: Apache-2.0 +# +# This software is distributed under the Apache License 2.0, +# the text of which is available at https://github.com/Orange-OpenSource/towards5gs-helm/blob/main/LICENSE +# or see the "LICENSE" file for more details. +# +# Author: Abderaouf KHICHANE, Ilhem FAJJARI, Ayoub BOUSSELMI +# Software description: An open-source project providing Helm charts to deploy 5G components (Core + RAN) on top of Kubernetes +# +apiVersion: "k8s.cni.cncf.io/v1" +kind: NetworkAttachmentDefinition +metadata: + name: free5gc-amf-1-n2 + namespace: amf-1 +spec: + config: '{ + "cniVersion": "0.3.1", + "plugins": [ + { + "type": "macvlan", + "capabilities": { "ips": true }, + "master": "ens5", + "mode": "bridge", + "ipam": { + "type": "static", + "routes": [] + } + }, { + "capabilities": { "mac": true }, + "type": "tuning" + } + ] + }' From d9ee9665ee5b2063a410de0c7765edbfa7619a9c Mon Sep 17 00:00:00 2001 From: vireshnavalli Date: Fri, 26 May 2023 05:58:12 +0000 Subject: [PATCH 2/9] updated main.go --- free5gc-operator/main.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/free5gc-operator/main.go b/free5gc-operator/main.go index 04809de..79d65af 100644 --- a/free5gc-operator/main.go +++ b/free5gc-operator/main.go @@ -99,6 +99,20 @@ func main() { os.Exit(1) } + schemeBuilder.Register(&workloadv1alpha1.AMFDeployment{}, &workloadv1alpha1.AMFDeploymentList{}) + if err := schemeBuilder.AddToScheme(mgr.GetScheme()); err != nil { + setupLog.Error(err, "Not able to register AMFDeployment kind") + os.Exit(1) + } + + if err = (&controllers.AMFDeploymentReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "AMFDeployment") + os.Exit(1) + } + if err = (&controllers.UPFDeploymentReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), From bc2ee44b1456d79f1c9ba437e0c6992bd6b4bae1 Mon Sep 17 00:00:00 2001 From: vireshnavalli Date: Fri, 26 May 2023 09:27:42 +0000 Subject: [PATCH 3/9] multus issue --- free5gc-operator/config/rbac/role.yaml | 52 +++++++++++++++++++ .../controllers/amfdeployment_controller.go | 9 ++-- free5gc-operator/test/nad/amf-n2-nad.yaml | 2 +- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/free5gc-operator/config/rbac/role.yaml b/free5gc-operator/config/rbac/role.yaml index 1e286b0..501f2a1 100644 --- a/free5gc-operator/config/rbac/role.yaml +++ b/free5gc-operator/config/rbac/role.yaml @@ -57,6 +57,58 @@ rules: - get - list - watch +- apiGroups: + - workload.nephio.org + resources: + - AMFdeployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - workload.nephio.org + resources: + - AMFdeployments/finalizers + verbs: + - update +- apiGroups: + - workload.nephio.org + resources: + - AMFdeployments/status + verbs: + - get + - patch + - update +- apiGroups: + - workload.nephio.org + resources: + - amfdeployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - workload.nephio.org + resources: + - amfdeployments/finalizers + verbs: + - update +- apiGroups: + - workload.nephio.org + resources: + - amfdeployments/status + verbs: + - get + - patch + - update - apiGroups: - workload.nephio.org resources: diff --git a/free5gc-operator/controllers/amfdeployment_controller.go b/free5gc-operator/controllers/amfdeployment_controller.go index 976a980..963d508 100644 --- a/free5gc-operator/controllers/amfdeployment_controller.go +++ b/free5gc-operator/controllers/amfdeployment_controller.go @@ -135,7 +135,7 @@ func constructAMFNadName(templateName string, suffix string) string { func getAMFNad(templateName string, spec *workloadv1alpha1.AMFDeploymentSpec) string { var ret string - n2CfgSlice := getIntConfigSlice(spec.Interfaces, "N2") + n2CfgSlice := getIntConfigSlice(spec.Interfaces, "n2") // n11CfgSlice := getIntConfigSlice(spec.Interfaces, "N11") // n4CfgSlice := getIntConfigSlice(spec.Interfaces, "N4") // n9CfgSlice := getIntConfigSlice(spec.Interfaces, "N9") @@ -261,9 +261,10 @@ func free5gcAMFDeployment(log logr.Logger, amfDeploy *workloadv1alpha1.AMFDeploy ContainerPort: 8805, }, }, - // Command: []string{ - // "/free5gc/config//wrapper.sh", - // }, + + Command: []string{"./amf"}, + Args: []string{"-c", "../config/amfcfg.yaml"}, + VolumeMounts: []apiv1.VolumeMount{ { MountPath: "/free5gc/config/", diff --git a/free5gc-operator/test/nad/amf-n2-nad.yaml b/free5gc-operator/test/nad/amf-n2-nad.yaml index 156839c..f75c051 100644 --- a/free5gc-operator/test/nad/amf-n2-nad.yaml +++ b/free5gc-operator/test/nad/amf-n2-nad.yaml @@ -24,7 +24,7 @@ spec: { "type": "macvlan", "capabilities": { "ips": true }, - "master": "ens5", + "master": "eth0", "mode": "bridge", "ipam": { "type": "static", From d780f830705c63d07ff0f9f872d81ada199efdda Mon Sep 17 00:00:00 2001 From: vireshnavalli Date: Fri, 26 May 2023 11:07:29 +0000 Subject: [PATCH 4/9] added command segment --- free5gc-operator/controllers/amfdeployment_controller.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/free5gc-operator/controllers/amfdeployment_controller.go b/free5gc-operator/controllers/amfdeployment_controller.go index 963d508..d636bc5 100644 --- a/free5gc-operator/controllers/amfdeployment_controller.go +++ b/free5gc-operator/controllers/amfdeployment_controller.go @@ -262,9 +262,9 @@ func free5gcAMFDeployment(log logr.Logger, amfDeploy *workloadv1alpha1.AMFDeploy }, }, - Command: []string{"./amf"}, - Args: []string{"-c", "../config/amfcfg.yaml"}, - + Command: []string{ + "sh", "-c", "set -x; do while [ $(curl --insecure --connect-timeout 1 -s -o /dev/null -w \"%{http_code}\" \"http://nrf-nnrf:8000\") -ne 200 ]; do echo waiting for dependencies; sleep 1; done;", + }, VolumeMounts: []apiv1.VolumeMount{ { MountPath: "/free5gc/config/", From 5e4721b10843857f08735e39be733b2eabf2004b Mon Sep 17 00:00:00 2001 From: vireshnavalli Date: Fri, 26 May 2023 15:12:53 +0000 Subject: [PATCH 5/9] AMF controller full code --- .../controllers/amfdeployment_controller.go | 55 ++++- .../controllers/free5gc_amfcfg.go | 230 ++++++++---------- 2 files changed, 160 insertions(+), 125 deletions(-) diff --git a/free5gc-operator/controllers/amfdeployment_controller.go b/free5gc-operator/controllers/amfdeployment_controller.go index d636bc5..ea14623 100644 --- a/free5gc-operator/controllers/amfdeployment_controller.go +++ b/free5gc-operator/controllers/amfdeployment_controller.go @@ -34,6 +34,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -262,9 +263,9 @@ func free5gcAMFDeployment(log logr.Logger, amfDeploy *workloadv1alpha1.AMFDeploy }, }, - Command: []string{ - "sh", "-c", "set -x; do while [ $(curl --insecure --connect-timeout 1 -s -o /dev/null -w \"%{http_code}\" \"http://nrf-nnrf:8000\") -ne 200 ]; do echo waiting for dependencies; sleep 1; done;", - }, + Command: []string{"./amf"}, + Args: []string{"-c", "../config/amfcfg.yaml"}, + VolumeMounts: []apiv1.VolumeMount{ { MountPath: "/free5gc/config/", @@ -312,6 +313,34 @@ func free5gcAMFDeployment(log logr.Logger, amfDeploy *workloadv1alpha1.AMFDeploy return deployment, nil } +func free5gcAMFCreateService(amfDeploy *workloadv1alpha1.AMFDeployment) *apiv1.Service { + namespace := amfDeploy.ObjectMeta.Namespace + instanceName := amfDeploy.ObjectMeta.Name + + labels := map[string]string{ + "name": instanceName, + } + + service := &apiv1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: instanceName + "-amf-svc", + Namespace: namespace, + }, + Spec: apiv1.ServiceSpec{ + Selector: labels, + Ports: []apiv1.ServicePort{{ + Name: "http", + Protocol: apiv1.ProtocolTCP, + Port: 80, + TargetPort: intstr.FromInt(80), + }}, + Type: apiv1.ServiceTypeClusterIP, + }, + } + + return service +} + func free5gcAMFCreateConfigmap(logger logr.Logger, amfDeploy *workloadv1alpha1.AMFDeployment) (*apiv1.ConfigMap, error) { namespace := amfDeploy.ObjectMeta.Namespace instanceName := amfDeploy.ObjectMeta.Name @@ -506,6 +535,13 @@ func (r *AMFDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Reques cmFound = true } + svcFound := false + svcName := amfDeploy.ObjectMeta.Name + "-amf-svc" + currSvc := &apiv1.Service{} + if err := r.Client.Get(ctx, types.NamespacedName{Name: svcName, Namespace: namespace}, currSvc); err == nil { + svcFound = true + } + dmFound := false dmName := amfDeploy.ObjectMeta.Name currDeployment := &appsv1.Deployment{} @@ -544,6 +580,19 @@ func (r *AMFDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Reques } } + if !svcFound { + svc := free5gcAMFCreateService(amfDeploy) + log.Info("Creating AMFDeployment service", "AMFDeployment.namespace", namespace, "Service.name", svc.ObjectMeta.Name) + // Set the controller reference, specifying that AMFDeployment controling underlying deployment + if err := ctrl.SetControllerReference(amfDeploy, svc, r.Scheme); err != nil { + log.Error(err, "Got error while setting Owner reference on AMF service.", "AMFDeployment.namespace", namespace) + } + if err := r.Client.Create(ctx, svc); err != nil { + log.Error(err, fmt.Sprintf("Error: failed to create an AMF service %s\n", err.Error())) + return reconcile.Result{}, err + } + } + if deployment, err := free5gcAMFDeployment(log, amfDeploy); err != nil { log.Error(err, fmt.Sprintf("Error: failed to generate deployment %s\n", err.Error())) return reconcile.Result{}, err diff --git a/free5gc-operator/controllers/free5gc_amfcfg.go b/free5gc-operator/controllers/free5gc_amfcfg.go index 2b29474..bb5e710 100644 --- a/free5gc-operator/controllers/free5gc_amfcfg.go +++ b/free5gc-operator/controllers/free5gc_amfcfg.go @@ -1,136 +1,122 @@ -/* -Free5GC Config -N2: NF Deployment IF - -sbi: - - scheme: http - registerIPv4: amf-namf - bindingIPv4: 0.0.0.0 - port: 80 - tls: - key: config/TLS/smf.key - pem: config/TLS/smf.pem - - nrfUri: http://nrf-nnrf:8000 -*/ package controllers var AMFCfgTemplate string = ` -info: - version: 1.0.0 - description: AMF configuration + info: + + version: 1.0.3 + description: AMF initial local configuration configuration: - ReportCaller: false - debugLevel: info - serviceNameList: - - namf-comm - - namf-evts + - namf-oam - - namf-mt - - namf-loc - ngapIpList: - - addr: {{ .N2_IP }} - - sbi: + + ngapIpList: + - {{ .N2_IP }} + sbi: scheme: http - registerIPv4: amf-namf - bindingIPv4: 0.0.0.0 + + registerIPv4: release-name-free5gc-amf-service # IP used to register to NRF + bindingIPv4: 0.0.0.0 # IP used to bind the service port: 80 tls: key: config/TLS/amf.key pem: config/TLS/amf.pem - - nrfUri: http://nrf-nnrf:8000 - servedGuamiList: - - plmnId: - mcc: 208 - mnc: 93 - amfId: cafe00 - supportTaiList: - - plmnId: - mcc: 208 - mnc: 93 - tac: 1 - plmnSupportList: - - plmnId: - mcc: 208 - mnc: 93 + nrfUri: http://nrf-nnrf:8000 + amfName: AMF + serviceNameList: + - namf-comm + - namf-evts + - namf-mt + - namf-loc + - namf-oam + servedGuamiList: + - plmnId: + mcc: 208 + mnc: 93 + amfId: cafe00 + supportTaiList: + - plmnId: + mcc: 208 + mnc: 93 + tac: 1 + plmnSupportList: + - plmnId: + mcc: 208 + mnc: 93 snssaiList: - - sst: 1 - sd: 010203 - - sst: 1 - sd: 112233 - supportDnnList: - - internet - security: - integrityOrder: - - NIA2 - cipheringOrder: - - NEA0 - networkName: - full: free5GC - short: free - locality: area1 # Name of the location where a set of AMF, SMF and UPFs are located - networkFeatureSupport5GS: # 5gs Network Feature Support IE, refer to TS 24.501 - enable: true # append this IE in Registration accept or not - length: 1 # IE content length (uinteger, range: 1~3) - imsVoPS: 0 # IMS voice over PS session indicator (uinteger, range: 0~1) - emc: 0 # Emergency service support indicator for 3GPP access (uinteger, range: 0~3) - emf: 0 # Emergency service fallback indicator for 3GPP access (uinteger, range: 0~3) - iwkN26: 0 # Interworking without N26 interface indicator (uinteger, range: 0~1) - mpsi: 0 # MPS indicator (uinteger, range: 0~1) - emcN3: 0 # Emergency service support indicator for Non-3GPP access (uinteger, range: 0~1) - mcsi: 0 # MCS indicator (uinteger, range: 0~1) - t3502Value: 720 - t3512Value: 3600 - non3gppDeregistrationTimerValue: 3240 - # retransmission timer for paging message - t3513: - enable: true # true or false - expireTime: 6s # default is 6 seconds - maxRetryTimes: 4 # the max number of retransmission - # retransmission timer for NAS Registration Accept message - t3522: - enable: true # true or false - expireTime: 6s # default is 6 seconds - maxRetryTimes: 4 # the max number of retransmission - # retransmission timer for NAS Registration Accept message - t3550: - enable: true # true or false - expireTime: 6s # default is 6 seconds - maxRetryTimes: 4 # the max number of retransmission - # retransmission timer for NAS Authentication Request/Security Mode Command message - t3560: - enable: true # true or false - expireTime: 6s # default is 6 seconds - maxRetryTimes: 4 # the max number of retransmission - # retransmission timer for NAS Notification message - t3565: - enable: true # true or false - expireTime: 6s # default is 6 seconds - maxRetryTimes: 4 # the max number of retransmission - t3570: - enable: true # true or false - expireTime: 6s # default is 6 seconds - maxRetryTimes: 4 # the max number of retransmission + - sst: 1 + sd: 010203 + - sst: 1 + sd: 112233 + supportDnnList: + - internet + security: + integrityOrder: + - NIA2 + cipheringOrder: + - NEA0 + networkName: + full: free5GC + short: free + locality: area1 # Name of the location where a set of AMF, SMF and UPFs are located + networkFeatureSupport5GS: # 5gs Network Feature Support IE, refer to TS 24.501 + enable: true # append this IE in Registration accept or not + length: 1 # IE content length (uinteger, range: 1~3) + imsVoPS: 0 # IMS voice over PS session indicator (uinteger, range: 0~1) + emc: 0 # Emergency service support indicator for 3GPP access (uinteger, range: 0~3) + emf: 0 # Emergency service fallback indicator for 3GPP access (uinteger, range: 0~3) + iwkN26: 0 # Interworking without N26 interface indicator (uinteger, range: 0~1) + mpsi: 0 # MPS indicator (uinteger, range: 0~1) + emcN3: 0 # Emergency service support indicator for Non-3GPP access (uinteger, range: 0~1) + mcsi: 0 # MCS indicator (uinteger, range: 0~1) + t3502Value: 720 + t3512Value: 3600 + non3gppDeregistrationTimerValue: 3240 + # retransmission timer for paging message + t3513: + enable: true # true or false + expireTime: 6s # default is 6 seconds + maxRetryTimes: 4 # the max number of retransmission + # retransmission timer for NAS Registration Accept message + t3522: + enable: true # true or false + expireTime: 6s # default is 6 seconds + maxRetryTimes: 4 # the max number of retransmission + # retransmission timer for NAS Registration Accept message + t3550: + enable: true # true or false + expireTime: 6s # default is 6 seconds + maxRetryTimes: 4 # the max number of retransmission + # retransmission timer for NAS Authentication Request/Security Mode Command message + t3560: + enable: true # true or false + expireTime: 6s # default is 6 seconds + maxRetryTimes: 4 # the max number of retransmission + # retransmission timer for NAS Notification message + t3565: + enable: true # true or false + expireTime: 6s # default is 6 seconds + maxRetryTimes: 4 # the max number of retransmission + t3570: + enable: true # true or false + expireTime: 6s # default is 6 seconds + maxRetryTimes: 4 # the max number of retransmission - logger: - AMF: - ReportCaller: false - debugLevel: info - Aper: - ReportCaller: false - debugLevel: info - FSM: - ReportCaller: false - debugLevel: info - NAS: - ReportCaller: false - debugLevel: info - NGAP: - ReportCaller: false - debugLevel: info -` +logger: + AMF: + ReportCaller: false + debugLevel: info + Aper: + ReportCaller: false + debugLevel: info + FSM: + ReportCaller: false + debugLevel: info + NAS: + ReportCaller: false + debugLevel: info + NGAP: + ReportCaller: false + debugLevel: info + ` From 84b8b4fc2abff6e9cecbc143aeffa541c46e3efe Mon Sep 17 00:00:00 2001 From: vireshnavalli Date: Mon, 29 May 2023 19:24:01 +0000 Subject: [PATCH 6/9] conflict files modified --- free5gc-operator/config/rbac/role.yaml | 3 +++ free5gc-operator/main.go | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/free5gc-operator/config/rbac/role.yaml b/free5gc-operator/config/rbac/role.yaml index c99e595..aa0f780 100644 --- a/free5gc-operator/config/rbac/role.yaml +++ b/free5gc-operator/config/rbac/role.yaml @@ -60,7 +60,10 @@ rules: - apiGroups: - workload.nephio.org resources: +<<<<<<< HEAD - smfdeployments +======= +>>>>>>> 0a7978e (conflict files modified) - amfdeployments verbs: - create diff --git a/free5gc-operator/main.go b/free5gc-operator/main.go index 687263e..76a7a28 100644 --- a/free5gc-operator/main.go +++ b/free5gc-operator/main.go @@ -108,8 +108,11 @@ func main() { setupLog.Error(err, "Not able to register AMFDeployment kind") os.Exit(1) } +<<<<<<< HEAD os.Exit(1) } +======= +>>>>>>> 0a7978e (conflict files modified) if err = (&controllers.UPFDeploymentReconciler{ Client: mgr.GetClient(), @@ -119,6 +122,7 @@ func main() { os.Exit(1) } +<<<<<<< HEAD if err = (&controllers.SMFDeploymentReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), @@ -128,10 +132,18 @@ func main() { } if err = (&controllers.AMFDeploymentReconciler{ +======= + if err = (&controllers.AMFDeploymentReconciler{ +>>>>>>> 0a7978e (conflict files modified) Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "AMFDeployment") +<<<<<<< HEAD +======= + os.Exit(1) + } +>>>>>>> 0a7978e (conflict files modified) //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { From 8733624d701c317ccd11b700f88bf1567d7f2218 Mon Sep 17 00:00:00 2001 From: vireshnavalli Date: Mon, 29 May 2023 20:46:20 +0000 Subject: [PATCH 7/9] updated --- .../controllers/amfdeployment_controller_test.go | 12 ++++++------ free5gc-operator/main.go | 16 +++------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/free5gc-operator/controllers/amfdeployment_controller_test.go b/free5gc-operator/controllers/amfdeployment_controller_test.go index abc752a..de01da1 100644 --- a/free5gc-operator/controllers/amfdeployment_controller_test.go +++ b/free5gc-operator/controllers/amfdeployment_controller_test.go @@ -35,7 +35,7 @@ func newAMFNxInterface(name string) workloadv1alpha1.InterfaceConfig { case "n2": gw := "10.10.10.1" n2int := workloadv1alpha1.InterfaceConfig{ - Name: "N2", + Name: "n2", IPv4: &workloadv1alpha1.IPv4{ Address: "10.10.10.10/24", Gateway: &gw, @@ -50,7 +50,7 @@ func newAmfDeployInstance(name string) *workloadv1alpha1.AMFDeployment { interfaces := []workloadv1alpha1.InterfaceConfig{} n2int := newAMFNxInterface("n2") interfaces = append(interfaces, n2int) - dnnName := "apn-test" + //dnnName := "apn-test" amfDeployInstance := &workloadv1alpha1.AMFDeployment{ ObjectMeta: metav1.ObjectMeta{ @@ -72,7 +72,7 @@ func newAmfDeployInstance(name string) *workloadv1alpha1.AMFDeployment { { Name: "vpc-internet", Interfaces: []string{ - "N2", + "n2", }, DataNetworks: []workloadv1alpha1.DataNetwork{ { @@ -140,7 +140,7 @@ func TestGetAMFNad(t *testing.T) { want := `[ {"name": "test-amf-deployment-n2", - "interface": "N2", + "interface": "n2", "ips": ["10.10.10.10/24"], "gateways": ["10.10.10.1"] } @@ -159,7 +159,7 @@ func TestFree5gcAMFCreateConfigmap(t *testing.T) { t.Errorf("free5gcAMFCreateConfigmap() returned unexpected error %v", err) } - n2IP, _ := getIPv4(amfDeploymentInstance.Spec.Interfaces, "N2") + n2IP, _ := getIPv4(amfDeploymentInstance.Spec.Interfaces, "n2") amfcfgStruct := AMFcfgStruct{} amfcfgStruct.N2_IP = n2IP @@ -439,7 +439,7 @@ func TestFree5gcAMFDeployment(t *testing.T) { Annotations: map[string]string{ "k8s.v1.cni.cncf.io/networks": `[ {"name": "test-amf-deployment-n2", - "interface": "N2", + "interface": "n2", "ips": ["10.10.10.10/24"], "gateways": ["10.10.10.1"] } diff --git a/free5gc-operator/main.go b/free5gc-operator/main.go index 76a7a28..cda54d2 100644 --- a/free5gc-operator/main.go +++ b/free5gc-operator/main.go @@ -102,17 +102,14 @@ func main() { schemeBuilder.Register(&workloadv1alpha1.SMFDeployment{}, &workloadv1alpha1.SMFDeploymentList{}) if err := schemeBuilder.AddToScheme(mgr.GetScheme()); err != nil { setupLog.Error(err, "Not able to register SMFDeployment kind") + os.Exit(1) + } - schemeBuilder.Register(&workloadv1alpha1.AMFDeployment{}, &workloadv1alpha1.AMFDeploymentList{}) + schemeBuilder.Register(&workloadv1alpha1.AMFDeployment{}, &workloadv1alpha1.AMFDeploymentList{}) if err := schemeBuilder.AddToScheme(mgr.GetScheme()); err != nil { setupLog.Error(err, "Not able to register AMFDeployment kind") os.Exit(1) } -<<<<<<< HEAD - os.Exit(1) - } -======= ->>>>>>> 0a7978e (conflict files modified) if err = (&controllers.UPFDeploymentReconciler{ Client: mgr.GetClient(), @@ -122,7 +119,6 @@ func main() { os.Exit(1) } -<<<<<<< HEAD if err = (&controllers.SMFDeploymentReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), @@ -131,19 +127,13 @@ func main() { os.Exit(1) } - if err = (&controllers.AMFDeploymentReconciler{ -======= if err = (&controllers.AMFDeploymentReconciler{ ->>>>>>> 0a7978e (conflict files modified) Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "AMFDeployment") -<<<<<<< HEAD -======= os.Exit(1) } ->>>>>>> 0a7978e (conflict files modified) //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { From a6b9206a98a15cafc3d291ea8732e1580e8338dc Mon Sep 17 00:00:00 2001 From: vireshnavalli Date: Mon, 29 May 2023 20:56:55 +0000 Subject: [PATCH 8/9] updated test controller --- .../amfdeployment_controller_test.go | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/free5gc-operator/controllers/amfdeployment_controller_test.go b/free5gc-operator/controllers/amfdeployment_controller_test.go index de01da1..aec7eae 100644 --- a/free5gc-operator/controllers/amfdeployment_controller_test.go +++ b/free5gc-operator/controllers/amfdeployment_controller_test.go @@ -68,26 +68,26 @@ func newAmfDeployInstance(name string) *workloadv1alpha1.AMFDeployment { MaxNFConnections: 2000, }, Interfaces: interfaces, - NetworkInstances: []workloadv1alpha1.NetworkInstance{ - { - Name: "vpc-internet", - Interfaces: []string{ - "n2", - }, - DataNetworks: []workloadv1alpha1.DataNetwork{ - { - Name: &dnnName, - Pool: []workloadv1alpha1.Pool{ - { - Prefix: "100.100.0.0/16", - }, - }, - }, - }, - BGP: nil, - Peers: []workloadv1alpha1.PeerConfig{}, - }, - }, +// NetworkInstances: []workloadv1alpha1.NetworkInstance{ +// { +// Name: "vpc-internet", +// Interfaces: []string{ +// "n2", +// }, +// DataNetworks: []workloadv1alpha1.DataNetwork{ +// { +// Name: &dnnName, +// Pool: []workloadv1alpha1.Pool{ +// { +// Prefix: "100.100.0.0/16", +// }, +// }, +// }, +// }, +// BGP: nil, +// Peers: []workloadv1alpha1.PeerConfig{}, +// }, +// }, }, }, } From 6c5bdf7997b7ff83b66999da52e7c96b22283528 Mon Sep 17 00:00:00 2001 From: vireshnavalli Date: Mon, 29 May 2023 21:07:10 +0000 Subject: [PATCH 9/9] updated test file --- .../controllers/amfdeployment_controller_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/free5gc-operator/controllers/amfdeployment_controller_test.go b/free5gc-operator/controllers/amfdeployment_controller_test.go index aec7eae..785a799 100644 --- a/free5gc-operator/controllers/amfdeployment_controller_test.go +++ b/free5gc-operator/controllers/amfdeployment_controller_test.go @@ -468,9 +468,10 @@ func TestFree5gcAMFDeployment(t *testing.T) { ContainerPort: 8805, }, }, - // Command: []string{ - // "/free5gc/config//wrapper.sh", - // }, + + Command: []string{"./amf"}, + Args: []string{"-c", "../config/amfcfg.yaml"}, + VolumeMounts: []apiv1.VolumeMount{ { MountPath: "/free5gc/config/",