From 77a0ef4a38cd875017b93866adbc8320ceef103e Mon Sep 17 00:00:00 2001 From: Andre Fredette Date: Tue, 4 Feb 2025 14:04:13 -0500 Subject: [PATCH] Support namespace-scopted BpfNsApplication CRD Signed-off-by: Andre Fredette --- PROJECT | 7 + TODO.md | 123 +---- apis/v1alpha1/bpfNsApplicationState_types.go | 135 +++++ apis/v1alpha1/bpfNsApplication_types.go | 1 + apis/v1alpha1/tcxNsProgram_types.go | 32 ++ apis/v1alpha1/xdpNsProgram_types.go | 32 ++ apis/v1alpha1/zz_generated.deepcopy.go | 189 +++++++ apis/v1alpha1/zz_generated.register.go | 2 + ...c.authorization.k8s.io_v1_clusterrole.yaml | 32 ++ ...bpfman-operator.clusterserviceversion.yaml | 119 ++-- .../bpfman.io_bpfnsapplications.yaml | 61 +++ .../bpfman.io_bpfnsapplicationstates.yaml | 425 ++++++++++++++ cmd/bpfman-agent/main.go | 7 + cmd/bpfman-operator/main.go | 16 + config/bpfman-deployment/config.yaml | 3 + .../bases/bpfman.io_bpfnsapplications.yaml | 61 +++ .../bpfman.io_bpfnsapplicationstates.yaml | 419 ++++++++++++++ config/crd/kustomization.yaml | 1 + config/rbac/bpfman-agent/role.yaml | 32 ++ config/rbac/bpfman-operator/role.yaml | 16 + .../bpfman.io_v1alpha1_bpfnsapplication.yaml | 88 +-- .../app-agent/cl-application-program.go | 13 +- controllers/app-agent/cl-tcx-program.go | 3 +- controllers/app-agent/cl-xdp-program.go | 3 +- controllers/app-agent/common.go | 8 - .../app-agent/ns-application-program.go | 518 ++++++++++++++++++ .../app-agent/ns-application-program_test.go | 257 +++++++++ controllers/app-agent/ns-tcx-program.go | 303 ++++++++++ controllers/app-agent/ns-xdp-program.go | 302 ++++++++++ controllers/app-agent/test_common.go | 97 ++++ ...test.go => cl-application-program_test.go} | 0 ...programs.go => cl-application-programs.go} | 0 controllers/app-operator/common.go | 4 +- controllers/app-operator/common_namespace.go | 84 +++ .../ns-application-program_test.go | 279 ++++++++++ .../app-operator/ns-application-programs.go | 135 +++++ hack/namespace_scoped.yaml | 6 +- .../apis/v1alpha1/bpfnsapplicationstate.go | 99 ++++ .../apis/v1alpha1/expansion_generated.go | 8 + .../typed/apis/v1alpha1/apis_client.go | 5 + .../apis/v1alpha1/bpfnsapplicationstate.go | 195 +++++++ .../apis/v1alpha1/fake/fake_apis_client.go | 4 + .../fake/fake_bpfnsapplicationstate.go | 141 +++++ .../apis/v1alpha1/generated_expansion.go | 2 + .../apis/v1alpha1/bpfnsapplicationstate.go | 90 +++ .../apis/v1alpha1/interface.go | 7 + pkg/client/externalversions/generic.go | 2 + 47 files changed, 4136 insertions(+), 230 deletions(-) create mode 100644 apis/v1alpha1/bpfNsApplicationState_types.go create mode 100644 bundle/manifests/bpfman.io_bpfnsapplicationstates.yaml create mode 100644 config/crd/bases/bpfman.io_bpfnsapplicationstates.yaml create mode 100644 controllers/app-agent/ns-application-program.go create mode 100644 controllers/app-agent/ns-application-program_test.go create mode 100644 controllers/app-agent/ns-tcx-program.go create mode 100644 controllers/app-agent/ns-xdp-program.go create mode 100644 controllers/app-agent/test_common.go rename controllers/app-operator/{application-cl-program_test.go => cl-application-program_test.go} (100%) rename controllers/app-operator/{application-cl-programs.go => cl-application-programs.go} (100%) create mode 100644 controllers/app-operator/common_namespace.go create mode 100644 controllers/app-operator/ns-application-program_test.go create mode 100644 controllers/app-operator/ns-application-programs.go create mode 100644 pkg/client/apis/v1alpha1/bpfnsapplicationstate.go create mode 100644 pkg/client/clientset/typed/apis/v1alpha1/bpfnsapplicationstate.go create mode 100644 pkg/client/clientset/typed/apis/v1alpha1/fake/fake_bpfnsapplicationstate.go create mode 100644 pkg/client/externalversions/apis/v1alpha1/bpfnsapplicationstate.go diff --git a/PROJECT b/PROJECT index edcdde0f9..dc9760099 100644 --- a/PROJECT +++ b/PROJECT @@ -136,4 +136,11 @@ resources: kind: BpfApplicationState path: github.com/bpfman/bpfman-operator/apis/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + controller: true + domain: bpfman.io + kind: BpfNsApplicationState + path: github.com/bpfman/bpfman-operator/apis/v1alpha1 + version: v1alpha1 version: "3" diff --git a/TODO.md b/TODO.md index afb57a35b..36751caf4 100644 --- a/TODO.md +++ b/TODO.md @@ -1,121 +1,24 @@ # Intro -The code has support for XDP, TCX, and Fentry programs in a BpfApplication. +The code has support for: +- XDP, TCX, and Fentry programs in the cluster-scoped BpfApplication, and +- XDP and TCX in the namespace-scoped BpfNsApplication It's written so that Dave's load/attach split code should drop in pretty easily, but it's not using it yet. I'm simulating the attachments by reloading the code for each attachment (like we do today). -# Observation/Question -Fentry and Fexit programs break the mold. - -Fentry and Fexit programs need to be loaded separately for each attach point, so -the user must specify the BPF function name and attach point together for each -attachment. The user can then attach or detach that program later, but if the -user wants to attach the same Fentry/Fexit program to a different attach point, -the program must be loaded again with the new attach point. - -For other program types, the user can load a program, and then attach or detach -the program to/from multiple attach points at any time after it has been loaded. - -Some differences that result from these requirements: -- Each Fentry/Fexit attach point results in a unique bpf program ID (even if - they all use the same bpf function) -- For other types, a given bpf program can have one bpf program ID (assigned - when it's loaded), plus multiple attach IDs (assigened when it is attached) -- We don't need an attach ID for Fentry/Fexit programs. - -We need to do one of the following: -- Not support the user modifying Fentry/Fexit attach points after the initial - BpfApplication load. -- Load the program if they add an attach point (which would result in an - independent set of global data), and unload the program if they remove an - attachment. - -Yaml options: - -**Option 1:** The current design uses a map indexed by the bpffunction name, so -we can only list a given bpffunction name once followed by a list of attach -points as shown below. This represents Fentry/Fexit programs the same way as -others, but they would need to behave differently as outlined above. - -```yaml -programs: - tcx_stats: - type: TCX - tcx: - attach_points: - - interfaceselector: - primarynodeinterface: true - priority: 500 - direction: ingress - - interfaceselector: - interfaces: - - eth1 - priority: 100 - direction: egress - test_fentry: - type: Fentry - fentry: - attach_points: - - function_name: do_unlinkat - attach: true - - function_name: tcp_connect - attach: false -``` - -**Options 2:** Use a slice, and allow the same Fentry/Fexit functions to be -included multiple times. The is more like the bpfman api, but potentially more -cumbersome for Fentry/Fexit programs. - -```yaml - programs: - - type: TCX - tcx: - bpffunctionname: tcx_stats - attach_points: - - interfaceselector: - primarynodeinterface: true - priority: 500 - direction: ingress - - interfaceselector: - interfaces: - - eth0 - priority: 100 - direction: egress - containers: - namespace: bpfman - pods: - matchLabels: - name: bpfman-daemon - containernames: - - bpfman - - bpfman-agent - - type: Fentry - fentry: - bpffunctionname: tcx_stats - function_name: do_unlinkat - attach: true - - type: Fentry - fentry: - bpffunctionname: tcx_stats - function_name: tcp_connect - attach: true -``` - # New Code The new code is mainly in these directories: ## Updated & working APIs: -- apis/v1alpha1/fentryProgram_types.go -- apis/v1alpha1/xdpProgram_types.go -- apis/v1alpha1/tcxProgram_types.go -- apis/v1alpha1/bpfApplication_types.go - apis/v1alpha1/bpfApplicationState_types.go + - Program Types: TCX, XDP, Fentry +- apis/v1alpha1/bpfNsApplicationState_types.go + - Program Types: TCX, XDP -Note: the rest are partially updated. ## New Agent: @@ -133,23 +36,23 @@ we run the operator. - Unit tests for the agent and the operator - The following working samples: - - config/samples/bpfman.io_v1alpha1_bpfapplication.yaml (XDP & TCX) - - config/samples/bpfman.io_v1alpha1_fentry_bpfapplication.yaml (Fentry) + - config/samples/bpfman.io_v1alpha1_bpfapplication.yaml (XDP, TCX and Fentry) + - config/samples/bpfman.io_v1alpha1_fentry_bpfnsapplication.yaml (XDP and TCX) # TODO: (In no particular order.) -- Implement Fentry/Fexit solution. +- ~~Implement Fentry/Fexit solution.~~ - Integrate with the new bpfman code with load/attach split (of course) -- Create a bpf.o file with all the application types for both cluster and - namespace scoped BpfApplicaitons. +- ~~Create a bpf.o file with all the application types for both cluster and + namespace scoped BpfApplicaitons.~~ - Redo the status/condition values. I’m currently using the existing framework and some values for status/conditions, but I intend to create a new set of conditions/status values that make more sense for the new design. - Review all comments and logs. -- Maybe make more code common. -- Support the rest of the program types (including namespace-scoped CRDs). +- ~~Support namespace-scoped BPF Application CRD~~ +- Support the rest of the program types. - Delete old directories. - Lots more testing and code cleanup. - Lots of other stuff (I'm sure). diff --git a/apis/v1alpha1/bpfNsApplicationState_types.go b/apis/v1alpha1/bpfNsApplicationState_types.go new file mode 100644 index 000000000..0c32364f7 --- /dev/null +++ b/apis/v1alpha1/bpfNsApplicationState_types.go @@ -0,0 +1,135 @@ +/* +Copyright 2023 The bpfman Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1types "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// BpfNsApplicationProgramState defines the desired state of BpfNsApplication +// +union +// +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'XDP' ? has(self.xdp) : !has(self.xdp)",message="xdp configuration is required when type is XDP, and forbidden otherwise" +// // +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'TC' ? has(self.tc) : !has(self.tc)",message="tc configuration is required when type is TC, and forbidden otherwise" +// +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'TCX' ? has(self.tcx) : !has(self.tcx)",message="tcx configuration is required when type is TCX, and forbidden otherwise" +// // +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'Uprobe' ? has(self.uprobe) : !has(self.uprobe)",message="uprobe configuration is required when type is Uprobe, and forbidden otherwise" +// // +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'Uretprobe' ? has(self.uretprobe) : !has(self.uretprobe)",message="uretprobe configuration is required when type is Uretprobe, and forbidden otherwise" +type BpfNsApplicationProgramState struct { + BpfProgramStateCommon `json:",inline"` + // Type specifies the bpf program type + // +unionDiscriminator + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum:="XDP";"TC";"TCX";"Fentry";"Fexit";"Kprobe";"Kretprobe";"Uprobe";"Uretprobe";"Tracepoint" + Type EBPFProgType `json:"type,omitempty"` + + // xdp defines the desired state of the application's XdpPrograms. + // +unionMember + // +optional + XDP *XdpNsProgramInfoState `json:"xdp,omitempty"` + + // // tc defines the desired state of the application's TcPrograms. + // // +unionMember + // // +optional + // TC *TcProgramInfoState `json:"tc,omitempty"` + + // tcx defines the desired state of the application's TcxPrograms. + // +unionMember + // +optional + TCX *TcxNsProgramInfoState `json:"tcx,omitempty"` + + // // uprobe defines the desired state of the application's UprobePrograms. + // // +unionMember + // // +optional + // Uprobe *UprobeProgramInfoState `json:"uprobe,omitempty"` + + // // uretprobe defines the desired state of the application's UretprobePrograms. + // // +unionMember + // // +optional + // Uretprobe *UprobeProgramInfoState `json:"uretprobe,omitempty"` +} + +// BpfNsApplicationSpec defines the desired state of BpfNsApplication +type BpfNsApplicationStateSpec struct { + // Node is the name of the node for this BpfNsApplicationStateSpec. + Node string `json:"node"` + // The number of times the BpfNsApplicationState has been updated. Set to 1 + // when the object is created, then it is incremented prior to each update. + // This allows us to verify that the API server has the updated object prior + // to starting a new Reconcile operation. + UpdateCount int64 `json:"updatecount"` + // AppLoadStatus reflects the status of loading the bpf application on the + // given node. + AppLoadStatus BpfProgramConditionType `json:"apploadstatus"` + // Programs is a list of bpf programs contained in the parent application. + // It is a map from the bpf program name to BpfNsApplicationProgramState + // elements. + Programs []BpfNsApplicationProgramState `json:"programs,omitempty"` +} + +// +genclient +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced + +// BpfNsApplicationState contains the per-node state of a BpfNsApplication. +// +kubebuilder:printcolumn:name="Node",type=string,JSONPath=".spec.node" +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[0].reason` +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +type BpfNsApplicationState struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec BpfNsApplicationStateSpec `json:"spec,omitempty"` + Status BpfAppStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +// BpfNsApplicationStateList contains a list of BpfNsApplicationState objects +type BpfNsApplicationStateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []BpfNsApplicationState `json:"items"` +} + +func (an BpfNsApplicationState) GetName() string { + return an.Name +} + +func (an BpfNsApplicationState) GetUID() metav1types.UID { + return an.UID +} + +func (an BpfNsApplicationState) GetAnnotations() map[string]string { + return an.Annotations +} + +func (an BpfNsApplicationState) GetLabels() map[string]string { + return an.Labels +} + +func (an BpfNsApplicationState) GetStatus() *BpfAppStatus { + return &an.Status +} + +func (an BpfNsApplicationState) GetClientObject() client.Object { + return &an +} + +func (anl BpfNsApplicationStateList) GetItems() []BpfNsApplicationState { + return anl.Items +} diff --git a/apis/v1alpha1/bpfNsApplication_types.go b/apis/v1alpha1/bpfNsApplication_types.go index 0d01a4bb1..e32f1acb0 100644 --- a/apis/v1alpha1/bpfNsApplication_types.go +++ b/apis/v1alpha1/bpfNsApplication_types.go @@ -28,6 +28,7 @@ import ( // +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'Uprobe' ? has(self.uprobe) : !has(self.uprobe)",message="uprobe configuration is required when type is Uprobe, and forbidden otherwise" // +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'Uretprobe' ? has(self.uretprobe) : !has(self.uretprobe)",message="uretprobe configuration is required when type is Uretprobe, and forbidden otherwise" type BpfNsApplicationProgram struct { + BpfProgramCommon `json:",inline"` // Type specifies the bpf program type // +unionDiscriminator // +kubebuilder:validation:Required diff --git a/apis/v1alpha1/tcxNsProgram_types.go b/apis/v1alpha1/tcxNsProgram_types.go index 5e721373b..f2d89ea2c 100644 --- a/apis/v1alpha1/tcxNsProgram_types.go +++ b/apis/v1alpha1/tcxNsProgram_types.go @@ -88,3 +88,35 @@ type TcxNsProgramList struct { metav1.ListMeta `json:"metadata,omitempty"` Items []TcxNsProgram `json:"items"` } + +type TcxNsProgramInfoState struct { + // The list of points to which the program should be attached. + // TcxAttachInfoState is similar to TcxAttachInfo, but the interface and + // container selectors are expanded, and we have one instance of + // TcxAttachInfoState for each unique attach point. The list is optional and + // may be udated after the bpf program has been loaded. + // +optional + AttachPoints []TcxAttachInfoState `json:"attach_points"` +} + +type TcxNsAttachInfoState struct { + AttachInfoCommon `json:",inline"` + + // Interface name to attach the tcx program to. + IfName string `json:"ifname"` + + // Optional container pid to attach the tcx program in. + ContainerPid uint32 `json:"containerpid"` + + // Direction specifies the direction of traffic the tcx program should + // attach to for a given network device. + // +kubebuilder:validation:Enum=ingress;egress + Direction string `json:"direction"` + + // Priority specifies the priority of the tcx program in relation to + // other programs of the same type with the same attach point. It is a value + // from 0 to 1000 where lower values have higher precedence. + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=1000 + Priority int32 `json:"priority"` +} diff --git a/apis/v1alpha1/xdpNsProgram_types.go b/apis/v1alpha1/xdpNsProgram_types.go index f1fed22bd..228c45273 100644 --- a/apis/v1alpha1/xdpNsProgram_types.go +++ b/apis/v1alpha1/xdpNsProgram_types.go @@ -88,3 +88,35 @@ type XdpNsProgramList struct { metav1.ListMeta `json:"metadata,omitempty"` Items []XdpNsProgram `json:"items"` } + +type XdpNsProgramInfoState struct { + // The list of points to which the program should be attached. + // XdpAttachInfoState is similar to XdpAttachInfo, but the interface and + // container selectors are expanded, and we have one instance of + // XdpAttachInfoState for each unique attach point. The list is optional and + // may be udated after the bpf program has been loaded. + // +optional + AttachPoints []XdpAttachInfoState `json:"attach_points"` +} + +type XdpNsAttachInfoState struct { + AttachInfoCommon `json:",inline"` + + // Interface name to attach the xdp program to. + IfName string `json:"ifname"` + + // Optional container pid to attach the xdp program in. + ContainerPid uint32 `json:"containerpid"` + + // Priority specifies the priority of the xdp program in relation to + // other programs of the same type with the same attach point. It is a value + // from 0 to 1000 where lower values have higher precedence. + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=1000 + Priority int32 `json:"priority"` + + // ProceedOn allows the user to call other xdp programs in chain on this exit code. + // Multiple values are supported by repeating the parameter. + // +kubebuilder:validation:MaxItems=6 + ProceedOn []XdpProceedOnValue `json:"proceedon"` +} diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 414148814..d348022a7 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -432,6 +432,7 @@ func (in *BpfNsApplicationList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BpfNsApplicationProgram) DeepCopyInto(out *BpfNsApplicationProgram) { *out = *in + in.BpfProgramCommon.DeepCopyInto(&out.BpfProgramCommon) if in.XDP != nil { in, out := &in.XDP, &out.XDP *out = new(XdpNsProgramInfo) @@ -469,6 +470,32 @@ func (in *BpfNsApplicationProgram) DeepCopy() *BpfNsApplicationProgram { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BpfNsApplicationProgramState) DeepCopyInto(out *BpfNsApplicationProgramState) { + *out = *in + in.BpfProgramStateCommon.DeepCopyInto(&out.BpfProgramStateCommon) + if in.XDP != nil { + in, out := &in.XDP, &out.XDP + *out = new(XdpNsProgramInfoState) + (*in).DeepCopyInto(*out) + } + if in.TCX != nil { + in, out := &in.TCX, &out.TCX + *out = new(TcxNsProgramInfoState) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BpfNsApplicationProgramState. +func (in *BpfNsApplicationProgramState) DeepCopy() *BpfNsApplicationProgramState { + if in == nil { + return nil + } + out := new(BpfNsApplicationProgramState) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BpfNsApplicationSpec) DeepCopyInto(out *BpfNsApplicationSpec) { *out = *in @@ -492,6 +519,87 @@ func (in *BpfNsApplicationSpec) DeepCopy() *BpfNsApplicationSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BpfNsApplicationState) DeepCopyInto(out *BpfNsApplicationState) { + *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 BpfNsApplicationState. +func (in *BpfNsApplicationState) DeepCopy() *BpfNsApplicationState { + if in == nil { + return nil + } + out := new(BpfNsApplicationState) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BpfNsApplicationState) 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 *BpfNsApplicationStateList) DeepCopyInto(out *BpfNsApplicationStateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]BpfNsApplicationState, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BpfNsApplicationStateList. +func (in *BpfNsApplicationStateList) DeepCopy() *BpfNsApplicationStateList { + if in == nil { + return nil + } + out := new(BpfNsApplicationStateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BpfNsApplicationStateList) 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 *BpfNsApplicationStateSpec) DeepCopyInto(out *BpfNsApplicationStateSpec) { + *out = *in + if in.Programs != nil { + in, out := &in.Programs, &out.Programs + *out = make([]BpfNsApplicationProgramState, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BpfNsApplicationStateSpec. +func (in *BpfNsApplicationStateSpec) DeepCopy() *BpfNsApplicationStateSpec { + if in == nil { + return nil + } + out := new(BpfNsApplicationStateSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BpfNsProgram) DeepCopyInto(out *BpfNsProgram) { *out = *in @@ -1574,6 +1682,22 @@ func (in *TcxNsAttachInfo) DeepCopy() *TcxNsAttachInfo { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TcxNsAttachInfoState) DeepCopyInto(out *TcxNsAttachInfoState) { + *out = *in + in.AttachInfoCommon.DeepCopyInto(&out.AttachInfoCommon) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TcxNsAttachInfoState. +func (in *TcxNsAttachInfoState) DeepCopy() *TcxNsAttachInfoState { + if in == nil { + return nil + } + out := new(TcxNsAttachInfoState) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TcxNsProgram) DeepCopyInto(out *TcxNsProgram) { *out = *in @@ -1624,6 +1748,28 @@ func (in *TcxNsProgramInfo) DeepCopy() *TcxNsProgramInfo { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TcxNsProgramInfoState) DeepCopyInto(out *TcxNsProgramInfoState) { + *out = *in + if in.AttachPoints != nil { + in, out := &in.AttachPoints, &out.AttachPoints + *out = make([]TcxAttachInfoState, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TcxNsProgramInfoState. +func (in *TcxNsProgramInfoState) DeepCopy() *TcxNsProgramInfoState { + if in == nil { + return nil + } + out := new(TcxNsProgramInfoState) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TcxNsProgramList) DeepCopyInto(out *TcxNsProgramList) { *out = *in @@ -2214,6 +2360,27 @@ func (in *XdpNsAttachInfo) DeepCopy() *XdpNsAttachInfo { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *XdpNsAttachInfoState) DeepCopyInto(out *XdpNsAttachInfoState) { + *out = *in + in.AttachInfoCommon.DeepCopyInto(&out.AttachInfoCommon) + if in.ProceedOn != nil { + in, out := &in.ProceedOn, &out.ProceedOn + *out = make([]XdpProceedOnValue, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new XdpNsAttachInfoState. +func (in *XdpNsAttachInfoState) DeepCopy() *XdpNsAttachInfoState { + if in == nil { + return nil + } + out := new(XdpNsAttachInfoState) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *XdpNsProgram) DeepCopyInto(out *XdpNsProgram) { *out = *in @@ -2264,6 +2431,28 @@ func (in *XdpNsProgramInfo) DeepCopy() *XdpNsProgramInfo { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *XdpNsProgramInfoState) DeepCopyInto(out *XdpNsProgramInfoState) { + *out = *in + if in.AttachPoints != nil { + in, out := &in.AttachPoints, &out.AttachPoints + *out = make([]XdpAttachInfoState, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new XdpNsProgramInfoState. +func (in *XdpNsProgramInfoState) DeepCopy() *XdpNsProgramInfoState { + if in == nil { + return nil + } + out := new(XdpNsProgramInfoState) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *XdpNsProgramList) DeepCopyInto(out *XdpNsProgramList) { *out = *in diff --git a/apis/v1alpha1/zz_generated.register.go b/apis/v1alpha1/zz_generated.register.go index 6079cf936..ad40829bb 100644 --- a/apis/v1alpha1/zz_generated.register.go +++ b/apis/v1alpha1/zz_generated.register.go @@ -67,6 +67,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &BpfApplicationStateList{}, &BpfNsApplication{}, &BpfNsApplicationList{}, + &BpfNsApplicationState{}, + &BpfNsApplicationStateList{}, &BpfNsProgram{}, &BpfNsProgramList{}, &BpfProgram{}, diff --git a/bundle/manifests/bpfman-agent-role_rbac.authorization.k8s.io_v1_clusterrole.yaml b/bundle/manifests/bpfman-agent-role_rbac.authorization.k8s.io_v1_clusterrole.yaml index 3025691b6..862b3448d 100644 --- a/bundle/manifests/bpfman-agent-role_rbac.authorization.k8s.io_v1_clusterrole.yaml +++ b/bundle/manifests/bpfman-agent-role_rbac.authorization.k8s.io_v1_clusterrole.yaml @@ -52,6 +52,38 @@ rules: - get - list - watch +- apiGroups: + - bpfman.io + resources: + - bpfnsapplications/finalizers + verbs: + - update +- apiGroups: + - bpfman.io + resources: + - bpfnsapplicationstates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - bpfman.io + resources: + - bpfnsapplicationstates/finalizers + verbs: + - update +- apiGroups: + - bpfman.io + resources: + - bpfnsapplicationstates/status + verbs: + - get + - patch + - update - apiGroups: - bpfman.io resources: diff --git a/bundle/manifests/bpfman-operator.clusterserviceversion.yaml b/bundle/manifests/bpfman-operator.clusterserviceversion.yaml index 105939d71..a9abe5543 100644 --- a/bundle/manifests/bpfman-operator.clusterserviceversion.yaml +++ b/bundle/manifests/bpfman-operator.clusterserviceversion.yaml @@ -118,78 +118,60 @@ metadata: "spec": { "bytecode": { "image": { - "url": "quay.io/bpfman-bytecode/go-app-counter:latest" + "url": "quay.io/bpfman-bytecode/app-test:latest" } }, "nodeselector": {}, "programs": [ { - "tc": { - "bpffunctionname": "stats", - "containers": { - "pods": { - "matchLabels": { - "app": "nginx" - } - } - }, - "direction": "ingress", - "interfaceselector": { - "primarynodeinterface": true - }, - "priority": 55 - }, - "type": "TC" - }, - { + "bpffunctionname": "tcx_next", "tcx": { - "bpffunctionname": "tcx_stats", - "containers": { - "pods": { - "matchLabels": { - "app": "nginx" - } + "attach_points": [ + { + "containers": { + "namespace": "acme", + "pods": { + "matchLabels": { + "app": "nginx" + } + } + }, + "direction": "egress", + "interfaceselector": { + "interfaces": [ + "eth0" + ] + }, + "priority": 100 } - }, - "direction": "ingress", - "interfaceselector": { - "primarynodeinterface": true - }, - "priority": 500 + ], + "bpffunctionname": "tcx_next" }, "type": "TCX" }, { - "type": "Uprobe", - "uprobe": { - "bpffunctionname": "uprobe_counter", - "containers": { - "pods": { - "matchLabels": { - "app": "nginx" - } - } - }, - "func_name": "malloc", - "retprobe": false, - "target": "libc" - } - }, - { + "bpffunctionname": "xdp_pass", "type": "XDP", "xdp": { - "bpffunctionname": "xdp_stats", - "containers": { - "pods": { - "matchLabels": { - "app": "nginx" - } + "attach_points": [ + { + "containers": { + "namespace": "acme", + "pods": { + "matchLabels": { + "app": "nginx" + } + } + }, + "interfaceselector": { + "interfaces": [ + "eth0" + ] + }, + "priority": 100 } - }, - "interfaceselector": { - "primarynodeinterface": true - }, - "priority": 55 + ], + "bpffunctionname": "xdp_pass" } } ] @@ -629,7 +611,7 @@ metadata: capabilities: Basic Install categories: OpenShift Optional containerImage: quay.io/bpfman/bpfman-operator:latest - createdAt: "2025-02-02T17:40:09Z" + createdAt: "2025-02-04T13:27:01Z" features.operators.openshift.io/cnf: "false" features.operators.openshift.io/cni: "false" features.operators.openshift.io/csi: "true" @@ -687,6 +669,9 @@ spec: kind: BpfNsApplication name: bpfnsapplications.bpfman.io version: v1alpha1 + - kind: BpfNsApplicationState + name: bpfnsapplicationstates.bpfman.io + version: v1alpha1 - description: BpfNsProgram is the Schema for the BpfNsProgram API displayName: Bpf Namespaced Program kind: BpfNsProgram @@ -1152,6 +1137,14 @@ spec: - get - patch - update + - apiGroups: + - bpfman.io + resources: + - bpfnsapplicationstates + verbs: + - get + - list + - watch - apiGroups: - bpfman.io resources: @@ -1698,6 +1691,14 @@ spec: - get - patch - update + - apiGroups: + - bpfman.io + resources: + - bpfnsapplicationstates + verbs: + - get + - list + - watch - apiGroups: - bpfman.io resources: diff --git a/bundle/manifests/bpfman.io_bpfnsapplications.yaml b/bundle/manifests/bpfman.io_bpfnsapplications.yaml index 3a7132595..b8c420ea3 100644 --- a/bundle/manifests/bpfman.io_bpfnsapplications.yaml +++ b/bundle/manifests/bpfman.io_bpfnsapplications.yaml @@ -212,6 +212,65 @@ spec: description: BpfNsApplicationProgram defines the desired state of BpfApplication properties: + bpffunctionname: + description: |- + BpfFunctionName is the name of the function that is the entry point for the BPF + program + type: string + oldmapownerselector: + description: |- + ANF-TODO: MapOwnerSelector has been moved to BpfAppCommon. Do not use in + new load/attach split code. + + + OldMapOwnerSelector is used to select the loaded eBPF program this eBPF + program will share a map with. The value is a label applied to the + BpfProgram to select. The selector must resolve to exactly one instance + of a BpfProgram on a given node or the eBPF program will not load. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic tc: description: tc defines the desired state of the application's TcNsPrograms. @@ -1126,6 +1185,8 @@ spec: required: - bpffunctionname type: object + required: + - bpffunctionname type: object x-kubernetes-validations: - message: xdp configuration is required when type is XDP, and forbidden diff --git a/bundle/manifests/bpfman.io_bpfnsapplicationstates.yaml b/bundle/manifests/bpfman.io_bpfnsapplicationstates.yaml new file mode 100644 index 000000000..d691aa69c --- /dev/null +++ b/bundle/manifests/bpfman.io_bpfnsapplicationstates.yaml @@ -0,0 +1,425 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + creationTimestamp: null + name: bpfnsapplicationstates.bpfman.io +spec: + group: bpfman.io + names: + kind: BpfNsApplicationState + listKind: BpfNsApplicationStateList + plural: bpfnsapplicationstates + singular: bpfnsapplicationstate + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.node + name: Node + type: string + - jsonPath: .status.conditions[0].reason + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: BpfNsApplicationState contains the per-node state of a BpfNsApplication. + 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: BpfNsApplicationSpec defines the desired state of BpfNsApplication + properties: + apploadstatus: + description: |- + AppLoadStatus reflects the status of loading the bpf application on the + given node. + type: string + node: + description: Node is the name of the node for this BpfNsApplicationStateSpec. + type: string + programs: + description: |- + Programs is a list of bpf programs contained in the parent application. + It is a map from the bpf program name to BpfNsApplicationProgramState + elements. + items: + description: |- + BpfNsApplicationProgramState defines the desired state of BpfNsApplication + // +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'TC' ? has(self.tc) : !has(self.tc)",message="tc configuration is required when type is TC, and forbidden otherwise" + // +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'Uprobe' ? has(self.uprobe) : !has(self.uprobe)",message="uprobe configuration is required when type is Uprobe, and forbidden otherwise" + // +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'Uretprobe' ? has(self.uretprobe) : !has(self.uretprobe)",message="uretprobe configuration is required when type is Uretprobe, and forbidden otherwise" + properties: + bpffunctionname: + description: |- + BpfFunctionName is the name of the function that is the entry point for the BPF + program + type: string + oldmapownerselector: + description: |- + ANF-TODO: MapOwnerSelector has been moved to BpfAppCommon. Do not use in + new load/attach split code. + + + OldMapOwnerSelector is used to select the loaded eBPF program this eBPF + program will share a map with. The value is a label applied to the + BpfProgram to select. The selector must resolve to exactly one instance + of a BpfProgram on a given node or the eBPF program will not load. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + program_id: + description: |- + ProgramId is the id of the program in the kernel. Not set until the + program is loaded. + format: int32 + type: integer + programattachstatus: + description: |- + ProgramAttachStatus records whether the program should be loaded and whether + the program is loaded. + type: string + tcx: + description: tcx defines the desired state of the application's + TcxPrograms. + properties: + attach_points: + description: |- + The list of points to which the program should be attached. + TcxAttachInfoState is similar to TcxAttachInfo, but the interface and + container selectors are expanded, and we have one instance of + TcxAttachInfoState for each unique attach point. The list is optional and + may be udated after the bpf program has been loaded. + items: + properties: + attachid: + description: |- + An identifier for the attach point assigned by bpfman. This field is + empty until the program is successfully attached and bpfman returns the + id. + ANF-TODO: For the POC, this will be the program ID. + format: int32 + type: integer + attachstatus: + description: |- + AttachStatus reflects whether the attachment has been reconciled + successfully, and if not, why. + type: string + containerpid: + description: Optional container pid to attach the + tcx program in. + format: int32 + type: integer + direction: + description: |- + Direction specifies the direction of traffic the tcx program should + attach to for a given network device. + enum: + - ingress + - egress + type: string + ifname: + description: Interface name to attach the tcx program + to. + type: string + priority: + description: |- + Priority specifies the priority of the tcx program in relation to + other programs of the same type with the same attach point. It is a value + from 0 to 1000 where lower values have higher precedence. + format: int32 + maximum: 1000 + minimum: 0 + type: integer + should_attach: + description: ShouldAttach reflects whether the attachment + should exist. + type: boolean + uuid: + description: |- + ANF-TODO: Putting a uuid here for now to maintain compatibility with the + existing BpfProgram. + type: string + required: + - attachid + - attachstatus + - direction + - ifname + - priority + - should_attach + - uuid + type: object + type: array + type: object + type: + description: Type specifies the bpf program type + enum: + - XDP + - TC + - TCX + - Fentry + - Fexit + - Kprobe + - Kretprobe + - Uprobe + - Uretprobe + - Tracepoint + type: string + xdp: + description: xdp defines the desired state of the application's + XdpPrograms. + properties: + attach_points: + description: |- + The list of points to which the program should be attached. + XdpAttachInfoState is similar to XdpAttachInfo, but the interface and + container selectors are expanded, and we have one instance of + XdpAttachInfoState for each unique attach point. The list is optional and + may be udated after the bpf program has been loaded. + items: + properties: + attachid: + description: |- + An identifier for the attach point assigned by bpfman. This field is + empty until the program is successfully attached and bpfman returns the + id. + ANF-TODO: For the POC, this will be the program ID. + format: int32 + type: integer + attachstatus: + description: |- + AttachStatus reflects whether the attachment has been reconciled + successfully, and if not, why. + type: string + containerpid: + description: Optional container pid to attach the + xdp program in. + format: int32 + type: integer + ifname: + description: Interface name to attach the xdp program + to. + type: string + priority: + description: |- + Priority specifies the priority of the xdp program in relation to + other programs of the same type with the same attach point. It is a value + from 0 to 1000 where lower values have higher precedence. + format: int32 + maximum: 1000 + minimum: 0 + type: integer + proceedon: + description: |- + ProceedOn allows the user to call other xdp programs in chain on this exit code. + Multiple values are supported by repeating the parameter. + items: + enum: + - aborted + - drop + - pass + - tx + - redirect + - dispatcher_return + type: string + maxItems: 6 + type: array + should_attach: + description: ShouldAttach reflects whether the attachment + should exist. + type: boolean + uuid: + description: |- + ANF-TODO: Putting a uuid here for now to maintain compatibility with the + existing BpfProgram. + type: string + required: + - attachid + - attachstatus + - ifname + - priority + - proceedon + - should_attach + - uuid + type: object + type: array + type: object + required: + - bpffunctionname + - programattachstatus + type: object + x-kubernetes-validations: + - message: xdp configuration is required when type is XDP, and forbidden + otherwise + rule: 'has(self.type) && self.type == ''XDP'' ? has(self.xdp) + : !has(self.xdp)' + - message: tcx configuration is required when type is TCX, and forbidden + otherwise + rule: 'has(self.type) && self.type == ''TCX'' ? has(self.tcx) + : !has(self.tcx)' + type: array + updatecount: + description: |- + The number of times the BpfNsApplicationState has been updated. Set to 1 + when the object is created, then it is incremented prior to each update. + This allows us to verify that the API server has the updated object prior + to starting a new Reconcile operation. + format: int64 + type: integer + required: + - apploadstatus + - node + - updatecount + type: object + status: + description: BpfAppStatus reflects the status of a BpfApplication or BpfApplicationState + object + properties: + conditions: + description: |- + For a BpfApplication object, Conditions contains the global cluster state + for the object. For a BpfApplicationState object, Conditions contains the + state of the BpfApplication object on the given node. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/cmd/bpfman-agent/main.go b/cmd/bpfman-agent/main.go index 6fd6d5b6f..1e3195449 100644 --- a/cmd/bpfman-agent/main.go +++ b/cmd/bpfman-agent/main.go @@ -259,6 +259,13 @@ func main() { // os.Exit(1) // } + if err = (&appagent.BpfNsApplicationReconciler{ + ReconcilerCommon: commonApp, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create BpfApplicationProgram controller", "controller", "BpfProgram") + os.Exit(1) + } + // if err = (&bpfmanagent.TcxNsProgramReconciler{ // NamespaceProgramReconciler: commonNamespace, // }).SetupWithManager(mgr); err != nil { diff --git a/cmd/bpfman-operator/main.go b/cmd/bpfman-operator/main.go index ffff66d18..92e4f7ef2 100644 --- a/cmd/bpfman-operator/main.go +++ b/cmd/bpfman-operator/main.go @@ -192,6 +192,15 @@ func main() { ReconcilerCommon: commonApp, } + commonNsApp := appoperator.ReconcilerCommon[bpfmaniov1alpha1.BpfNsApplicationState, bpfmaniov1alpha1.BpfNsApplicationStateList]{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + } + + commonNamespaceApp := appoperator.NamespaceProgramReconciler{ + ReconcilerCommon: commonNsApp, + } + setupLog.Info("Discovering APIs") dc, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig()) if err != nil { @@ -287,6 +296,13 @@ func main() { // os.Exit(1) // } + if err = (&appoperator.BpfNsApplicationReconciler{ + NamespaceProgramReconciler: commonNamespaceApp, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "BpfApplication") + os.Exit(1) + } + // if err = (&bpfmanoperator.TcxNsProgramReconciler{ // NamespaceProgramReconciler: commonNamespace, // }).SetupWithManager(mgr); err != nil { diff --git a/config/bpfman-deployment/config.yaml b/config/bpfman-deployment/config.yaml index fd9fdaa54..11402f831 100644 --- a/config/bpfman-deployment/config.yaml +++ b/config/bpfman-deployment/config.yaml @@ -18,3 +18,6 @@ data: [database] max_retries = 30 millisec_delay = 10000 + [signing] + allow_unsigned = true + verify_enabled = false diff --git a/config/crd/bases/bpfman.io_bpfnsapplications.yaml b/config/crd/bases/bpfman.io_bpfnsapplications.yaml index 172ebd916..7c9744283 100644 --- a/config/crd/bases/bpfman.io_bpfnsapplications.yaml +++ b/config/crd/bases/bpfman.io_bpfnsapplications.yaml @@ -212,6 +212,65 @@ spec: description: BpfNsApplicationProgram defines the desired state of BpfApplication properties: + bpffunctionname: + description: |- + BpfFunctionName is the name of the function that is the entry point for the BPF + program + type: string + oldmapownerselector: + description: |- + ANF-TODO: MapOwnerSelector has been moved to BpfAppCommon. Do not use in + new load/attach split code. + + + OldMapOwnerSelector is used to select the loaded eBPF program this eBPF + program will share a map with. The value is a label applied to the + BpfProgram to select. The selector must resolve to exactly one instance + of a BpfProgram on a given node or the eBPF program will not load. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic tc: description: tc defines the desired state of the application's TcNsPrograms. @@ -1126,6 +1185,8 @@ spec: required: - bpffunctionname type: object + required: + - bpffunctionname type: object x-kubernetes-validations: - message: xdp configuration is required when type is XDP, and forbidden diff --git a/config/crd/bases/bpfman.io_bpfnsapplicationstates.yaml b/config/crd/bases/bpfman.io_bpfnsapplicationstates.yaml new file mode 100644 index 000000000..8ab457833 --- /dev/null +++ b/config/crd/bases/bpfman.io_bpfnsapplicationstates.yaml @@ -0,0 +1,419 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: bpfnsapplicationstates.bpfman.io +spec: + group: bpfman.io + names: + kind: BpfNsApplicationState + listKind: BpfNsApplicationStateList + plural: bpfnsapplicationstates + singular: bpfnsapplicationstate + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.node + name: Node + type: string + - jsonPath: .status.conditions[0].reason + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: BpfNsApplicationState contains the per-node state of a BpfNsApplication. + 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: BpfNsApplicationSpec defines the desired state of BpfNsApplication + properties: + apploadstatus: + description: |- + AppLoadStatus reflects the status of loading the bpf application on the + given node. + type: string + node: + description: Node is the name of the node for this BpfNsApplicationStateSpec. + type: string + programs: + description: |- + Programs is a list of bpf programs contained in the parent application. + It is a map from the bpf program name to BpfNsApplicationProgramState + elements. + items: + description: |- + BpfNsApplicationProgramState defines the desired state of BpfNsApplication + // +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'TC' ? has(self.tc) : !has(self.tc)",message="tc configuration is required when type is TC, and forbidden otherwise" + // +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'Uprobe' ? has(self.uprobe) : !has(self.uprobe)",message="uprobe configuration is required when type is Uprobe, and forbidden otherwise" + // +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'Uretprobe' ? has(self.uretprobe) : !has(self.uretprobe)",message="uretprobe configuration is required when type is Uretprobe, and forbidden otherwise" + properties: + bpffunctionname: + description: |- + BpfFunctionName is the name of the function that is the entry point for the BPF + program + type: string + oldmapownerselector: + description: |- + ANF-TODO: MapOwnerSelector has been moved to BpfAppCommon. Do not use in + new load/attach split code. + + + OldMapOwnerSelector is used to select the loaded eBPF program this eBPF + program will share a map with. The value is a label applied to the + BpfProgram to select. The selector must resolve to exactly one instance + of a BpfProgram on a given node or the eBPF program will not load. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + program_id: + description: |- + ProgramId is the id of the program in the kernel. Not set until the + program is loaded. + format: int32 + type: integer + programattachstatus: + description: |- + ProgramAttachStatus records whether the program should be loaded and whether + the program is loaded. + type: string + tcx: + description: tcx defines the desired state of the application's + TcxPrograms. + properties: + attach_points: + description: |- + The list of points to which the program should be attached. + TcxAttachInfoState is similar to TcxAttachInfo, but the interface and + container selectors are expanded, and we have one instance of + TcxAttachInfoState for each unique attach point. The list is optional and + may be udated after the bpf program has been loaded. + items: + properties: + attachid: + description: |- + An identifier for the attach point assigned by bpfman. This field is + empty until the program is successfully attached and bpfman returns the + id. + ANF-TODO: For the POC, this will be the program ID. + format: int32 + type: integer + attachstatus: + description: |- + AttachStatus reflects whether the attachment has been reconciled + successfully, and if not, why. + type: string + containerpid: + description: Optional container pid to attach the + tcx program in. + format: int32 + type: integer + direction: + description: |- + Direction specifies the direction of traffic the tcx program should + attach to for a given network device. + enum: + - ingress + - egress + type: string + ifname: + description: Interface name to attach the tcx program + to. + type: string + priority: + description: |- + Priority specifies the priority of the tcx program in relation to + other programs of the same type with the same attach point. It is a value + from 0 to 1000 where lower values have higher precedence. + format: int32 + maximum: 1000 + minimum: 0 + type: integer + should_attach: + description: ShouldAttach reflects whether the attachment + should exist. + type: boolean + uuid: + description: |- + ANF-TODO: Putting a uuid here for now to maintain compatibility with the + existing BpfProgram. + type: string + required: + - attachid + - attachstatus + - direction + - ifname + - priority + - should_attach + - uuid + type: object + type: array + type: object + type: + description: Type specifies the bpf program type + enum: + - XDP + - TC + - TCX + - Fentry + - Fexit + - Kprobe + - Kretprobe + - Uprobe + - Uretprobe + - Tracepoint + type: string + xdp: + description: xdp defines the desired state of the application's + XdpPrograms. + properties: + attach_points: + description: |- + The list of points to which the program should be attached. + XdpAttachInfoState is similar to XdpAttachInfo, but the interface and + container selectors are expanded, and we have one instance of + XdpAttachInfoState for each unique attach point. The list is optional and + may be udated after the bpf program has been loaded. + items: + properties: + attachid: + description: |- + An identifier for the attach point assigned by bpfman. This field is + empty until the program is successfully attached and bpfman returns the + id. + ANF-TODO: For the POC, this will be the program ID. + format: int32 + type: integer + attachstatus: + description: |- + AttachStatus reflects whether the attachment has been reconciled + successfully, and if not, why. + type: string + containerpid: + description: Optional container pid to attach the + xdp program in. + format: int32 + type: integer + ifname: + description: Interface name to attach the xdp program + to. + type: string + priority: + description: |- + Priority specifies the priority of the xdp program in relation to + other programs of the same type with the same attach point. It is a value + from 0 to 1000 where lower values have higher precedence. + format: int32 + maximum: 1000 + minimum: 0 + type: integer + proceedon: + description: |- + ProceedOn allows the user to call other xdp programs in chain on this exit code. + Multiple values are supported by repeating the parameter. + items: + enum: + - aborted + - drop + - pass + - tx + - redirect + - dispatcher_return + type: string + maxItems: 6 + type: array + should_attach: + description: ShouldAttach reflects whether the attachment + should exist. + type: boolean + uuid: + description: |- + ANF-TODO: Putting a uuid here for now to maintain compatibility with the + existing BpfProgram. + type: string + required: + - attachid + - attachstatus + - ifname + - priority + - proceedon + - should_attach + - uuid + type: object + type: array + type: object + required: + - bpffunctionname + - programattachstatus + type: object + x-kubernetes-validations: + - message: xdp configuration is required when type is XDP, and forbidden + otherwise + rule: 'has(self.type) && self.type == ''XDP'' ? has(self.xdp) + : !has(self.xdp)' + - message: tcx configuration is required when type is TCX, and forbidden + otherwise + rule: 'has(self.type) && self.type == ''TCX'' ? has(self.tcx) + : !has(self.tcx)' + type: array + updatecount: + description: |- + The number of times the BpfNsApplicationState has been updated. Set to 1 + when the object is created, then it is incremented prior to each update. + This allows us to verify that the API server has the updated object prior + to starting a new Reconcile operation. + format: int64 + type: integer + required: + - apploadstatus + - node + - updatecount + type: object + status: + description: BpfAppStatus reflects the status of a BpfApplication or BpfApplicationState + object + properties: + conditions: + description: |- + For a BpfApplication object, Conditions contains the global cluster state + for the object. For a BpfApplicationState object, Conditions contains the + state of the BpfApplication object on the given node. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 5d24eac35..a1eb2d31d 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -19,6 +19,7 @@ resources: - bases/bpfman.io_uprobensprograms.yaml - bases/bpfman.io_bpfnsapplications.yaml - bases/bpfman.io_bpfapplicationstates.yaml + - bases/bpfman.io_bpfnsapplicationstates.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: diff --git a/config/rbac/bpfman-agent/role.yaml b/config/rbac/bpfman-agent/role.yaml index 6874d3812..b252ed389 100644 --- a/config/rbac/bpfman-agent/role.yaml +++ b/config/rbac/bpfman-agent/role.yaml @@ -52,6 +52,38 @@ rules: - get - list - watch +- apiGroups: + - bpfman.io + resources: + - bpfnsapplications/finalizers + verbs: + - update +- apiGroups: + - bpfman.io + resources: + - bpfnsapplicationstates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - bpfman.io + resources: + - bpfnsapplicationstates/finalizers + verbs: + - update +- apiGroups: + - bpfman.io + resources: + - bpfnsapplicationstates/status + verbs: + - get + - patch + - update - apiGroups: - bpfman.io resources: diff --git a/config/rbac/bpfman-operator/role.yaml b/config/rbac/bpfman-operator/role.yaml index 68fb2a332..0eefb694e 100644 --- a/config/rbac/bpfman-operator/role.yaml +++ b/config/rbac/bpfman-operator/role.yaml @@ -76,6 +76,14 @@ rules: - get - patch - update +- apiGroups: + - bpfman.io + resources: + - bpfnsapplicationstates + verbs: + - get + - list + - watch - apiGroups: - bpfman.io resources: @@ -480,6 +488,14 @@ rules: - get - patch - update +- apiGroups: + - bpfman.io + resources: + - bpfnsapplicationstates + verbs: + - get + - list + - watch - apiGroups: - bpfman.io resources: diff --git a/config/samples/bpfman.io_v1alpha1_bpfnsapplication.yaml b/config/samples/bpfman.io_v1alpha1_bpfnsapplication.yaml index 16260b119..bf6b6fac5 100644 --- a/config/samples/bpfman.io_v1alpha1_bpfnsapplication.yaml +++ b/config/samples/bpfman.io_v1alpha1_bpfnsapplication.yaml @@ -10,47 +10,53 @@ spec: nodeselector: {} bytecode: image: - url: quay.io/bpfman-bytecode/go-app-counter:latest + url: quay.io/bpfman-bytecode/app-test:latest programs: - - type: TC - tc: - bpffunctionname: stats - interfaceselector: - primarynodeinterface: true - priority: 55 - direction: ingress - containers: - pods: - matchLabels: - app: nginx - - type: TCX + # - type: TC + # tc: + # bpffunctionname: stats + # interfaceselector: + # primarynodeinterface: true + # priority: 55 + # direction: ingress + # containers: + # pods: + # matchLabels: + # app: nginx + - bpffunctionname: tcx_next + type: TCX tcx: - bpffunctionname: tcx_stats - interfaceselector: - primarynodeinterface: true - priority: 500 - direction: ingress - containers: - pods: - matchLabels: - app: nginx - - type: Uprobe - uprobe: - bpffunctionname: uprobe_counter - func_name: malloc - target: libc - retprobe: false - containers: - pods: - matchLabels: - app: nginx - - type: XDP + bpffunctionname: tcx_next + attach_points: + - interfaceselector: + interfaces: + - eth0 + priority: 100 + direction: egress + containers: + pods: + matchLabels: + app: nginx + # - type: Uprobe + # uprobe: + # bpffunctionname: uprobe_counter + # func_name: malloc + # target: libc + # retprobe: false + # containers: + # pods: + # matchLabels: + # app: nginx + - bpffunctionname: xdp_pass + type: XDP xdp: - bpffunctionname: xdp_stats - interfaceselector: - primarynodeinterface: true - priority: 55 - containers: - pods: - matchLabels: - app: nginx + bpffunctionname: xdp_pass + attach_points: + - interfaceselector: + interfaces: + - eth0 + priority: 100 + containers: + pods: + matchLabels: + app: nginx diff --git a/controllers/app-agent/cl-application-program.go b/controllers/app-agent/cl-application-program.go index 937d8b780..6409ed673 100644 --- a/controllers/app-agent/cl-application-program.go +++ b/controllers/app-agent/cl-application-program.go @@ -54,9 +54,13 @@ type BpfApplicationReconciler struct { currentAppState *bpfmaniov1alpha1.BpfApplicationState } -// func (r *BpfApplicationReconciler) getRecType() string { -// return internal.ApplicationString -// } +type ProgramReconcilerCommon struct { + // ANF-TODO: appCommon is needed to load the program. It won't be needed + // after the load/attch split is ready. + appCommon bpfmaniov1alpha1.BpfAppCommon + currentProgram *bpfmaniov1alpha1.BpfApplicationProgram + currentProgramState *bpfmaniov1alpha1.BpfApplicationProgramState +} func (r *BpfApplicationReconciler) getAppStateName() string { return r.currentAppState.Name @@ -118,7 +122,6 @@ func (r *BpfApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error { func (r *BpfApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { // Initialize node and current program - // r.currentApp = &bpfmaniov1alpha1.BpfApplication{} r.ourNode = &v1.Node{} r.Logger = ctrl.Log.WithName("cluster-app") r.finalizer = internal.BpfApplicationControllerFinalizer @@ -158,7 +161,7 @@ func (r *BpfApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Reque r.currentApp = appProgram - // if bpfAppStateNew is true, then we need to create a new + // If bpfAppStateNew is true, then we need to create a new // BpfApplicationState at the end of the reconcile instead of just // updating the existing one. appState, bpfAppStateNew, err := r.getBpfAppState(ctx, true) diff --git a/controllers/app-agent/cl-tcx-program.go b/controllers/app-agent/cl-tcx-program.go index 349a88dde..8b67b3d44 100644 --- a/controllers/app-agent/cl-tcx-program.go +++ b/controllers/app-agent/cl-tcx-program.go @@ -250,7 +250,8 @@ func (r *TcxProgramReconciler) removeAttachPoints(attachPoints []bpfmaniov1alpha return remainingAttachPoints } -// getInterfaces expands TcxAttachInfo into a list of specific attach points. It works pretty much like the old getExpectedBpfPrograms. +// getExpectedAttachPoints expands *AttachInfo into a list of specific attach +// points. func (r *TcxProgramReconciler) getExpectedAttachPoints(ctx context.Context, attachInfo bpfmaniov1alpha1.TcxAttachInfo, ) ([]bpfmaniov1alpha1.TcxAttachInfoState, error) { interfaces, err := getInterfaces(&attachInfo.InterfaceSelector, r.ourNode) diff --git a/controllers/app-agent/cl-xdp-program.go b/controllers/app-agent/cl-xdp-program.go index bb77cc86b..0edcee673 100644 --- a/controllers/app-agent/cl-xdp-program.go +++ b/controllers/app-agent/cl-xdp-program.go @@ -273,7 +273,8 @@ func (r *XdpProgramReconciler) removeAttachPoints(attachPoints []bpfmaniov1alpha return newAttachPoints } -// getInterfaces expands XdpAttachInfo into a list of specific attach points. It works pretty much like the old getExpectedBpfPrograms. +// getExpectedAttachPoints expands *AttachInfo into a list of specific attach +// points. func (r *XdpProgramReconciler) getExpectedAttachPoints(ctx context.Context, attachInfo bpfmaniov1alpha1.XdpAttachInfo, ) ([]bpfmaniov1alpha1.XdpAttachInfoState, error) { interfaces, err := getInterfaces(&attachInfo.InterfaceSelector, r.ourNode) diff --git a/controllers/app-agent/common.go b/controllers/app-agent/common.go index e75815cb5..359ea8c05 100644 --- a/controllers/app-agent/common.go +++ b/controllers/app-agent/common.go @@ -67,14 +67,6 @@ type ReconcilerCommon struct { ourNode *v1.Node } -type ProgramReconcilerCommon struct { - // ANF-TODO: appCommon is needed to load the program. It won't be needed - // after the load/attch split is ready. - appCommon bpfmaniov1alpha1.BpfAppCommon - currentProgram *bpfmaniov1alpha1.BpfApplicationProgram - currentProgramState *bpfmaniov1alpha1.BpfApplicationProgramState -} - // ApplicationReconciler is an interface that defines the methods needed to // reconcile a BpfApplication. type ApplicationReconciler interface { diff --git a/controllers/app-agent/ns-application-program.go b/controllers/app-agent/ns-application-program.go new file mode 100644 index 000000000..abd1ff2da --- /dev/null +++ b/controllers/app-agent/ns-application-program.go @@ -0,0 +1,518 @@ +/* +Copyright 2025. + +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 appagent + +import ( + "context" + "fmt" + "reflect" + "time" + + bpfmaniov1alpha1 "github.com/bpfman/bpfman-operator/apis/v1alpha1" + "github.com/bpfman/bpfman-operator/internal" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +//+kubebuilder:rbac:groups=bpfman.io,resources=bpfnsapplications,verbs=get;list;watch +//+kubebuilder:rbac:groups=bpfman.io,resources=bpfnsapplicationstates,verbs=get;list;watch +// +kubebuilder:rbac:groups=bpfman.io,resources=bpfnsapplicationstates,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=bpfman.io,resources=bpfnsapplicationstates/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=bpfman.io,resources=bpfnsapplicationstates/finalizers,verbs=update +// +kubebuilder:rbac:groups=bpfman.io,resources=bpfnsapplications/finalizers,verbs=update +// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=nodes,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get + +type BpfNsApplicationReconciler struct { + ReconcilerCommon + currentApp *bpfmaniov1alpha1.BpfNsApplication + currentAppState *bpfmaniov1alpha1.BpfNsApplicationState +} + +type ProgramNsReconcilerCommon struct { + // ANF-TODO: appCommon is needed to load the program. It won't be needed + // after the load/attch split is ready. + appCommon bpfmaniov1alpha1.BpfAppCommon + currentProgram *bpfmaniov1alpha1.BpfNsApplicationProgram + currentProgramState *bpfmaniov1alpha1.BpfNsApplicationProgramState + namespace string +} + +func (r *BpfNsApplicationReconciler) getAppStateName() string { + return r.currentAppState.Name +} + +func (r *BpfNsApplicationReconciler) getNode() *v1.Node { + return r.ourNode +} + +func (r *BpfNsApplicationReconciler) getNodeSelector() *metav1.LabelSelector { + return &r.currentApp.Spec.NodeSelector +} + +func (r *BpfNsApplicationReconciler) GetStatus() *bpfmaniov1alpha1.BpfAppStatus { + return &r.currentAppState.Status +} + +func (r *BpfNsApplicationReconciler) isBeingDeleted() bool { + return !r.currentApp.GetDeletionTimestamp().IsZero() +} + +func (r *BpfNsApplicationReconciler) updateBpfAppStatus(ctx context.Context, condition metav1.Condition) error { + r.currentAppState.Status.Conditions = nil + meta.SetStatusCondition(&r.currentAppState.Status.Conditions, condition) + return r.Status().Update(ctx, r.currentAppState) +} + +func (r *BpfNsApplicationReconciler) updateLoadStatus(newCondition bpfmaniov1alpha1.BpfProgramConditionType) { + r.currentAppState.Spec.AppLoadStatus = newCondition +} + +// SetupWithManager sets up the controller with the Manager. The Bpfman-Agent +// should reconcile whenever a BpfNsApplication object is updated, load/unload bpf +// programs on the node via bpfman, and create or update a BpfNsApplicationState +// object to reflect per node state information. +func (r *BpfNsApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&bpfmaniov1alpha1.BpfNsApplication{}, builder.WithPredicates(predicate.And(predicate.GenerationChangedPredicate{}, predicate.ResourceVersionChangedPredicate{}))). + WithOptions(controller.Options{MaxConcurrentReconciles: 1}). + Owns(&bpfmaniov1alpha1.BpfNsApplicationState{}, + builder.WithPredicates(internal.BpfNodePredicate(r.NodeName)), + ). + // Only trigger reconciliation if node labels change since that could + // make the BpfNsApplication no longer select the Node. Additionally only + // care about node events specific to our node + Watches( + &v1.Node{}, + &handler.EnqueueRequestForObject{}, + builder.WithPredicates(predicate.And(predicate.LabelChangedPredicate{}, nodePredicate(r.NodeName))), + ). + // Watch for changes in Pod resources in case we are using a container selector. + Watches( + &v1.Pod{}, + &handler.EnqueueRequestForObject{}, + builder.WithPredicates(podOnNodePredicate(r.NodeName)), + ). + Complete(r) +} + +func (r *BpfNsApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + // Initialize node and current program + r.ourNode = &v1.Node{} + r.Logger = ctrl.Log.WithName("namespace-app") + r.finalizer = internal.BpfNsApplicationControllerFinalizer + r.recType = internal.ApplicationString + + r.Logger.Info("Enter BpfNsApplication Reconcile", "Name", req.Name) + + // Lookup K8s node object for this bpfman-agent This should always succeed + if err := r.Get(ctx, types.NamespacedName{Namespace: v1.NamespaceAll, Name: r.NodeName}, r.ourNode); err != nil { + return ctrl.Result{Requeue: false}, fmt.Errorf("failed getting bpfman-agent node %s : %v", + req.NamespacedName, err) + } + + // Get the list of existing BpfNsApplication objects + appPrograms := &bpfmaniov1alpha1.BpfNsApplicationList{} + opts := []client.ListOption{} + if err := r.List(ctx, appPrograms, opts...); err != nil { + return ctrl.Result{Requeue: false}, fmt.Errorf("failed getting BpfNsApplicationPrograms for full reconcile %s : %v", + req.NamespacedName, err) + } + if len(appPrograms.Items) == 0 { + r.Logger.Info("BpfNsApplicationController found no application Programs") + return ctrl.Result{Requeue: false}, nil + } + + for appProgramIndex := range appPrograms.Items { + appProgram := &appPrograms.Items[appProgramIndex] + // ANF-TODO: After load/attach split, we will need to load the code defined + // in the BpfNsApplication here one time before we go through the list of + // programs. However, for now, we need to keep the current behavior and + // load it for every attachment. + + // Get the corresponding BpfNsApplicationState object, and if it doesn't + // exist, instantiate a copy, but don't create it until after we've + // processed each program and its attachments. + // Only list bpfPrograms for this *Program and the controller's node + + r.currentApp = appProgram + + // If bpfAppStateNew is true, then we need to create a new + // BpfNsApplicationState at the end of the reconcile instead of just + // updating the existing one. + appState, bpfAppStateNew, err := r.getBpfAppState(ctx, true) + if err != nil { + r.Logger.Error(err, "failed to get BpfNsApplicationState") + return ctrl.Result{}, err + } + r.currentAppState = appState + + // Save a copy of the original BpfNsApplicationState to check for changes + // at the end of the reconcile process. This approach simplifies the + // code and reduces the risk of errors by avoiding the need to track + // changes throughout. We don't need to do this for new + // BpfNsApplicationStates because they don't exist yet and will need to be + // created anyway. + var bpfAppStateOriginal *bpfmaniov1alpha1.BpfNsApplicationState + if !bpfAppStateNew { + bpfAppStateOriginal = r.currentAppState.DeepCopy() + } + + r.Logger.Info("From getBpfAppState", "new", bpfAppStateNew) + + // Make sure the BpfNsApplication code is loaded on the node. + r.Logger.Info("Calling reconcileLoad()") + err = r.reconcileLoad(r) + if err != nil { + // There's no point continuing to reconcile the attachments if we + // can't load the code. + r.Logger.Error(err, "failed to reconcileLoad") + objectChanged, _ := r.updateBpfAppStateSpec(ctx, bpfAppStateOriginal, bpfAppStateNew) + statusChanged := r.updateStatus(ctx, r, bpfmaniov1alpha1.ProgramReconcileError) + if statusChanged || objectChanged { + return ctrl.Result{Requeue: true, RequeueAfter: retryDurationAgent}, nil + } else { + // If nothing changed, continue with the next BpfNsApplication. + // Otherwise, one bad BpfNsApplication can block the rest. + continue + } + } + + // Initialize the BpfNsApplicationState status to Success. It will be set + // to Error if any of the programs have an error. + bpfApplicationStatus := bpfmaniov1alpha1.ProgramReconcileSuccess + + // function + + // Reconcile each program in the BpfNsApplication + for progIndex := range appProgram.Spec.Programs { + prog := &appProgram.Spec.Programs[progIndex] + progState, err := r.getProgState(prog, r.currentAppState.Spec.Programs) + if err != nil { + // ANF-TODO: This entry should have been created when the + // BpfNsApplication was loaded. If it's not here, then we need to + // do another load, and we'll need to work out how to do that. + // If we just do a load here for the new program, then it won't + // share global data with the existing programs. So, we need to + // decide whether to just do an incremental load, or unload the + // existing programs and reload everything. In the future, we + // may be able to add more seamless support for incremental + // loads. However, in this POC code, we're going to log an error + // and continue. + r.Logger.Error(fmt.Errorf("ProgramState not found"), + "ProgramState not found", "App Name", r.currentApp.Name, "BpfFunctionName", prog.BpfFunctionName) + continue + } + + var rec ProgramReconciler + + switch prog.Type { + // ANF-TODO: Implement support for other program types. + + // case bpfmaniov1alpha1.ProgTypeUprobe: + // case bpfmaniov1alpha1.ProgTypeTC: + + case bpfmaniov1alpha1.ProgTypeTCX: + rec = &TcxNsProgramReconciler{ + ReconcilerCommon: r.ReconcilerCommon, + ProgramNsReconcilerCommon: ProgramNsReconcilerCommon{ + appCommon: r.currentApp.Spec.BpfAppCommon, + currentProgram: prog, + currentProgramState: progState, + namespace: r.currentApp.Namespace, + }, + } + + case bpfmaniov1alpha1.ProgTypeXDP: + rec = &XdpNsProgramReconciler{ + ReconcilerCommon: r.ReconcilerCommon, + ProgramNsReconcilerCommon: ProgramNsReconcilerCommon{ + appCommon: r.currentApp.Spec.BpfAppCommon, + currentProgram: prog, + currentProgramState: progState, + namespace: r.currentApp.Namespace, + }, + } + + default: + bpfApplicationStatus = bpfmaniov1alpha1.ProgramReconcileError + r.Logger.Error(fmt.Errorf("unsupported bpf program type"), "unsupported bpf program type", "ProgType", prog.Type) + // Skip this program and continue to the next one + continue + } + + err = rec.reconcileProgram(ctx, rec, r.isBeingDeleted()) + if err != nil { + updateSimpleStatus(&progState.ProgramAttachStatus, bpfmaniov1alpha1.BpfProgCondAttachError) + bpfApplicationStatus = bpfmaniov1alpha1.ProgramReconcileError + r.Logger.Error(err, "reconcile program failure", "App Name", r.currentApp.Name, "BpfFunctionName", prog.BpfFunctionName, "Type", prog.Type) + } else { + updateSimpleStatus(&progState.ProgramAttachStatus, bpfmaniov1alpha1.BpfProgCondAttachSuccess) + r.Logger.Info("reconcile program success", "App Name", r.currentApp.Name, "BpfFunctionName", prog.BpfFunctionName, "Type", prog.Type) + } + + r.Logger.Info("Done reconciling program", "Application", appProgramIndex, "Name", r.currentAppState.GetName()) + } + + // We've completed reconciling all programs and if something has + // changed, we need to create or update the BpfNsApplicationState. + specChanged, err := r.updateBpfAppStateSpec(ctx, bpfAppStateOriginal, bpfAppStateNew) + if err != nil { + r.Logger.Error(err, "failed to update BpfNsApplicationState", "Name", r.currentAppState.Name) + r.updateStatus(ctx, r, bpfmaniov1alpha1.ProgramReconcileError) + // If there was an error updating the object, request a requeue + // because we can't be sure what was updated and whether the manager + // will requeue us without the request. + return ctrl.Result{Requeue: true, RequeueAfter: retryDurationAgent}, nil + } + + statusChanged := r.updateStatus(ctx, r, bpfApplicationStatus) + + if specChanged || statusChanged { + r.Logger.Info("BpfNsApplicationState updated", "Name", r.currentAppState.Name, "Spec Changed", + specChanged, "Status Changed", statusChanged) + return ctrl.Result{}, nil + } + + if r.isBeingDeleted() { + r.Logger.Info("BpfNsApplication is being deleted", "Name", r.currentApp.Name) + if r.removeFinalizer(ctx, r.currentAppState, r.finalizer) { + return ctrl.Result{}, nil + } + } + + // Nothing changed, so continue with next BpfNsApplication object. + r.Logger.Info("No changes to BpfNsApplicationState object", "Name", r.currentAppState.Name) + } + + // We're done with all the BpfNsApplication objects, so we can return. + r.Logger.Info("All BpfNsApplication objects have been reconciled") + return ctrl.Result{}, nil +} + +// getProgState returns the BpfNsApplicationProgramState object for the current node. +func (r *BpfNsApplicationReconciler) getProgState(prog *bpfmaniov1alpha1.BpfNsApplicationProgram, + programs []bpfmaniov1alpha1.BpfNsApplicationProgramState) (*bpfmaniov1alpha1.BpfNsApplicationProgramState, error) { + // ANF-TODO: Finish implementing for other program types. + for i := range programs { + progState := &programs[i] + if progState.Type == prog.Type && progState.BpfFunctionName == prog.BpfFunctionName { + return progState, nil + } + } + return nil, fmt.Errorf("BpfNsApplicationProgramState not found") +} + +// updateBpfAppStateSpec creates or updates the BpfNsApplicationState object if it is +// new or has changed. It returns true if the object was created or updated, and +// an error if the API call fails. If true is returned without an error, the +// reconciler should return immediately because a new reconcile will be +// triggered. If an error is returned, the code should return and request a +// requeue because it's uncertain whether a reconcile will be triggered. If +// false is returned without an error, the reconciler may continue reconciling +// because nothing was changed. +func (r *BpfNsApplicationReconciler) updateBpfAppStateSpec(ctx context.Context, originalAppState *bpfmaniov1alpha1.BpfNsApplicationState, + bpfAppStateNew bool) (bool, error) { + + // We've completed reconciling this program and something has + // changed. We need to create or update the BpfNsApplicationState. + if bpfAppStateNew { + // Create a new BpfNsApplicationState + r.currentAppState.Spec.UpdateCount = 1 + r.Logger.Info("Creating new BpfNsApplicationState object", "Name", r.currentAppState.Name, + "bpfAppStateNew", bpfAppStateNew, "UpdateCount", r.currentAppState.Spec.UpdateCount) + if err := r.Create(ctx, r.currentAppState); err != nil { + r.Logger.Error(err, "failed to create BpfNsApplicationState") + return true, err + } + return r.waitForBpfAppStateUpdate(ctx) + } else if !reflect.DeepEqual(originalAppState.Spec, r.currentAppState.Spec) { + // Update the BpfNsApplicationState + r.currentAppState.Spec.UpdateCount = r.currentAppState.Spec.UpdateCount + 1 + r.Logger.Info("Updating BpfNsApplicationState object", "Name", r.currentAppState.Name, "bpfAppStateNew", bpfAppStateNew, "UpdateCount", r.currentAppState.Spec.UpdateCount) + if err := r.Update(ctx, r.currentAppState); err != nil { + r.Logger.Error(err, "failed to update BpfNsApplicationState") + return true, err + } + return r.waitForBpfAppStateUpdate(ctx) + } + return false, nil +} + +// waitForBpfAppStateUpdate waits for the new BpfNsApplicationState object to be ready. +// bpfman saves state in the BpfNsApplicationState object that controls what needs +// to be done, so it is critical for each reconcile attempt to have the updated +// information. However, it takes time for objects to be created or updated, and +// for the API server to be able to return the update. I've seen cases where +// the new object isn't ready when a reconcile is launched too soon after an +// update. A field called "UpdateCount" is used to ensure we get the updated +// object. Kubernetes maintains a similar value called "Generation" which we +// might be able to use instead, but I'm not 100% sure I can trust it yet. When +// waitForBpfAppStateUpdate gets the updated object, it also updates r.currentAppState +// so the object can be used for subsequent operations (like a status update). +// From observations so far on kind, the updated object is sometimes ready on +// the first try, and sometimes it takes one more try. I've not seen it take +// more than one retry. waitForBpfAppStateUpdate currently waits for up to 10 seconds +// (100 * 100ms). +func (r *BpfNsApplicationReconciler) waitForBpfAppStateUpdate(ctx context.Context) (bool, error) { + const maxRetries = 100 + const retryInterval = 100 * time.Millisecond + + var bpfAppState *bpfmaniov1alpha1.BpfNsApplicationState + var err error + r.Logger.Info("waitForBpfAppState()", "UpdateCount", r.currentAppState.Spec.UpdateCount, "currentGeneration", r.currentAppState.GetGeneration()) + + for i := 0; i < maxRetries; i++ { + bpfAppState, _, err = r.getBpfAppState(ctx, false) + if err != nil { + // If we get an error, we'll just log it and keep trying. + r.Logger.Info("Error getting BpfNsApplicationState", "Attempt", i, "error", err) + } else if bpfAppState != nil && bpfAppState.Spec.UpdateCount >= r.currentAppState.Spec.UpdateCount { + r.Logger.Info("Found new bpfAppState", "Attempt", i, "UpdateCount", bpfAppState.Spec.UpdateCount, "currentGeneration", bpfAppState.GetGeneration()) + r.currentAppState = bpfAppState + return true, nil + } + time.Sleep(retryInterval) + } + + r.Logger.Info("Didn't find new BpfNsApplicationState", "Attempts", maxRetries) + return false, fmt.Errorf("failed to get new BpfNsApplicationState after %d retries", maxRetries) +} + +// getBpfAppState returns the BpfNsApplicationState object for the current node. If +// needed to be created, the returned bool will be true. Otherwise, it will be false. +func (r *BpfNsApplicationReconciler) getBpfAppState(ctx context.Context, createIfNotFound bool) (*bpfmaniov1alpha1.BpfNsApplicationState, bool, error) { + + appProgramList := &bpfmaniov1alpha1.BpfNsApplicationStateList{} + + opts := []client.ListOption{ + client.MatchingLabels{ + internal.BpfAppStateOwner: r.currentApp.GetName(), + internal.K8sHostLabel: r.NodeName, + }, + } + + err := r.List(ctx, appProgramList, opts...) + if err != nil { + return nil, false, err + } + + if len(appProgramList.Items) == 1 { + // We got exatly one BpfNsApplicationState, so return it + return &appProgramList.Items[0], false, nil + } + if len(appProgramList.Items) > 1 { + // This should never happen, but if it does, return an error + return nil, false, fmt.Errorf("more than one BpfNsApplicationState found (%d)", len(appProgramList.Items)) + } + // There are no BpfNsApplicationStates for this BpfNsApplication on this node. + if createIfNotFound { + return r.createBpfAppState() + } else { + return nil, false, nil + } +} + +func (r *BpfNsApplicationReconciler) createBpfAppState() (*bpfmaniov1alpha1.BpfNsApplicationState, bool, error) { + bpfAppState := &bpfmaniov1alpha1.BpfNsApplicationState{ + ObjectMeta: metav1.ObjectMeta{ + Name: generateUniqueName(r.currentApp.Name), + Namespace: r.currentApp.Namespace, + Finalizers: []string{r.finalizer}, + Labels: map[string]string{ + internal.BpfAppStateOwner: r.currentApp.GetName(), + internal.K8sHostLabel: r.NodeName, + }, + }, + Spec: bpfmaniov1alpha1.BpfNsApplicationStateSpec{ + Node: r.NodeName, + AppLoadStatus: bpfmaniov1alpha1.BpfProgCondNotLoaded, + UpdateCount: 0, + Programs: []bpfmaniov1alpha1.BpfNsApplicationProgramState{}, + }, + Status: bpfmaniov1alpha1.BpfAppStatus{Conditions: []metav1.Condition{}}, + } + + err := r.initializeNodeProgramList(bpfAppState) + if err != nil { + return nil, false, fmt.Errorf("failed to initialize BpfNsApplicationState program list: %v", err) + } + + // Make the corresponding BpfProgramConfig the owner + if err := ctrl.SetControllerReference(r.currentApp, bpfAppState, r.Scheme); err != nil { + return nil, false, fmt.Errorf("failed to set bpfAppState object owner reference: %v", err) + } + + return bpfAppState, true, nil +} + +func (r *BpfNsApplicationReconciler) initializeNodeProgramList(bpfAppState *bpfmaniov1alpha1.BpfNsApplicationState) error { + // The list should only be initialized once when the BpfNsApplication is first + // created. After that, the user can't add or remove programs. + if len(bpfAppState.Spec.Programs) != 0 { + return fmt.Errorf("BpfNsApplicationState programs list has already been initialized") + } + + for _, prog := range r.currentApp.Spec.Programs { + // Check if it's already on the list. If it is, this is an error + // because a given bpf function can only be loaded once per + // BpfNsApplication. + _, err := r.getProgState(&prog, bpfAppState.Spec.Programs) + if err == nil { + return fmt.Errorf("duplicate bpf function detected. bpfFunctionName: %s", prog.BpfFunctionName) + } + progState := bpfmaniov1alpha1.BpfNsApplicationProgramState{ + BpfProgramStateCommon: bpfmaniov1alpha1.BpfProgramStateCommon{ + BpfProgramCommon: prog.BpfProgramCommon, + ProgramAttachStatus: bpfmaniov1alpha1.BpfProgCondNotAttached, + }, + Type: prog.Type, + } + switch prog.Type { + case bpfmaniov1alpha1.ProgTypeTC: + panic(fmt.Sprintf("%v not implemented yet", prog.Type)) + case bpfmaniov1alpha1.ProgTypeTCX: + progState.TCX = &bpfmaniov1alpha1.TcxNsProgramInfoState{ + AttachPoints: []bpfmaniov1alpha1.TcxAttachInfoState{}, + } + case bpfmaniov1alpha1.ProgTypeUprobe: + panic(fmt.Sprintf("%v not implemented yet", prog.Type)) + case bpfmaniov1alpha1.ProgTypeUretprobe: + panic(fmt.Sprintf("%v not implemented yet", prog.Type)) + case bpfmaniov1alpha1.ProgTypeXDP: + progState.XDP = &bpfmaniov1alpha1.XdpNsProgramInfoState{ + AttachPoints: []bpfmaniov1alpha1.XdpAttachInfoState{}, + } + default: + panic(fmt.Sprintf("unexpected EBPFProgType: %#v", prog.Type)) + } + + bpfAppState.Spec.Programs = append(bpfAppState.Spec.Programs, progState) + } + + return nil +} diff --git a/controllers/app-agent/ns-application-program_test.go b/controllers/app-agent/ns-application-program_test.go new file mode 100644 index 000000000..ffe1334b9 --- /dev/null +++ b/controllers/app-agent/ns-application-program_test.go @@ -0,0 +1,257 @@ +package appagent + +import ( + "context" + "reflect" + "testing" + + bpfmaniov1alpha1 "github.com/bpfman/bpfman-operator/apis/v1alpha1" + agenttestutils "github.com/bpfman/bpfman-operator/controllers/app-agent/internal/test-utils" + "github.com/bpfman/bpfman-operator/internal" + testutils "github.com/bpfman/bpfman-operator/internal/test-utils" + "github.com/stretchr/testify/require" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" +) + +func TestBpfNsApplicationControllerCreate(t *testing.T) { + var ( + // global config + appProgramName = "fakeAppProgram" + namespace = "bpfman" + bytecodePath = "/tmp/hello.o" + xdpBpfFunctionName = "XdpTest" + tcxBpfFunctionName = "TcxTest" + priority = 50 + fakeNode = testutils.NewNode("fake-control-plane") + fakeInt0 = "eth0" + // fakeInt1 = "eth1" + fakePodName = "my-pod" + fakeContainerName = "my-container-1" + fakePid = int64(4490) + + ctx = context.TODO() + ) + + programs := []bpfmaniov1alpha1.BpfNsApplicationProgram{} + + fakeInts := []string{fakeInt0} + + interfaceSelector := bpfmaniov1alpha1.InterfaceSelector{ + Interfaces: &fakeInts, + } + + proceedOn := []bpfmaniov1alpha1.XdpProceedOnValue{bpfmaniov1alpha1.XdpProceedOnValue("pass"), + bpfmaniov1alpha1.XdpProceedOnValue("dispatcher_return")} + + xdpAttachInfo := bpfmaniov1alpha1.XdpNsAttachInfo{ + InterfaceSelector: interfaceSelector, + Containers: bpfmaniov1alpha1.ContainerNsSelector{ + Pods: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test", + }, + }, + }, Priority: int32(priority), + ProceedOn: proceedOn, + } + + xdpProgram := bpfmaniov1alpha1.BpfNsApplicationProgram{ + BpfProgramCommon: bpfmaniov1alpha1.BpfProgramCommon{ + BpfFunctionName: xdpBpfFunctionName, + }, + Type: bpfmaniov1alpha1.ProgTypeXDP, + XDP: &bpfmaniov1alpha1.XdpNsProgramInfo{ + AttachPoints: []bpfmaniov1alpha1.XdpNsAttachInfo{xdpAttachInfo}, + }, + } + + programs = append(programs, xdpProgram) + + tcxAttachInfo := bpfmaniov1alpha1.TcxNsAttachInfo{ + InterfaceSelector: interfaceSelector, + Containers: bpfmaniov1alpha1.ContainerNsSelector{ + Pods: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test", + }, + }, + }, Direction: "ingress", + Priority: int32(priority), + } + tcProgram := bpfmaniov1alpha1.BpfNsApplicationProgram{ + BpfProgramCommon: bpfmaniov1alpha1.BpfProgramCommon{ + BpfFunctionName: tcxBpfFunctionName, + }, + Type: bpfmaniov1alpha1.ProgTypeTCX, + TCX: &bpfmaniov1alpha1.TcxNsProgramInfo{ + AttachPoints: []bpfmaniov1alpha1.TcxNsAttachInfo{tcxAttachInfo}, + }, + } + programs = append(programs, tcProgram) + + bpfApp := &bpfmaniov1alpha1.BpfNsApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: appProgramName, + Namespace: namespace, + }, + Spec: bpfmaniov1alpha1.BpfNsApplicationSpec{ + BpfAppCommon: bpfmaniov1alpha1.BpfAppCommon{ + NodeSelector: metav1.LabelSelector{}, + ByteCode: bpfmaniov1alpha1.BytecodeSelector{ + Path: &bytecodePath, + }, + }, + Programs: programs, + }, + } + + // Objects to track in the fake client. + objs := []runtime.Object{fakeNode, bpfApp} + + // Register operator types with the runtime scheme. + s := scheme.Scheme + s.AddKnownTypes(bpfmaniov1alpha1.SchemeGroupVersion, bpfApp) + s.AddKnownTypes(bpfmaniov1alpha1.SchemeGroupVersion, &bpfmaniov1alpha1.BpfNsApplicationList{}) + s.AddKnownTypes(bpfmaniov1alpha1.SchemeGroupVersion, &bpfmaniov1alpha1.BpfNsApplication{}) + s.AddKnownTypes(bpfmaniov1alpha1.SchemeGroupVersion, &bpfmaniov1alpha1.BpfNsApplicationStateList{}) + s.AddKnownTypes(bpfmaniov1alpha1.SchemeGroupVersion, &bpfmaniov1alpha1.BpfNsApplicationState{}) + + // Create a fake client to mock API calls. + cl := fake.NewClientBuilder().WithStatusSubresource(bpfApp).WithStatusSubresource(&bpfmaniov1alpha1.BpfNsApplicationState{}).WithRuntimeObjects(objs...).Build() + + cli := agenttestutils.NewBpfmanClientFake() + + testContainers := FakeContainerGetter{ + containerList: &[]ContainerInfo{ + { + podName: fakePodName, + containerName: fakeContainerName, + pid: fakePid, + }, + }, + } + + // rc := ReconcilerCommon[bpfmaniov1alpha1.BpfNsProgram, bpfmaniov1alpha1.BpfNsProgramList]{ + // Client: cl, + // Scheme: s, + // BpfmanClient: cli, + // NodeName: fakeNode.Name, + // Containers: &testContainers, + // appOwner: App, + // } + // npr := NamespaceProgramReconciler{ + // ReconcilerCommon: rc, + // } + + rc := ReconcilerCommon{ + Client: cl, + Scheme: s, + BpfmanClient: cli, + NodeName: fakeNode.Name, + ourNode: fakeNode, + Containers: &testContainers, + } + + // Set development Logger, so we can see all logs in tests. + logf.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true}))) + + // Create a ReconcileMemcached object with the scheme and fake client. + r := &BpfNsApplicationReconciler{ + ReconcilerCommon: rc, + } + + // Mock request to simulate Reconcile() being called on an event for a + // watched resource . + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: appProgramName, + Namespace: namespace, + }, + } + + // First reconcile should create the BpfNsApplicationState object + r.Logger.Info("First reconcile") + res, err := r.Reconcile(ctx, req) + require.NoError(t, err) + + r.Logger.Info("First reconcile", "res:", res, "err:", err) + + // Require no requeue + require.False(t, res.Requeue) + + // Check the BpfNsApplicationState Object was created successfully + bpfAppState, bpfAppStateNew, err := r.getBpfAppState(ctx, false) + require.NoError(t, err) + + // Make sure we got bpfAppState from the api server and didn't create a new + // one. + require.Equal(t, false, bpfAppStateNew) + + require.Equal(t, 1, len(bpfAppState.Status.Conditions)) + require.Equal(t, string(bpfmaniov1alpha1.ProgramReconcileSuccess), bpfAppState.Status.Conditions[0].Type) + + require.Equal(t, fakeNode.Name, bpfAppState.Labels[internal.K8sHostLabel]) + + require.Equal(t, appProgramName, bpfAppState.Labels[internal.BpfAppStateOwner]) + + require.Equal(t, internal.BpfNsApplicationControllerFinalizer, bpfAppState.Finalizers[0]) + + for _, program := range bpfAppState.Spec.Programs { + r.Logger.Info("ProgramAttachStatus check", "program", program.BpfFunctionName) + require.Equal(t, bpfmaniov1alpha1.BpfProgCondAttachSuccess, program.ProgramAttachStatus) + } + + // Do a 2nd reconcile and make sure it doesn't change + r.Logger.Info("Second reconcile") + res, err = r.Reconcile(ctx, req) + require.NoError(t, err) + + // Require no requeue + require.False(t, res.Requeue) + + r.Logger.Info("Second reconcile", "res:", res, "err:", err) + + // Check the BpfNsApplicationState Object was created successfully + bpfAppState2, bpfAppStateNew, err := r.getBpfAppState(ctx, false) + require.NoError(t, err) + + // Make sure we got bpfAppState from the api server and didn't create a new + // one. + require.Equal(t, false, bpfAppStateNew) + + // Check that the bpfAppState was not updated + require.True(t, reflect.DeepEqual(bpfAppState, bpfAppState2)) + + currentXdpProgram := programs[0] + + attachPoint := bpfAppState2.Spec.Programs[0].XDP.AttachPoints[0] + + xdpReconciler := &XdpNsProgramReconciler{ + ReconcilerCommon: rc, + ProgramNsReconcilerCommon: ProgramNsReconcilerCommon{ + appCommon: bpfmaniov1alpha1.BpfAppCommon{ + NodeSelector: metav1.LabelSelector{}, + ByteCode: bpfmaniov1alpha1.BytecodeSelector{ + Path: &bytecodePath, + }, + }, + currentProgram: ¤tXdpProgram, + currentProgramState: &bpfmaniov1alpha1.BpfNsApplicationProgramState{}, + }, + currentAttachPoint: &attachPoint, + } + + loadRequest, err := xdpReconciler.getLoadRequest(nil) + require.NoError(t, err) + + require.Equal(t, xdpBpfFunctionName, loadRequest.Name) + require.Equal(t, uint32(6), loadRequest.ProgramType) +} diff --git a/controllers/app-agent/ns-tcx-program.go b/controllers/app-agent/ns-tcx-program.go new file mode 100644 index 000000000..7a5a6657c --- /dev/null +++ b/controllers/app-agent/ns-tcx-program.go @@ -0,0 +1,303 @@ +/* +Copyright 2025. + +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. +*/ + +//lint:file-ignore U1000 Linter claims functions unused, but are required for generic + +package appagent + +import ( + "context" + "fmt" + "reflect" + + bpfmaniov1alpha1 "github.com/bpfman/bpfman-operator/apis/v1alpha1" + bpfmanagentinternal "github.com/bpfman/bpfman-operator/controllers/app-agent/internal" + internal "github.com/bpfman/bpfman-operator/internal" + gobpfman "github.com/bpfman/bpfman/clients/gobpfman/v1" + "github.com/google/uuid" + + v1 "k8s.io/api/core/v1" +) + +//+kubebuilder:rbac:groups=bpfman.io,resources=tcxprograms,verbs=get;list;watch + +// TcxNsProgramReconciler contains the info required to reconcile a TcxNsProgram +type TcxNsProgramReconciler struct { + ReconcilerCommon + ProgramNsReconcilerCommon + currentAttachPoint *bpfmaniov1alpha1.TcxAttachInfoState +} + +func (r *TcxNsProgramReconciler) getProgId() *uint32 { + return r.currentProgramState.ProgramId +} + +func (r *TcxNsProgramReconciler) getProgType() internal.ProgramType { + return internal.Tc +} + +func (r *TcxNsProgramReconciler) getNode() *v1.Node { + return r.ourNode +} + +func (r *TcxNsProgramReconciler) getBpfGlobalData() map[string][]byte { + return r.appCommon.GlobalData +} + +func (r *TcxNsProgramReconciler) shouldAttach() bool { + return r.currentAttachPoint.ShouldAttach +} + +func (r *TcxNsProgramReconciler) getUUID() string { + return r.currentAttachPoint.UUID +} + +func (r *TcxNsProgramReconciler) getAttachId() *uint32 { + return r.currentAttachPoint.AttachId +} + +func (r *TcxNsProgramReconciler) setAttachId(id *uint32) { + r.currentAttachPoint.AttachId = id +} + +func (r *TcxNsProgramReconciler) setAttachStatus(status bpfmaniov1alpha1.BpfProgramConditionType) bool { + return updateSimpleStatus(&r.currentAttachPoint.AttachStatus, status) +} + +func (r *TcxNsProgramReconciler) getAttachStatus() bpfmaniov1alpha1.BpfProgramConditionType { + return r.currentAttachPoint.AttachStatus +} + +func (r *TcxNsProgramReconciler) getNamespace() string { + return r.namespace +} + +func (r *TcxNsProgramReconciler) getLoadRequest(mapOwnerId *uint32) (*gobpfman.LoadRequest, error) { + + r.Logger.Info("Getting load request", "bpfFunctionName", r.currentProgram.BpfFunctionName, "reqAttachInfo", r.currentAttachPoint, "mapOwnerId", + mapOwnerId, "ByteCode", r.appCommon.ByteCode) + + bytecode, err := bpfmanagentinternal.GetBytecode(r.Client, &r.appCommon.ByteCode) + if err != nil { + return nil, fmt.Errorf("failed to process bytecode selector: %v", err) + } + + attachInfo := &gobpfman.TCXAttachInfo{ + Priority: r.currentAttachPoint.Priority, + Iface: r.currentAttachPoint.IfName, + Direction: r.currentAttachPoint.Direction, + } + + if r.currentAttachPoint.ContainerPid != nil { + netns := fmt.Sprintf("/host/proc/%d/ns/net", *r.currentAttachPoint.ContainerPid) + attachInfo.Netns = &netns + } + + // ANF-TODO: This is a temporary workaround for backwards compatibility. + // Fix it after old code removed. + var bpfFunctionName string + if r.currentProgram.BpfFunctionName != "" { + bpfFunctionName = r.currentProgram.BpfFunctionName + } else { + bpfFunctionName = r.currentProgram.TCX.BpfFunctionName + } + + loadRequest := gobpfman.LoadRequest{ + Bytecode: bytecode, + Name: bpfFunctionName, + ProgramType: uint32(internal.Tc), + Attach: &gobpfman.AttachInfo{ + Info: &gobpfman.AttachInfo_TcxAttachInfo{ + TcxAttachInfo: attachInfo, + }, + }, + Metadata: map[string]string{internal.UuidMetadataKey: string(r.currentAttachPoint.UUID), internal.ProgramNameKey: "BpfApplication"}, + GlobalData: r.appCommon.GlobalData, + MapOwnerId: mapOwnerId, + } + + return &loadRequest, nil +} + +// updateAttachInfo processes the *ProgramInfo and updates the list of attach +// points contained in *AttachInfoState. +func (r *TcxNsProgramReconciler) updateAttachInfo(ctx context.Context, isBeingDeleted bool) error { + r.Logger.Info("TCX updateAttachInfo()", "isBeingDeleted", isBeingDeleted) + + // Set ShouldAttach for all attach points in the node CRD to false. We'll + // update this in the next step for all attach points that are still + // present. + for i := range r.currentProgramState.TCX.AttachPoints { + r.currentProgramState.TCX.AttachPoints[i].ShouldAttach = false + } + + if isBeingDeleted { + // If the program is being deleted, we don't need to do anything else. + // + // ANF-TODO: When we have load/attach split, we shouldn't even need to + // set ShouldAttach to false above, because unloading the program should + // remove all attachments and updateAttachInfo won't be called. We + // probably should delete AttachPoints when unloading the program. + return nil + } + + for _, attachInfo := range r.currentProgram.TCX.AttachPoints { + expectedAttachPoints, error := r.getExpectedAttachPoints(ctx, attachInfo) + if error != nil { + return fmt.Errorf("failed to get node attach points: %v", error) + } + for _, attachPoint := range expectedAttachPoints { + index := r.findAttachPoint(attachPoint) + if index != nil { + // Attach point already exists, so set ShouldAttach to true. + r.currentProgramState.TCX.AttachPoints[*index].AttachInfoCommon.ShouldAttach = true + } else { + // Attach point doesn't exist, so add it. + r.Logger.Info("Attach point doesn't exist. Adding it.") + r.currentProgramState.TCX.AttachPoints = append(r.currentProgramState.TCX.AttachPoints, attachPoint) + } + } + } + + // If any existing attach point is no longer on a list of expected attach + // points, ShouldAttach will remain set to false and it will get detached in + // a following step. + + return nil +} + +// ANF-TODO: Confirm what constitutes a match between two attach points. E.g., +// what if everything the same, but the priority and/or proceed_on values are +// different? +func (r *TcxNsProgramReconciler) findAttachPoint(attachInfoState bpfmaniov1alpha1.TcxAttachInfoState) *int { + for i, a := range r.currentProgramState.TCX.AttachPoints { + // attachInfoState is the same as a if the the following fields are the + // same: IfName, ContainerPid, Priority, and Direction. + if a.IfName == attachInfoState.IfName && reflect.DeepEqual(a.ContainerPid, attachInfoState.ContainerPid) && + a.Priority == attachInfoState.Priority && a.Direction == attachInfoState.Direction { + return &i + } + } + return nil +} + +// processAttachInfo processes the attach points in *AttachInfoState. Based on +// the current state, it calls bpfman to attach or detach, or does nothing if +// the state is correct. It returns a boolean indicating if any changes were +// made. +// +// ANF-TODO: Generalize this function and move it into common. +func (r *TcxNsProgramReconciler) processAttachInfo(ctx context.Context, mapOwnerStatus *MapOwnerParamStatus) error { + r.Logger.Info("Processing attach info", "bpfFunctionName", r.currentProgram.TCX.BpfFunctionName, + "mapOwnerStatus", mapOwnerStatus) + + // Get existing ebpf state from bpfman. + loadedBpfPrograms, err := bpfmanagentinternal.ListBpfmanPrograms(ctx, r.BpfmanClient, internal.Tc) + if err != nil { + r.Logger.Error(err, "failed to list loaded bpfman programs") + updateSimpleStatus(&r.currentProgramState.ProgramAttachStatus, bpfmaniov1alpha1.BpfProgCondAttachError) + return fmt.Errorf("failed to list loaded bpfman programs: %v", err) + } + + // The following map is used to keep track of attach points that need to be + // removed. If it's not empty at the end of the loop, we'll remove the + // attach points. + attachPointsToRemove := make(map[int]bool) + + var lastReconcileAttachmentError error = nil + for i := range r.currentProgramState.TCX.AttachPoints { + r.currentAttachPoint = &r.currentProgramState.TCX.AttachPoints[i] + remove, err := r.reconcileBpfAttachment(ctx, r, loadedBpfPrograms, mapOwnerStatus) + if err != nil { + r.Logger.Error(err, "failed to reconcile bpf attachment", "index", i) + // All errors are logged, but the last error is saved to return and + // we continue to process the rest of the attach points so errors + // don't block valid attach points. + lastReconcileAttachmentError = err + } + + if remove { + r.Logger.Info("Marking attach point for removal", "index", i) + attachPointsToRemove[i] = true + } + } + + if len(attachPointsToRemove) > 0 { + r.Logger.Info("Removing attach points", "attachPointsToRemove", attachPointsToRemove) + r.currentProgramState.TCX.AttachPoints = r.removeAttachPoints(r.currentProgramState.TCX.AttachPoints, attachPointsToRemove) + } + + return lastReconcileAttachmentError +} + +// removeAttachPoints removes attach points from a slice of attach points based on the keys in the map. +func (r *TcxNsProgramReconciler) removeAttachPoints(attachPoints []bpfmaniov1alpha1.TcxAttachInfoState, attachPointsToRemove map[int]bool) []bpfmaniov1alpha1.TcxAttachInfoState { + var remainingAttachPoints []bpfmaniov1alpha1.TcxAttachInfoState + for i, a := range attachPoints { + if _, ok := attachPointsToRemove[i]; !ok { + remainingAttachPoints = append(remainingAttachPoints, a) + } + } + return remainingAttachPoints +} + +// getExpectedAttachPoints expands *AttachInfo into a list of specific attach +// points. +func (r *TcxNsProgramReconciler) getExpectedAttachPoints(ctx context.Context, attachInfo bpfmaniov1alpha1.TcxNsAttachInfo, +) ([]bpfmaniov1alpha1.TcxAttachInfoState, error) { + interfaces, err := getInterfaces(&attachInfo.InterfaceSelector, r.ourNode) + if err != nil { + return nil, fmt.Errorf("failed to get interfaces for TcxNsProgram: %v", err) + } + + nodeAttachPoints := []bpfmaniov1alpha1.TcxAttachInfoState{} + + containerInfo, err := r.Containers.GetContainers( + ctx, + r.getNamespace(), + attachInfo.Containers.Pods, + attachInfo.Containers.ContainerNames, + r.Logger, + ) + if err != nil { + return nil, fmt.Errorf("failed to get container pids: %v", err) + } + + if containerInfo != nil && len(*containerInfo) != 0 { + // Containers were found, so create attach points. + for i := range *containerInfo { + container := (*containerInfo)[i] + for _, iface := range interfaces { + containerPid := uint32(container.pid) + attachPoint := bpfmaniov1alpha1.TcxAttachInfoState{ + AttachInfoCommon: bpfmaniov1alpha1.AttachInfoCommon{ + ShouldAttach: true, + UUID: uuid.New().String(), + AttachId: nil, + AttachStatus: bpfmaniov1alpha1.BpfProgCondNotAttached, + }, + IfName: iface, + ContainerPid: &containerPid, + Priority: attachInfo.Priority, + Direction: attachInfo.Direction, + } + nodeAttachPoints = append(nodeAttachPoints, attachPoint) + } + } + } + + return nodeAttachPoints, nil +} diff --git a/controllers/app-agent/ns-xdp-program.go b/controllers/app-agent/ns-xdp-program.go new file mode 100644 index 000000000..e1a9ca15a --- /dev/null +++ b/controllers/app-agent/ns-xdp-program.go @@ -0,0 +1,302 @@ +/* +Copyright 2025. + +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. +*/ + +//lint:file-ignore U1000 Linter claims functions unused, but are required for generic + +package appagent + +import ( + "context" + "fmt" + "reflect" + + bpfmaniov1alpha1 "github.com/bpfman/bpfman-operator/apis/v1alpha1" + bpfmanagentinternal "github.com/bpfman/bpfman-operator/controllers/app-agent/internal" + internal "github.com/bpfman/bpfman-operator/internal" + gobpfman "github.com/bpfman/bpfman/clients/gobpfman/v1" + "github.com/google/uuid" + + v1 "k8s.io/api/core/v1" +) + +//+kubebuilder:rbac:groups=bpfman.io,resources=xdpprograms,verbs=get;list;watch + +// XdpNsProgramReconciler contains the info required to reconcile an XdpNsProgram +type XdpNsProgramReconciler struct { + ReconcilerCommon + ProgramNsReconcilerCommon + currentAttachPoint *bpfmaniov1alpha1.XdpAttachInfoState +} + +func (r *XdpNsProgramReconciler) getProgId() *uint32 { + return r.currentProgramState.ProgramId +} + +func (r *XdpNsProgramReconciler) getProgType() internal.ProgramType { + return internal.Xdp +} + +func (r *XdpNsProgramReconciler) getNode() *v1.Node { + return r.ourNode +} + +func (r *XdpNsProgramReconciler) getBpfGlobalData() map[string][]byte { + return r.appCommon.GlobalData +} + +func (r *XdpNsProgramReconciler) shouldAttach() bool { + return r.currentAttachPoint.ShouldAttach +} + +func (r *XdpNsProgramReconciler) getUUID() string { + return r.currentAttachPoint.UUID +} + +func (r *XdpNsProgramReconciler) getAttachId() *uint32 { + return r.currentAttachPoint.AttachId +} + +func (r *XdpNsProgramReconciler) setAttachId(id *uint32) { + r.currentAttachPoint.AttachId = id +} + +func (r *XdpNsProgramReconciler) setAttachStatus(status bpfmaniov1alpha1.BpfProgramConditionType) bool { + return updateSimpleStatus(&r.currentAttachPoint.AttachStatus, status) +} + +func (r *XdpNsProgramReconciler) getAttachStatus() bpfmaniov1alpha1.BpfProgramConditionType { + return r.currentAttachPoint.AttachStatus +} + +func (r *XdpNsProgramReconciler) getNamespace() string { + return r.namespace +} + +func (r *XdpNsProgramReconciler) getLoadRequest(mapOwnerId *uint32) (*gobpfman.LoadRequest, error) { + + r.Logger.Info("Getting load request", "bpfFunctionName", r.currentProgram.BpfFunctionName, "reqAttachInfo", r.currentAttachPoint, "mapOwnerId", + mapOwnerId, "ByteCode", r.appCommon.ByteCode) + + bytecode, err := bpfmanagentinternal.GetBytecode(r.Client, &r.appCommon.ByteCode) + if err != nil { + return nil, fmt.Errorf("failed to process bytecode selector: %v", err) + } + + attachInfo := &gobpfman.XDPAttachInfo{ + Priority: r.currentAttachPoint.Priority, + Iface: r.currentAttachPoint.IfName, + ProceedOn: xdpProceedOnToInt(r.currentAttachPoint.ProceedOn), + } + + if r.currentAttachPoint.ContainerPid != nil { + netns := fmt.Sprintf("/host/proc/%d/ns/net", *r.currentAttachPoint.ContainerPid) + attachInfo.Netns = &netns + } + + // ANF-TODO: This is a temporary workaround for backwards compatibility. + // Fix it after old code removed. + var bpfFunctionName string + if r.currentProgram.BpfFunctionName != "" { + bpfFunctionName = r.currentProgram.BpfFunctionName + } else { + bpfFunctionName = r.currentProgram.XDP.BpfFunctionName + } + + loadRequest := gobpfman.LoadRequest{ + Bytecode: bytecode, + Name: bpfFunctionName, + ProgramType: uint32(internal.Xdp), + Attach: &gobpfman.AttachInfo{ + Info: &gobpfman.AttachInfo_XdpAttachInfo{ + XdpAttachInfo: attachInfo, + }, + }, + Metadata: map[string]string{internal.UuidMetadataKey: string(r.currentAttachPoint.UUID), internal.ProgramNameKey: "BpfApplication"}, + GlobalData: r.appCommon.GlobalData, + MapOwnerId: mapOwnerId, + } + + return &loadRequest, nil +} + +// updateAttachInfo processes the *ProgramInfo and updates the list of attach +// points contained in *AttachInfoState. +func (r *XdpNsProgramReconciler) updateAttachInfo(ctx context.Context, isBeingDeleted bool) error { + r.Logger.Info("XDP updateAttachInfo()", "isBeingDeleted", isBeingDeleted) + + // Set ShouldAttach for all attach points in the node CRD to false. We'll + // update this in the next step for all attach points that are still + // present. + for i := range r.currentProgramState.XDP.AttachPoints { + r.currentProgramState.XDP.AttachPoints[i].ShouldAttach = false + } + + if isBeingDeleted { + // If the program is being deleted, we don't need to do anything else. + // + // ANF-TODO: When we have load/attach split, we shouldn't even need to + // set ShouldAttach to false above, because unloading the program should + // remove all attachments and updateAttachInfo won't be called. We + // probably should delete AttachPoints when unloading the program. + return nil + } + + for _, attachInfo := range r.currentProgram.XDP.AttachPoints { + expectedAttachPoints, error := r.getExpectedAttachPoints(ctx, attachInfo) + if error != nil { + return fmt.Errorf("failed to get node attach points: %v", error) + } + for _, attachPoint := range expectedAttachPoints { + index := r.findAttachPoint(attachPoint) + if index != nil { + // Attach point already exists, so set ShouldAttach to true. + r.currentProgramState.XDP.AttachPoints[*index].AttachInfoCommon.ShouldAttach = true + } else { + // Attach point doesn't exist, so add it. + r.currentProgramState.XDP.AttachPoints = append(r.currentProgramState.XDP.AttachPoints, attachPoint) + } + } + } + + // If any existing attach point is no longer on a list of expected attach + // points, ShouldAttach will remain set to false and it will get detached in + // a following step. + + return nil +} + +// ANF-TODO: Confirm what constitutes a match between two attach points. E.g., +// what if everything the same, but the priority and/or proceed_on values are +// different? +func (r *XdpNsProgramReconciler) findAttachPoint(attachInfoState bpfmaniov1alpha1.XdpAttachInfoState) *int { + for i, a := range r.currentProgramState.XDP.AttachPoints { + // attachInfoState is the same as a if the the following fields are the + // same: IfName, ContainerPid, Priority, and ProceedOn. + if a.IfName == attachInfoState.IfName && reflect.DeepEqual(a.ContainerPid, attachInfoState.ContainerPid) && + a.Priority == attachInfoState.Priority && reflect.DeepEqual(a.ProceedOn, attachInfoState.ProceedOn) { + return &i + } + } + return nil +} + +// processAttachInfo processes the attach points in *AttachInfoState. Based on +// the current state, it calls bpfman to attach or detach, or does nothing if +// the state is correct. It returns a boolean indicating if any changes were +// made. +// +// ANF-TODO: Generalize this function and move it into common. +func (r *XdpNsProgramReconciler) processAttachInfo(ctx context.Context, mapOwnerStatus *MapOwnerParamStatus) error { + r.Logger.Info("Processing attach info", "bpfFunctionName", r.currentProgram.XDP.BpfFunctionName, + "mapOwnerStatus", mapOwnerStatus) + + // Get existing ebpf state from bpfman. + loadedBpfPrograms, err := bpfmanagentinternal.ListBpfmanPrograms(ctx, r.BpfmanClient, internal.Xdp) + if err != nil { + r.Logger.Error(err, "failed to list loaded bpfman programs") + updateSimpleStatus(&r.currentProgramState.ProgramAttachStatus, bpfmaniov1alpha1.BpfProgCondAttachError) + return fmt.Errorf("failed to list loaded bpfman programs: %v", err) + } + + // The following map is used to keep track of attach points that need to be + // removed. If it's not empty at the end of the loop, we'll remove the + // attach points. + attachPointsToRemove := make(map[int]bool) + + var lastReconcileAttachmentError error = nil + for i := range r.currentProgramState.XDP.AttachPoints { + r.currentAttachPoint = &r.currentProgramState.XDP.AttachPoints[i] + remove, err := r.reconcileBpfAttachment(ctx, r, loadedBpfPrograms, mapOwnerStatus) + if err != nil { + r.Logger.Error(err, "failed to reconcile bpf attachment", "index", i) + // All errors are logged, but the last error is saved to return and + // we continue to process the rest of the attach points so errors + // don't block valid attach points. + lastReconcileAttachmentError = err + } + + if remove { + r.Logger.Info("Marking attach point for removal", "index", i) + attachPointsToRemove[i] = true + } + } + + if len(attachPointsToRemove) > 0 { + r.Logger.Info("Removing attach points", "attachPointsToRemove", attachPointsToRemove) + r.currentProgramState.XDP.AttachPoints = r.removeAttachPoints(r.currentProgramState.XDP.AttachPoints, attachPointsToRemove) + } + + return lastReconcileAttachmentError +} + +// removeAttachPoints removes attach points from a slice of attach points based on the keys in the map. +func (r *XdpNsProgramReconciler) removeAttachPoints(attachPoints []bpfmaniov1alpha1.XdpAttachInfoState, attachPointsToRemove map[int]bool) []bpfmaniov1alpha1.XdpAttachInfoState { + var newAttachPoints []bpfmaniov1alpha1.XdpAttachInfoState + for i, a := range attachPoints { + if _, ok := attachPointsToRemove[i]; !ok { + newAttachPoints = append(newAttachPoints, a) + } + } + return newAttachPoints +} + +// getExpectedAttachPoints expands *AttachInfo into a list of specific attach +// points. +func (r *XdpNsProgramReconciler) getExpectedAttachPoints(ctx context.Context, attachInfo bpfmaniov1alpha1.XdpNsAttachInfo, +) ([]bpfmaniov1alpha1.XdpAttachInfoState, error) { + interfaces, err := getInterfaces(&attachInfo.InterfaceSelector, r.ourNode) + if err != nil { + return nil, fmt.Errorf("failed to get interfaces for XdpNsProgram: %v", err) + } + + nodeAttachPoints := []bpfmaniov1alpha1.XdpAttachInfoState{} + + containerInfo, err := r.Containers.GetContainers( + ctx, + r.getNamespace(), + attachInfo.Containers.Pods, + attachInfo.Containers.ContainerNames, + r.Logger, + ) + if err != nil { + return nil, fmt.Errorf("failed to get container pids: %v", err) + } + + if containerInfo != nil && len(*containerInfo) != 0 { + // Containers were found, so create attach points. + for i := range *containerInfo { + container := (*containerInfo)[i] + for _, iface := range interfaces { + containerPid := uint32(container.pid) + attachPoint := bpfmaniov1alpha1.XdpAttachInfoState{ + AttachInfoCommon: bpfmaniov1alpha1.AttachInfoCommon{ + ShouldAttach: true, + UUID: uuid.New().String(), + AttachId: nil, + AttachStatus: bpfmaniov1alpha1.BpfProgCondNotAttached, + }, + IfName: iface, + ContainerPid: &containerPid, + Priority: attachInfo.Priority, + ProceedOn: attachInfo.ProceedOn, + } + nodeAttachPoints = append(nodeAttachPoints, attachPoint) + } + } + } + + return nodeAttachPoints, nil +} diff --git a/controllers/app-agent/test_common.go b/controllers/app-agent/test_common.go new file mode 100644 index 000000000..6f896cd0e --- /dev/null +++ b/controllers/app-agent/test_common.go @@ -0,0 +1,97 @@ +package appagent + +import ( + "context" + "testing" + + bpfmaniov1alpha1 "github.com/bpfman/bpfman-operator/apis/v1alpha1" + "github.com/go-logr/logr" + + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientGoFake "k8s.io/client-go/kubernetes/fake" +) + +type FakeContainerGetter struct { + containerList *[]ContainerInfo +} + +func (f *FakeContainerGetter) GetContainers( + ctx context.Context, + selectorNamespace string, + selectorPods metav1.LabelSelector, + selectorContainerNames *[]string, + logger logr.Logger, +) (*[]ContainerInfo, error) { + return f.containerList, nil +} + +func TestGetPods(t *testing.T) { + ctx := context.TODO() + + // Create a fake clientset + clientset := clientGoFake.NewSimpleClientset() + + // Create a ContainerSelector + containerSelector := &bpfmaniov1alpha1.ContainerSelector{ + Pods: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test", + }, + }, + Namespace: "default", + } + + nodeName := "test-node" + + // Create a Pod that matches the label selector and is on the correct node + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + Labels: map[string]string{ + "app": "test", + }, + }, + Spec: v1.PodSpec{ + NodeName: nodeName, + }, + } + + containerGetter := RealContainerGetter{ + nodeName: nodeName, + clientSet: clientset, + } + + // Add the Pod to the fake clientset + _, err := clientset.CoreV1().Pods("default").Create(ctx, pod, metav1.CreateOptions{}) + require.NoError(t, err) + + // Call getPods and check the returned PodList + podList, err := containerGetter.getPodsForNode( + ctx, + containerSelector.Namespace, + containerSelector.Pods, + ) + require.NoError(t, err) + require.Len(t, podList.Items, 1) + require.Equal(t, "test-pod", podList.Items[0].Name) + + // Try another selector + // Create a ContainerSelector + containerSelector = &bpfmaniov1alpha1.ContainerSelector{ + Pods: metav1.LabelSelector{ + MatchLabels: map[string]string{}, + }, + } + + podList, err = containerGetter.getPodsForNode( + ctx, + containerSelector.Namespace, + containerSelector.Pods, + ) + require.NoError(t, err) + require.Len(t, podList.Items, 1) + require.Equal(t, "test-pod", podList.Items[0].Name) +} diff --git a/controllers/app-operator/application-cl-program_test.go b/controllers/app-operator/cl-application-program_test.go similarity index 100% rename from controllers/app-operator/application-cl-program_test.go rename to controllers/app-operator/cl-application-program_test.go diff --git a/controllers/app-operator/application-cl-programs.go b/controllers/app-operator/cl-application-programs.go similarity index 100% rename from controllers/app-operator/application-cl-programs.go rename to controllers/app-operator/cl-application-programs.go diff --git a/controllers/app-operator/common.go b/controllers/app-operator/common.go index ef3b5981c..584903247 100644 --- a/controllers/app-operator/common.go +++ b/controllers/app-operator/common.go @@ -36,8 +36,8 @@ import ( ) // +kubebuilder:rbac:groups=bpfman.io,resources=bpfapplicationstates,verbs=get;list;watch -// +kubebuilder:rbac:groups=bpfman.io,resources=bpfnsprograms,verbs=get;list;watch -// +kubebuilder:rbac:groups=bpfman.io,namespace=bpfman,resources=bpfnsprograms,verbs=get;list;watch +// +kubebuilder:rbac:groups=bpfman.io,resources=bpfnsapplicationstates,verbs=get;list;watch +// +kubebuilder:rbac:groups=bpfman.io,namespace=bpfman,resources=bpfnsapplicationstates,verbs=get;list;watch // +kubebuilder:rbac:groups=core,resources=nodes,verbs=get;list;watch const ( diff --git a/controllers/app-operator/common_namespace.go b/controllers/app-operator/common_namespace.go new file mode 100644 index 000000000..9c938e407 --- /dev/null +++ b/controllers/app-operator/common_namespace.go @@ -0,0 +1,84 @@ +/* +Copyright 2024. + +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 appoperator + +import ( + "context" + "reflect" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + bpfmaniov1alpha1 "github.com/bpfman/bpfman-operator/apis/v1alpha1" + internal "github.com/bpfman/bpfman-operator/internal" +) + +type NamespaceProgramReconciler struct { + ReconcilerCommon[bpfmaniov1alpha1.BpfNsApplicationState, bpfmaniov1alpha1.BpfNsApplicationStateList] +} + +//lint:ignore U1000 Linter claims function unused, but generics confusing linter +func (r *NamespaceProgramReconciler) getBpfList( + ctx context.Context, + progName string, + progNamespace string, +) (*bpfmaniov1alpha1.BpfNsApplicationStateList, error) { + + bpfProgramList := &bpfmaniov1alpha1.BpfNsApplicationStateList{} + + // Only list bpfPrograms for this Program + opts := []client.ListOption{ + client.MatchingLabels{internal.BpfProgramOwner: progName}, + client.InNamespace(progNamespace), + } + + err := r.List(ctx, bpfProgramList, opts...) + if err != nil { + return nil, err + } + + return bpfProgramList, nil +} + +//lint:ignore U1000 Linter claims function unused, but generics confusing linter +func (r *NamespaceProgramReconciler) containsFinalizer( + bpfProgram *bpfmaniov1alpha1.BpfNsApplicationState, + finalizer string, +) bool { + return controllerutil.ContainsFinalizer(bpfProgram, finalizer) +} + +func statusChangedPredicateNamespace() predicate.Funcs { + return predicate.Funcs{ + GenericFunc: func(e event.GenericEvent) bool { + return false + }, + CreateFunc: func(e event.CreateEvent) bool { + return false + }, + UpdateFunc: func(e event.UpdateEvent) bool { + oldObject := e.ObjectOld.(*bpfmaniov1alpha1.BpfNsApplicationState) + newObject := e.ObjectNew.(*bpfmaniov1alpha1.BpfNsApplicationState) + return !reflect.DeepEqual(oldObject.GetStatus(), newObject.Status) + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return false + }, + } +} diff --git a/controllers/app-operator/ns-application-program_test.go b/controllers/app-operator/ns-application-program_test.go new file mode 100644 index 000000000..c4706049b --- /dev/null +++ b/controllers/app-operator/ns-application-program_test.go @@ -0,0 +1,279 @@ +/* +Copyright 2024. + +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 appoperator + +import ( + "context" + "fmt" + "testing" + + bpfmaniov1alpha1 "github.com/bpfman/bpfman-operator/apis/v1alpha1" + internal "github.com/bpfman/bpfman-operator/internal" + testutils "github.com/bpfman/bpfman-operator/internal/test-utils" + + "github.com/stretchr/testify/require" + meta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +// Runs the ApplicationProgramReconcile test. If multiCondition == true, it runs it +// with an error case in which the program object has multiple conditions. +func appNsProgramReconcile(t *testing.T, multiCondition bool) { + var ( + name = "fakeAppProgram" + namespace = "bpfman" + bytecodePath = "/tmp/hello.o" + // bpfTcFunctionName = "tc_test" + // bpfUprobeFunctionName = "uprobe_test" + xdpBpfFunctionName = "xdp-test" + fakeNode = testutils.NewNode("fake-control-plane") + // tcFakeInt = "eth0" + // tcDirection = "ingress" + // uprobeFunctionName = "malloc" + // uprobeTarget = "libc" + // uprobeOffset = 0 + // uprobeRetprobe = false + fakeInt = "eth0" + xdpPriority = 50 + ctx = context.TODO() + bpfProgName = fmt.Sprintf("%s-%s", name, fakeNode.Name) + ) + + // { + // Type: bpfmaniov1alpha1.ProgTypeTC, + // TC: &bpfmaniov1alpha1.TcNsProgramInfo{ + // BpfProgramCommon: bpfmaniov1alpha1.BpfProgramCommon{ + // BpfFunctionName: bpfTcFunctionName, + // }, + // AttachPoints: []bpfmaniov1alpha1.TcNsAttachInfo{ + // { + // InterfaceSelector: bpfmaniov1alpha1.InterfaceSelector{ + // Interfaces: &[]string{tcFakeInt}, + // }, + // Priority: 0, + // Direction: tcDirection, + // ProceedOn: []bpfmaniov1alpha1.TcProceedOnValue{ + // bpfmaniov1alpha1.TcProceedOnValue("pipe"), + // bpfmaniov1alpha1.TcProceedOnValue("dispatcher_return"), + // }, + // }, + // }, + // }, + // }, + // { + // Type: bpfmaniov1alpha1.ProgTypeUprobe, + // Uprobe: &bpfmaniov1alpha1.UprobeNsProgramInfo{ + // BpfProgramCommon: bpfmaniov1alpha1.BpfProgramCommon{ + // BpfFunctionName: bpfUprobeFunctionName, + // }, + // AttachPoints: []bpfmaniov1alpha1.UprobeNsAttachInfo{ + // { + // FunctionName: uprobeFunctionName, + // Target: uprobeTarget, + // Offset: uint64(uprobeOffset), + // RetProbe: uprobeRetprobe, + // // Containers: bpfmaniov1alpha1.ContainerNsSelector{ + // // Pods: metav1.LabelSelector{ + // // MatchLabels: map[string]string{ + // // "app": "test", + // // }, + // // }, + // // }, + // }, + // }, + // }, + // }, + + fakeInts := []string{fakeInt} + + interfaceSelector := bpfmaniov1alpha1.InterfaceSelector{ + Interfaces: &fakeInts, + } + + proceedOn := []bpfmaniov1alpha1.XdpProceedOnValue{bpfmaniov1alpha1.XdpProceedOnValue("pass"), + bpfmaniov1alpha1.XdpProceedOnValue("dispatcher_return")} + + attachInfo := bpfmaniov1alpha1.XdpNsAttachInfo{ + InterfaceSelector: interfaceSelector, + Containers: bpfmaniov1alpha1.ContainerNsSelector{ + Pods: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test", + }, + }, + }, + Priority: int32(xdpPriority), + ProceedOn: proceedOn, + } + + programs := []bpfmaniov1alpha1.BpfNsApplicationProgram{} + + xdpProgram := bpfmaniov1alpha1.BpfNsApplicationProgram{ + BpfProgramCommon: bpfmaniov1alpha1.BpfProgramCommon{ + BpfFunctionName: xdpBpfFunctionName, + }, + Type: bpfmaniov1alpha1.ProgTypeXDP, + XDP: &bpfmaniov1alpha1.XdpNsProgramInfo{ + AttachPoints: []bpfmaniov1alpha1.XdpNsAttachInfo{attachInfo}, + }, + } + + programs = append(programs, xdpProgram) + + // A AppProgram object with metadata and spec. + App := &bpfmaniov1alpha1.BpfNsApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: bpfmaniov1alpha1.BpfNsApplicationSpec{ + BpfAppCommon: bpfmaniov1alpha1.BpfAppCommon{ + NodeSelector: metav1.LabelSelector{}, + ByteCode: bpfmaniov1alpha1.BytecodeSelector{ + Path: &bytecodePath, + }, + }, + Programs: programs, + }, + } + + // The expected accompanying BpfNsApplicationState object + expectedBpfProg := &bpfmaniov1alpha1.BpfNsApplicationState{ + ObjectMeta: metav1.ObjectMeta{ + Name: bpfProgName, + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{ + { + Name: App.Name, + Controller: &[]bool{true}[0], + }, + }, + Labels: map[string]string{internal.BpfProgramOwner: App.Name, internal.K8sHostLabel: fakeNode.Name}, + Finalizers: []string{internal.BpfNsApplicationControllerFinalizer}, + }, + Spec: bpfmaniov1alpha1.BpfNsApplicationStateSpec{ + AppLoadStatus: bpfmaniov1alpha1.BpfProgCondLoaded, + Programs: []bpfmaniov1alpha1.BpfNsApplicationProgramState{}, + }, + Status: bpfmaniov1alpha1.BpfAppStatus{ + Conditions: []metav1.Condition{bpfmaniov1alpha1.ProgramReconcileSuccess.Condition("")}, + }, + } + + // Objects to track in the fake client. + objs := []runtime.Object{fakeNode, App, expectedBpfProg} + + // Register operator types with the runtime scheme. + s := scheme.Scheme + s.AddKnownTypes(bpfmaniov1alpha1.SchemeGroupVersion, App) + s.AddKnownTypes(bpfmaniov1alpha1.SchemeGroupVersion, &bpfmaniov1alpha1.BpfNsApplicationState{}) + s.AddKnownTypes(bpfmaniov1alpha1.SchemeGroupVersion, &bpfmaniov1alpha1.BpfNsApplicationStateList{}) + + // Create a fake client to mock API calls. + cl := fake.NewClientBuilder().WithStatusSubresource(App).WithRuntimeObjects(objs...).Build() + + rc := ReconcilerCommon[bpfmaniov1alpha1.BpfNsApplicationState, bpfmaniov1alpha1.BpfNsApplicationStateList]{ + Client: cl, + Scheme: s, + } + + npr := NamespaceProgramReconciler{ + ReconcilerCommon: rc, + } + + // Set development Logger so we can see all logs in tests. + logf.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true}))) + + // Create a ApplicationProgram object with the scheme and fake client. + r := &BpfNsApplicationReconciler{NamespaceProgramReconciler: npr} + + // Mock request to simulate Reconcile() being called on an event for a + // watched resource . + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: name, + Namespace: namespace, + }, + } + + // First reconcile should add the finalizer to the applicationProgram object + res, err := r.Reconcile(ctx, req) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + + // Require no requeue + require.False(t, res.Requeue) + + // Check the BpfNsApplicationState Object was created successfully + err = cl.Get(ctx, types.NamespacedName{Name: App.Name, Namespace: App.Namespace}, App) + require.NoError(t, err) + + // Check the bpfman-operator finalizer was successfully added + require.Contains(t, App.GetFinalizers(), internal.BpfmanOperatorFinalizer) + + // NOTE: THIS IS A TEST FOR AN ERROR PATH. THERE SHOULD NEVER BE MORE THAN + // ONE CONDITION. + if multiCondition { + // Add some random conditions and verify that the condition still gets + // updated correctly. + meta.SetStatusCondition(&App.Status.Conditions, bpfmaniov1alpha1.ProgramDeleteError.Condition("bogus condition #1")) + if err := r.Status().Update(ctx, App); err != nil { + r.Logger.V(1).Info("failed to set App Ns Program object status") + } + meta.SetStatusCondition(&App.Status.Conditions, bpfmaniov1alpha1.ProgramReconcileError.Condition("bogus condition #2")) + if err := r.Status().Update(ctx, App); err != nil { + r.Logger.V(1).Info("failed to set App Ns Program object status") + } + // Make sure we have 2 conditions + require.Equal(t, 2, len(App.Status.Conditions)) + } + + // Second reconcile should check BpfNsApplicationState Status and write Success condition to TcNsProgram Status + res, err = r.Reconcile(ctx, req) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + + // Require no requeue + require.False(t, res.Requeue) + + // Check the BpfNsApplicationState Object was created successfully + err = cl.Get(ctx, types.NamespacedName{Name: App.Name, Namespace: App.Namespace}, App) + require.NoError(t, err) + + // Make sure we only have 1 condition now + require.Equal(t, 1, len(App.Status.Conditions)) + // Make sure it's the right one. + require.Equal(t, App.Status.Conditions[0].Type, string(bpfmaniov1alpha1.ProgramReconcileSuccess)) +} + +func TestAppNsProgramReconcile(t *testing.T) { + appNsProgramReconcile(t, false) +} + +func TestAppNsUpdateStatus(t *testing.T) { + appNsProgramReconcile(t, true) +} diff --git a/controllers/app-operator/ns-application-programs.go b/controllers/app-operator/ns-application-programs.go new file mode 100644 index 000000000..5d3884040 --- /dev/null +++ b/controllers/app-operator/ns-application-programs.go @@ -0,0 +1,135 @@ +/* +Copyright 2024 The bpfman 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 appoperator + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/handler" + + bpfmaniov1alpha1 "github.com/bpfman/bpfman-operator/apis/v1alpha1" + internal "github.com/bpfman/bpfman-operator/internal" +) + +//+kubebuilder:rbac:groups=bpfman.io,resources=bpfnsapplications,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=bpfman.io,resources=bpfnsapplications/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=bpfman.io,resources=bpfnsapplications/finalizers,verbs=update +//+kubebuilder:rbac:groups=bpfman.io,namespace=bpfman,resources=bpfnsapplications,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=bpfman.io,namespace=bpfman,resources=bpfnsapplications/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=bpfman.io,namespace=bpfman,resources=bpfnsapplications/finalizers,verbs=update + +// BpfNsApplicationReconciler reconciles a BpfNsApplication object +type BpfNsApplicationReconciler struct { + NamespaceProgramReconciler +} + +//lint:ignore U1000 Linter claims function unused, but generics confusing linter +func (r *BpfNsApplicationReconciler) getRecCommon() *ReconcilerCommon[bpfmaniov1alpha1.BpfNsApplicationState, bpfmaniov1alpha1.BpfNsApplicationStateList] { + return &r.NamespaceProgramReconciler.ReconcilerCommon +} + +//lint:ignore U1000 Linter claims function unused, but generics confusing linter +func (r *BpfNsApplicationReconciler) getFinalizer() string { + return internal.BpfNsApplicationControllerFinalizer +} + +// SetupWithManager sets up the controller with the Manager. +func (r *BpfNsApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&bpfmaniov1alpha1.BpfNsApplication{}). + // Watch BpfNsApplicationStates which are owned by BpfNsApplications + Watches( + &bpfmaniov1alpha1.BpfNsApplicationState{}, + &handler.EnqueueRequestForObject{}, + builder.WithPredicates(statusChangedPredicateNamespace()), + ). + Complete(r) +} + +func (r *BpfNsApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.Logger = ctrl.Log.WithName("application") + r.Logger.Info("bpfman-operator enter: application-ns", + "Namespace", req.NamespacedName.Namespace, "Name", req.NamespacedName.Name) + + appProgram := &bpfmaniov1alpha1.BpfNsApplication{} + if err := r.Get(ctx, req.NamespacedName, appProgram); err != nil { + // Reconcile was triggered by BpfNsApplicationState event, get parent appProgram Object. + if errors.IsNotFound(err) { + bpfProgram := &bpfmaniov1alpha1.BpfNsApplicationState{} + if err := r.Get(ctx, req.NamespacedName, bpfProgram); err != nil { + if errors.IsNotFound(err) { + r.Logger.V(1).Info("BpfNsApplicationState not found stale reconcile, exiting", + "Namespace", req.NamespacedName.Namespace, "Name", req.NamespacedName.Name) + } else { + r.Logger.Error(err, "failed getting BpfNsApplicationState Object", + "Namespace", req.NamespacedName.Namespace, "Name", req.NamespacedName.Name) + } + return ctrl.Result{}, nil + } + + // Get owning appProgram object from ownerRef + ownerRef := metav1.GetControllerOf(bpfProgram) + if ownerRef == nil { + return ctrl.Result{Requeue: false}, fmt.Errorf("failed getting BpfNsApplicationState Object owner") + } + + if err := r.Get(ctx, types.NamespacedName{Namespace: req.NamespacedName.Namespace, Name: ownerRef.Name}, appProgram); err != nil { + if errors.IsNotFound(err) { + r.Logger.Info("Application Programs from ownerRef not found stale reconcile exiting", + "Namespace", req.NamespacedName.Namespace, "Name", req.NamespacedName.Name) + } else { + r.Logger.Error(err, "failed getting Application Programs Object from ownerRef", + "Namespace", req.NamespacedName.Namespace, "Name", req.NamespacedName.Name) + } + return ctrl.Result{}, nil + } + + } else { + r.Logger.Error(err, "failed getting Application Programs Object", + "Namespace", req.NamespacedName.Namespace, "Name", req.NamespacedName.Name) + return ctrl.Result{}, nil + } + } + + return reconcileBpfProgram(ctx, r, appProgram) +} + +//lint:ignore U1000 Linter claims function unused, but generics confusing linter +func (r *BpfNsApplicationReconciler) updateStatus( + ctx context.Context, + namespace string, + name string, + cond bpfmaniov1alpha1.ProgramConditionType, + message string, +) (ctrl.Result, error) { + // Sometimes we end up with a stale FentryProgram due to races, do this + // get to ensure we're up to date before attempting a status update. + app := &bpfmaniov1alpha1.BpfNsApplication{} + if err := r.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, app); err != nil { + r.Logger.V(1).Info("failed to get fresh Application Programs object...requeuing") + return ctrl.Result{Requeue: true, RequeueAfter: retryDurationOperator}, nil + } + + return r.updateCondition(ctx, app, &app.Status.Conditions, cond, message) +} diff --git a/hack/namespace_scoped.yaml b/hack/namespace_scoped.yaml index 3e48015ea..62185f624 100644 --- a/hack/namespace_scoped.yaml +++ b/hack/namespace_scoped.yaml @@ -27,7 +27,7 @@ rules: - apiGroups: - bpfman.io resources: - - bpfnsprograms + - bpfnsapplicationstates verbs: - get - list @@ -35,7 +35,7 @@ rules: - apiGroups: - bpfman.io resources: - - bpfnsprograms/status + - bpfnsapplicationstates/status verbs: - get - list @@ -43,7 +43,7 @@ rules: - apiGroups: - bpfman.io # resources: ['xdpnsprograms'] - resources: ['bpfnsapplications', 'tcnsprograms', 'tcxnsprograms', 'uprobensprograms', 'xdpnsprograms'] + resources: ['bpfnsapplications'] verbs: - create - delete diff --git a/pkg/client/apis/v1alpha1/bpfnsapplicationstate.go b/pkg/client/apis/v1alpha1/bpfnsapplicationstate.go new file mode 100644 index 000000000..537b7e199 --- /dev/null +++ b/pkg/client/apis/v1alpha1/bpfnsapplicationstate.go @@ -0,0 +1,99 @@ +/* +Copyright 2023 The bpfman 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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/bpfman/bpfman-operator/apis/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// BpfNsApplicationStateLister helps list BpfNsApplicationStates. +// All objects returned here must be treated as read-only. +type BpfNsApplicationStateLister interface { + // List lists all BpfNsApplicationStates in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.BpfNsApplicationState, err error) + // BpfNsApplicationStates returns an object that can list and get BpfNsApplicationStates. + BpfNsApplicationStates(namespace string) BpfNsApplicationStateNamespaceLister + BpfNsApplicationStateListerExpansion +} + +// bpfNsApplicationStateLister implements the BpfNsApplicationStateLister interface. +type bpfNsApplicationStateLister struct { + indexer cache.Indexer +} + +// NewBpfNsApplicationStateLister returns a new BpfNsApplicationStateLister. +func NewBpfNsApplicationStateLister(indexer cache.Indexer) BpfNsApplicationStateLister { + return &bpfNsApplicationStateLister{indexer: indexer} +} + +// List lists all BpfNsApplicationStates in the indexer. +func (s *bpfNsApplicationStateLister) List(selector labels.Selector) (ret []*v1alpha1.BpfNsApplicationState, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.BpfNsApplicationState)) + }) + return ret, err +} + +// BpfNsApplicationStates returns an object that can list and get BpfNsApplicationStates. +func (s *bpfNsApplicationStateLister) BpfNsApplicationStates(namespace string) BpfNsApplicationStateNamespaceLister { + return bpfNsApplicationStateNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// BpfNsApplicationStateNamespaceLister helps list and get BpfNsApplicationStates. +// All objects returned here must be treated as read-only. +type BpfNsApplicationStateNamespaceLister interface { + // List lists all BpfNsApplicationStates in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.BpfNsApplicationState, err error) + // Get retrieves the BpfNsApplicationState from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.BpfNsApplicationState, error) + BpfNsApplicationStateNamespaceListerExpansion +} + +// bpfNsApplicationStateNamespaceLister implements the BpfNsApplicationStateNamespaceLister +// interface. +type bpfNsApplicationStateNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all BpfNsApplicationStates in the indexer for a given namespace. +func (s bpfNsApplicationStateNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.BpfNsApplicationState, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.BpfNsApplicationState)) + }) + return ret, err +} + +// Get retrieves the BpfNsApplicationState from the indexer for a given namespace and name. +func (s bpfNsApplicationStateNamespaceLister) Get(name string) (*v1alpha1.BpfNsApplicationState, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("bpfnsapplicationstate"), name) + } + return obj.(*v1alpha1.BpfNsApplicationState), nil +} diff --git a/pkg/client/apis/v1alpha1/expansion_generated.go b/pkg/client/apis/v1alpha1/expansion_generated.go index 17040799e..e114f7b53 100644 --- a/pkg/client/apis/v1alpha1/expansion_generated.go +++ b/pkg/client/apis/v1alpha1/expansion_generated.go @@ -34,6 +34,14 @@ type BpfNsApplicationListerExpansion interface{} // BpfNsApplicationNamespaceLister. type BpfNsApplicationNamespaceListerExpansion interface{} +// BpfNsApplicationStateListerExpansion allows custom methods to be added to +// BpfNsApplicationStateLister. +type BpfNsApplicationStateListerExpansion interface{} + +// BpfNsApplicationStateNamespaceListerExpansion allows custom methods to be added to +// BpfNsApplicationStateNamespaceLister. +type BpfNsApplicationStateNamespaceListerExpansion interface{} + // BpfNsProgramListerExpansion allows custom methods to be added to // BpfNsProgramLister. type BpfNsProgramListerExpansion interface{} diff --git a/pkg/client/clientset/typed/apis/v1alpha1/apis_client.go b/pkg/client/clientset/typed/apis/v1alpha1/apis_client.go index 140883ed5..addccfe45 100644 --- a/pkg/client/clientset/typed/apis/v1alpha1/apis_client.go +++ b/pkg/client/clientset/typed/apis/v1alpha1/apis_client.go @@ -31,6 +31,7 @@ type BpfmanV1alpha1Interface interface { BpfApplicationsGetter BpfApplicationStatesGetter BpfNsApplicationsGetter + BpfNsApplicationStatesGetter BpfNsProgramsGetter BpfProgramsGetter FentryProgramsGetter @@ -64,6 +65,10 @@ func (c *BpfmanV1alpha1Client) BpfNsApplications(namespace string) BpfNsApplicat return newBpfNsApplications(c, namespace) } +func (c *BpfmanV1alpha1Client) BpfNsApplicationStates(namespace string) BpfNsApplicationStateInterface { + return newBpfNsApplicationStates(c, namespace) +} + func (c *BpfmanV1alpha1Client) BpfNsPrograms(namespace string) BpfNsProgramInterface { return newBpfNsPrograms(c, namespace) } diff --git a/pkg/client/clientset/typed/apis/v1alpha1/bpfnsapplicationstate.go b/pkg/client/clientset/typed/apis/v1alpha1/bpfnsapplicationstate.go new file mode 100644 index 000000000..95134349b --- /dev/null +++ b/pkg/client/clientset/typed/apis/v1alpha1/bpfnsapplicationstate.go @@ -0,0 +1,195 @@ +/* +Copyright 2023 The bpfman 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "github.com/bpfman/bpfman-operator/apis/v1alpha1" + scheme "github.com/bpfman/bpfman-operator/pkg/client/clientset/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// BpfNsApplicationStatesGetter has a method to return a BpfNsApplicationStateInterface. +// A group's client should implement this interface. +type BpfNsApplicationStatesGetter interface { + BpfNsApplicationStates(namespace string) BpfNsApplicationStateInterface +} + +// BpfNsApplicationStateInterface has methods to work with BpfNsApplicationState resources. +type BpfNsApplicationStateInterface interface { + Create(ctx context.Context, bpfNsApplicationState *v1alpha1.BpfNsApplicationState, opts v1.CreateOptions) (*v1alpha1.BpfNsApplicationState, error) + Update(ctx context.Context, bpfNsApplicationState *v1alpha1.BpfNsApplicationState, opts v1.UpdateOptions) (*v1alpha1.BpfNsApplicationState, error) + UpdateStatus(ctx context.Context, bpfNsApplicationState *v1alpha1.BpfNsApplicationState, opts v1.UpdateOptions) (*v1alpha1.BpfNsApplicationState, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.BpfNsApplicationState, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.BpfNsApplicationStateList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.BpfNsApplicationState, err error) + BpfNsApplicationStateExpansion +} + +// bpfNsApplicationStates implements BpfNsApplicationStateInterface +type bpfNsApplicationStates struct { + client rest.Interface + ns string +} + +// newBpfNsApplicationStates returns a BpfNsApplicationStates +func newBpfNsApplicationStates(c *BpfmanV1alpha1Client, namespace string) *bpfNsApplicationStates { + return &bpfNsApplicationStates{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the bpfNsApplicationState, and returns the corresponding bpfNsApplicationState object, and an error if there is any. +func (c *bpfNsApplicationStates) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.BpfNsApplicationState, err error) { + result = &v1alpha1.BpfNsApplicationState{} + err = c.client.Get(). + Namespace(c.ns). + Resource("bpfnsapplicationstates"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of BpfNsApplicationStates that match those selectors. +func (c *bpfNsApplicationStates) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.BpfNsApplicationStateList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.BpfNsApplicationStateList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("bpfnsapplicationstates"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested bpfNsApplicationStates. +func (c *bpfNsApplicationStates) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("bpfnsapplicationstates"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a bpfNsApplicationState and creates it. Returns the server's representation of the bpfNsApplicationState, and an error, if there is any. +func (c *bpfNsApplicationStates) Create(ctx context.Context, bpfNsApplicationState *v1alpha1.BpfNsApplicationState, opts v1.CreateOptions) (result *v1alpha1.BpfNsApplicationState, err error) { + result = &v1alpha1.BpfNsApplicationState{} + err = c.client.Post(). + Namespace(c.ns). + Resource("bpfnsapplicationstates"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(bpfNsApplicationState). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a bpfNsApplicationState and updates it. Returns the server's representation of the bpfNsApplicationState, and an error, if there is any. +func (c *bpfNsApplicationStates) Update(ctx context.Context, bpfNsApplicationState *v1alpha1.BpfNsApplicationState, opts v1.UpdateOptions) (result *v1alpha1.BpfNsApplicationState, err error) { + result = &v1alpha1.BpfNsApplicationState{} + err = c.client.Put(). + Namespace(c.ns). + Resource("bpfnsapplicationstates"). + Name(bpfNsApplicationState.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(bpfNsApplicationState). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *bpfNsApplicationStates) UpdateStatus(ctx context.Context, bpfNsApplicationState *v1alpha1.BpfNsApplicationState, opts v1.UpdateOptions) (result *v1alpha1.BpfNsApplicationState, err error) { + result = &v1alpha1.BpfNsApplicationState{} + err = c.client.Put(). + Namespace(c.ns). + Resource("bpfnsapplicationstates"). + Name(bpfNsApplicationState.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(bpfNsApplicationState). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the bpfNsApplicationState and deletes it. Returns an error if one occurs. +func (c *bpfNsApplicationStates) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("bpfnsapplicationstates"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *bpfNsApplicationStates) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("bpfnsapplicationstates"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched bpfNsApplicationState. +func (c *bpfNsApplicationStates) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.BpfNsApplicationState, err error) { + result = &v1alpha1.BpfNsApplicationState{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("bpfnsapplicationstates"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/client/clientset/typed/apis/v1alpha1/fake/fake_apis_client.go b/pkg/client/clientset/typed/apis/v1alpha1/fake/fake_apis_client.go index 9f6914c20..dfb258321 100644 --- a/pkg/client/clientset/typed/apis/v1alpha1/fake/fake_apis_client.go +++ b/pkg/client/clientset/typed/apis/v1alpha1/fake/fake_apis_client.go @@ -40,6 +40,10 @@ func (c *FakeBpfmanV1alpha1) BpfNsApplications(namespace string) v1alpha1.BpfNsA return &FakeBpfNsApplications{c, namespace} } +func (c *FakeBpfmanV1alpha1) BpfNsApplicationStates(namespace string) v1alpha1.BpfNsApplicationStateInterface { + return &FakeBpfNsApplicationStates{c, namespace} +} + func (c *FakeBpfmanV1alpha1) BpfNsPrograms(namespace string) v1alpha1.BpfNsProgramInterface { return &FakeBpfNsPrograms{c, namespace} } diff --git a/pkg/client/clientset/typed/apis/v1alpha1/fake/fake_bpfnsapplicationstate.go b/pkg/client/clientset/typed/apis/v1alpha1/fake/fake_bpfnsapplicationstate.go new file mode 100644 index 000000000..087c3ca8b --- /dev/null +++ b/pkg/client/clientset/typed/apis/v1alpha1/fake/fake_bpfnsapplicationstate.go @@ -0,0 +1,141 @@ +/* +Copyright 2023 The bpfman 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "github.com/bpfman/bpfman-operator/apis/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeBpfNsApplicationStates implements BpfNsApplicationStateInterface +type FakeBpfNsApplicationStates struct { + Fake *FakeBpfmanV1alpha1 + ns string +} + +var bpfnsapplicationstatesResource = v1alpha1.SchemeGroupVersion.WithResource("bpfnsapplicationstates") + +var bpfnsapplicationstatesKind = v1alpha1.SchemeGroupVersion.WithKind("BpfNsApplicationState") + +// Get takes name of the bpfNsApplicationState, and returns the corresponding bpfNsApplicationState object, and an error if there is any. +func (c *FakeBpfNsApplicationStates) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.BpfNsApplicationState, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(bpfnsapplicationstatesResource, c.ns, name), &v1alpha1.BpfNsApplicationState{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.BpfNsApplicationState), err +} + +// List takes label and field selectors, and returns the list of BpfNsApplicationStates that match those selectors. +func (c *FakeBpfNsApplicationStates) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.BpfNsApplicationStateList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(bpfnsapplicationstatesResource, bpfnsapplicationstatesKind, c.ns, opts), &v1alpha1.BpfNsApplicationStateList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.BpfNsApplicationStateList{ListMeta: obj.(*v1alpha1.BpfNsApplicationStateList).ListMeta} + for _, item := range obj.(*v1alpha1.BpfNsApplicationStateList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested bpfNsApplicationStates. +func (c *FakeBpfNsApplicationStates) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(bpfnsapplicationstatesResource, c.ns, opts)) + +} + +// Create takes the representation of a bpfNsApplicationState and creates it. Returns the server's representation of the bpfNsApplicationState, and an error, if there is any. +func (c *FakeBpfNsApplicationStates) Create(ctx context.Context, bpfNsApplicationState *v1alpha1.BpfNsApplicationState, opts v1.CreateOptions) (result *v1alpha1.BpfNsApplicationState, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(bpfnsapplicationstatesResource, c.ns, bpfNsApplicationState), &v1alpha1.BpfNsApplicationState{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.BpfNsApplicationState), err +} + +// Update takes the representation of a bpfNsApplicationState and updates it. Returns the server's representation of the bpfNsApplicationState, and an error, if there is any. +func (c *FakeBpfNsApplicationStates) Update(ctx context.Context, bpfNsApplicationState *v1alpha1.BpfNsApplicationState, opts v1.UpdateOptions) (result *v1alpha1.BpfNsApplicationState, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(bpfnsapplicationstatesResource, c.ns, bpfNsApplicationState), &v1alpha1.BpfNsApplicationState{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.BpfNsApplicationState), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeBpfNsApplicationStates) UpdateStatus(ctx context.Context, bpfNsApplicationState *v1alpha1.BpfNsApplicationState, opts v1.UpdateOptions) (*v1alpha1.BpfNsApplicationState, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(bpfnsapplicationstatesResource, "status", c.ns, bpfNsApplicationState), &v1alpha1.BpfNsApplicationState{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.BpfNsApplicationState), err +} + +// Delete takes name of the bpfNsApplicationState and deletes it. Returns an error if one occurs. +func (c *FakeBpfNsApplicationStates) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(bpfnsapplicationstatesResource, c.ns, name, opts), &v1alpha1.BpfNsApplicationState{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeBpfNsApplicationStates) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(bpfnsapplicationstatesResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.BpfNsApplicationStateList{}) + return err +} + +// Patch applies the patch and returns the patched bpfNsApplicationState. +func (c *FakeBpfNsApplicationStates) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.BpfNsApplicationState, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(bpfnsapplicationstatesResource, c.ns, name, pt, data, subresources...), &v1alpha1.BpfNsApplicationState{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.BpfNsApplicationState), err +} diff --git a/pkg/client/clientset/typed/apis/v1alpha1/generated_expansion.go b/pkg/client/clientset/typed/apis/v1alpha1/generated_expansion.go index 212285d6c..1dea0377d 100644 --- a/pkg/client/clientset/typed/apis/v1alpha1/generated_expansion.go +++ b/pkg/client/clientset/typed/apis/v1alpha1/generated_expansion.go @@ -24,6 +24,8 @@ type BpfApplicationStateExpansion interface{} type BpfNsApplicationExpansion interface{} +type BpfNsApplicationStateExpansion interface{} + type BpfNsProgramExpansion interface{} type BpfProgramExpansion interface{} diff --git a/pkg/client/externalversions/apis/v1alpha1/bpfnsapplicationstate.go b/pkg/client/externalversions/apis/v1alpha1/bpfnsapplicationstate.go new file mode 100644 index 000000000..e8aee4633 --- /dev/null +++ b/pkg/client/externalversions/apis/v1alpha1/bpfnsapplicationstate.go @@ -0,0 +1,90 @@ +/* +Copyright 2023 The bpfman 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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + apisv1alpha1 "github.com/bpfman/bpfman-operator/apis/v1alpha1" + v1alpha1 "github.com/bpfman/bpfman-operator/pkg/client/apis/v1alpha1" + clientset "github.com/bpfman/bpfman-operator/pkg/client/clientset" + internalinterfaces "github.com/bpfman/bpfman-operator/pkg/client/externalversions/internalinterfaces" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// BpfNsApplicationStateInformer provides access to a shared informer and lister for +// BpfNsApplicationStates. +type BpfNsApplicationStateInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.BpfNsApplicationStateLister +} + +type bpfNsApplicationStateInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewBpfNsApplicationStateInformer constructs a new informer for BpfNsApplicationState type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewBpfNsApplicationStateInformer(client clientset.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredBpfNsApplicationStateInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredBpfNsApplicationStateInformer constructs a new informer for BpfNsApplicationState type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredBpfNsApplicationStateInformer(client clientset.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.BpfmanV1alpha1().BpfNsApplicationStates(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.BpfmanV1alpha1().BpfNsApplicationStates(namespace).Watch(context.TODO(), options) + }, + }, + &apisv1alpha1.BpfNsApplicationState{}, + resyncPeriod, + indexers, + ) +} + +func (f *bpfNsApplicationStateInformer) defaultInformer(client clientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredBpfNsApplicationStateInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *bpfNsApplicationStateInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apisv1alpha1.BpfNsApplicationState{}, f.defaultInformer) +} + +func (f *bpfNsApplicationStateInformer) Lister() v1alpha1.BpfNsApplicationStateLister { + return v1alpha1.NewBpfNsApplicationStateLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/externalversions/apis/v1alpha1/interface.go b/pkg/client/externalversions/apis/v1alpha1/interface.go index b53d7323f..fdff7b152 100644 --- a/pkg/client/externalversions/apis/v1alpha1/interface.go +++ b/pkg/client/externalversions/apis/v1alpha1/interface.go @@ -30,6 +30,8 @@ type Interface interface { BpfApplicationStates() BpfApplicationStateInformer // BpfNsApplications returns a BpfNsApplicationInformer. BpfNsApplications() BpfNsApplicationInformer + // BpfNsApplicationStates returns a BpfNsApplicationStateInformer. + BpfNsApplicationStates() BpfNsApplicationStateInformer // BpfNsPrograms returns a BpfNsProgramInformer. BpfNsPrograms() BpfNsProgramInformer // BpfPrograms returns a BpfProgramInformer. @@ -86,6 +88,11 @@ func (v *version) BpfNsApplications() BpfNsApplicationInformer { return &bpfNsApplicationInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// BpfNsApplicationStates returns a BpfNsApplicationStateInformer. +func (v *version) BpfNsApplicationStates() BpfNsApplicationStateInformer { + return &bpfNsApplicationStateInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // BpfNsPrograms returns a BpfNsProgramInformer. func (v *version) BpfNsPrograms() BpfNsProgramInformer { return &bpfNsProgramInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/pkg/client/externalversions/generic.go b/pkg/client/externalversions/generic.go index efb05511c..2363b488d 100644 --- a/pkg/client/externalversions/generic.go +++ b/pkg/client/externalversions/generic.go @@ -59,6 +59,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Bpfman().V1alpha1().BpfApplicationStates().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("bpfnsapplications"): return &genericInformer{resource: resource.GroupResource(), informer: f.Bpfman().V1alpha1().BpfNsApplications().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("bpfnsapplicationstates"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Bpfman().V1alpha1().BpfNsApplicationStates().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("bpfnsprograms"): return &genericInformer{resource: resource.GroupResource(), informer: f.Bpfman().V1alpha1().BpfNsPrograms().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("bpfprograms"):